blob: 3a6836cab1dc8014fc921217fdf81dd3feefefa0 [file]
#include "rive/animation/keyframe_interpolator.hpp"
#include "rive/artboard.hpp"
#include "rive/artboard_component_list.hpp"
#include "rive/constraints/layout_constraint.hpp"
#include "rive/drawable.hpp"
#include "rive/factory.hpp"
#include "rive/intrinsically_sizeable.hpp"
#include "rive/layout_component.hpp"
#include "rive/component_origin.hpp"
#include "rive/layout/grid_track.hpp"
#include "rive/layout/layout_style_applier.hpp"
#include "rive/nested_artboard_layout.hpp"
#include "rive/node.hpp"
#include "rive/math/aabb.hpp"
#include "rive/shapes/path.hpp"
#include "rive/shapes/paint/fill.hpp"
#include "rive/shapes/paint/shape_paint.hpp"
#include "rive/shapes/paint/stroke.hpp"
#include "rive/solo.hpp"
#include "rive/layout/layout_data.hpp"
#include "rive/layout/layout_component_style.hpp"
#ifdef WITH_RIVE_LAYOUT
#include "rive/transform_component.hpp"
#include "yoga/YGEnums.h"
#include "yoga/YGFloatOptional.h"
#endif
#include <vector>
using namespace rive;
#ifdef WITH_RIVE_LAYOUT
void LayoutComponent::addLayoutStyleApplier(LayoutStyleApplier* applier)
{
if (m_layoutData != nullptr)
{
m_layoutData->addApplier(applier);
}
}
#endif
namespace
{
// Containers transparent to layout provide no layout node and no
// sizing of their own, so the layout above descends through them. A plain group
// exposes all its children; a Solo exposes only its active one. Anything else
// (n-slicers, nested artboards, shapes) is opaque — a participant inside one
// would never be collected.
bool isTransparentLayoutContainer(Component* component)
{
return component->coreType() == NodeBase::typeKey || component->is<Solo>();
}
// An ArtboardComponentList provides a layout node unconditionally — it never
// opted in the way a participant does. Wrapping one in a group is how a file
// says "render these items but don't lay them out", freeing them to be placed
// by x/y or a follow-path constraint, so a transparent container stays opaque
// to it unless the list opts in. Everything else is seen through.
bool joinsLayoutThroughContainer(Component* component)
{
if (!component->is<ArtboardComponentList>())
{
return true;
}
auto flags =
static_cast<DrawableFlag>(component->as<Drawable>()->drawableFlags());
return (flags & DrawableFlag::ParticipatesInLayout) ==
DrawableFlag::ParticipatesInLayout;
}
// Visit every provider that belongs to a layout — its direct children, plus any
// nested inside transparent containers; depth-first, so layout order follows
// hierarchy order. Nested LayoutComponents and providers are leaves here (they
// own their subtree). Every child-walking layout pass goes through this, so a
// nested participant is collected, sized, cascaded and re-synced like a direct
// child.
template <typename F>
void forEachLayoutProvider(Component* from, F&& visit, bool nested = false)
{
// A Solo only lets its active child through.
if (from->is<Solo>())
{
if (auto* active = from->as<Solo>()->activeComponent())
{
if (auto* provider = LayoutNodeProvider::from(active))
{
if (!nested || joinsLayoutThroughContainer(active))
{
visit(active, provider);
}
}
else if (isTransparentLayoutContainer(active))
{
forEachLayoutProvider(active, visit, true);
}
}
return;
}
for (auto* child : from->as<ContainerComponent>()->children())
{
if (auto* provider = LayoutNodeProvider::from(child))
{
if (!nested || joinsLayoutThroughContainer(child))
{
visit(child, provider);
}
}
else if (isTransparentLayoutContainer(child))
{
forEachLayoutProvider(child, visit, true);
}
}
}
// The optional origin child, or nullptr. Walked rather than cached, matching
// how NestedArtboard resolves the same child in this runtime.
ComponentOrigin* originChild(const ContainerComponent* owner)
{
for (auto* child : owner->children())
{
if (child->is<ComponentOrigin>())
{
return child->as<ComponentOrigin>();
}
}
return nullptr;
}
} // namespace
#if defined(WITH_RIVE_LAYOUT) && defined(WITH_RIVE_TOOLS) && defined(DEBUG)
uint32_t LayoutData::count = 0;
#endif
float LayoutComponent::pivotOriginX() const
{
auto* origin = originChild(this);
return origin != nullptr ? origin->originX() : 0.0f;
}
float LayoutComponent::pivotOriginY() const
{
auto* origin = originChild(this);
return origin != nullptr ? origin->originY() : 0.0f;
}
void LayoutComponent::buildDependencies()
{
Super::buildDependencies();
if (parent() != nullptr)
{
parent()->addDependent(this);
}
// Set the blend mode on all the shape paints. If we ever animate this
// property, we'll need to update it in the update cycle/mark dirty when the
// blend mode changes.
for (auto paint : m_ShapePaints)
{
paint->blendMode(blendMode());
}
}
Core* LayoutComponent::hitTest(HitInfo*, const Mat2D&) { return nullptr; }
bool LayoutComponent::hitTestPoint(const Vec2D& position,
bool skipOnUnclipped,
bool isPrimaryHit)
{
Mat2D inverseWorld;
if (worldTransform().invert(&inverseWorld))
{
// If the layout is not clipped and skipOnUnclipped is true, we
// don't care about whether it contains the position
auto canSkip = skipOnUnclipped && !this->clip();
if (!canSkip)
{
auto localWorld = inverseWorld * position;
if (this->is<Artboard>())
{
auto artboard = this->as<Artboard>();
if (artboard->originX() != 0 || artboard->originY() != 0)
{
localWorld +=
Vec2D(artboard->originX() * artboard->layoutWidth(),
artboard->originY() * artboard->layoutHeight());
}
}
if (!localBounds().contains(localWorld))
{
return false;
}
}
return Drawable::hitTestPoint(position, true, isPrimaryHit);
}
return false;
}
void LayoutComponent::update(ComponentDirt value)
{
Super::update(value);
#ifdef WITH_RIVE_LAYOUT
if (value == ComponentDirt::Filthy)
{
// Use this to prevent layout animation on startup
interruptAnimation();
}
#endif
if (hasDirt(value, ComponentDirt::RenderOpacity))
{
propagateOpacity(childOpacity());
}
if (parent() != nullptr && hasDirt(value, ComponentDirt::WorldTransform))
{
Mat2D parentWorld =
parent()->is<WorldTransformComponent>()
? (parent()->as<WorldTransformComponent>())->worldTransform()
: Mat2D();
auto location = Vec2D(m_layout.left(), m_layout.top());
if (parent()->is<Artboard>())
{
auto art = parent()->as<Artboard>();
location -= Vec2D(art->layoutWidth() * art->originX(),
art->layoutHeight() * art->originY());
}
auto slot = Mat2D::fromTranslation(location);
// Apply the node's own rotation/scale on top of the layout slot,
// pivoting at the origin (default 0,0 = top-left). x/y offset:
// follow-up.
if (rotation() != 0.0f || scaleX() != 1.0f || scaleY() != 1.0f)
{
Mat2D local =
rotation() != 0.0f ? Mat2D::fromRotation(rotation()) : Mat2D();
local.scaleByValues(scaleX(), scaleY());
float ox = pivotOriginX();
float oy = pivotOriginY();
if (ox != 0.0f || oy != 0.0f)
{
float px = ox * layoutWidth();
float py = oy * layoutHeight();
local = Mat2D::multiply(
Mat2D::fromTranslate(px, py),
Mat2D::multiply(local, Mat2D::fromTranslate(-px, -py)));
}
slot = Mat2D::multiply(slot, local);
}
m_WorldTransform = Mat2D::multiply(parentWorld, slot);
updateConstraints();
}
if (hasDirt(value,
ComponentDirt::Path | ComponentDirt::WorldTransform |
ComponentDirt::LayoutStyle))
{
updateRenderPath();
}
m_positionLeftChanged = false;
m_positionTopChanged = false;
}
void LayoutComponent::widthOverride(float width, int unitValue, bool isRow)
{
m_widthOverride = width;
m_widthUnitValueOverride = unitValue;
m_parentIsRow = isRow;
markLayoutNodeDirty();
}
void LayoutComponent::heightOverride(float height, int unitValue, bool isRow)
{
m_heightOverride = height;
m_heightUnitValueOverride = unitValue;
m_parentIsRow = isRow;
markLayoutNodeDirty();
}
void LayoutComponent::parentIsRow(bool isRow)
{
m_parentIsRow = isRow;
markLayoutNodeDirty();
}
void LayoutComponent::widthIntrinsicallySizeOverride(bool intrinsic)
{
m_widthIntrinsicallySizeOverride = intrinsic;
// If we have an intrinsically sized override, set units to auto
// otherwise set to points
m_widthUnitValueOverride = intrinsic ? 3 : 1;
markLayoutNodeDirty();
}
void LayoutComponent::heightIntrinsicallySizeOverride(bool intrinsic)
{
m_heightIntrinsicallySizeOverride = intrinsic;
// If we have an intrinsically sized override, set units to auto
// otherwise set to points
m_heightUnitValueOverride = intrinsic ? 3 : 1;
markLayoutNodeDirty();
}
void LayoutComponent::forcedWidth(float width)
{
if (m_forcedWidth == width)
{
return;
}
m_forcedWidth = width;
markLayoutStyleDirty();
markLayoutNodeDirty();
}
void LayoutComponent::forcedHeight(float height)
{
if (m_forcedHeight == height)
{
return;
}
m_forcedHeight = height;
markLayoutStyleDirty();
markLayoutNodeDirty();
}
void LayoutComponent::updateConstraints()
{
if (layoutConstraints().size() > 0)
{
for (auto parentConstraint : layoutConstraints())
{
parentConstraint->constrainChild(this);
}
}
Super::updateConstraints();
}
bool LayoutComponent::overridesKeyedInterpolation(int propertyKey)
{
#ifdef WITH_RIVE_LAYOUT
if (animates())
{
switch (propertyKey)
{
case LayoutComponentBase::widthPropertyKey:
case LayoutComponentBase::heightPropertyKey:
return true;
default:
return false;
}
}
#endif
return false;
}
bool LayoutComponent::isHidden() const
{
return Super::isHidden() || isCollapsed();
}
bool LayoutComponent::isCollapsed() const
{
if (Super::isCollapsed())
{
return true;
}
#ifdef WITH_RIVE_LAYOUT
return styleDisplayHidden();
#endif
return false;
}
void LayoutComponent::propagateCollapse(bool collapse)
{
auto collapsed = collapse || isCollapsed();
for (Component* child : children())
{
child->collapse(collapsed);
}
updateCollapsables();
}
bool LayoutComponent::collapse(bool value)
{
if (!Component::collapse(value))
{
return false;
}
propagateCollapse(value);
return true;
}
#ifdef WITH_RIVE_LAYOUT
LayoutComponent::LayoutComponent() :
m_layoutData(new LayoutData()), m_proxy(this)
{
m_layoutData->node.getConfig()->setPointScaleFactor(0);
}
float LayoutComponent::gapHorizontal()
{
if (m_style == nullptr)
{
return 0;
}
return m_style->gapHorizontalUnits() == YGUnitPercent
? (m_style->gapHorizontal() / 100.0f) * layoutWidth()
: m_style->gapHorizontal();
}
float LayoutComponent::gapVertical()
{
if (m_style == nullptr)
{
return 0;
}
return m_style->gapVerticalUnits() == YGUnitPercent
? (m_style->gapVertical() / 100.0f) * layoutHeight()
: m_style->gapVertical();
}
StatusCode LayoutComponent::onAddedDirty(CoreContext* context)
{
auto code = Super::onAddedDirty(context);
if (code != StatusCode::Ok)
{
return code;
}
auto coreStyle = context->resolve(styleId());
if (coreStyle == nullptr || !coreStyle->is<LayoutComponentStyle>())
{
return StatusCode::MissingObject;
}
m_style = static_cast<LayoutComponentStyle*>(coreStyle);
addChild(m_style);
#ifdef WITH_RIVE_LAYOUT
// We contribute sizing the style cannot see; it contributes the rest.
addLayoutStyleApplier(this);
addLayoutStyleApplier(m_style);
#endif
return StatusCode::Ok;
}
StatusCode LayoutComponent::onAddedClean(CoreContext* context)
{
auto code = Super::onAddedClean(context);
if (code != StatusCode::Ok)
{
return code;
}
markLayoutStyleDirty();
syncLayoutChildren();
propagateCollapse(isCollapsed());
return StatusCode::Ok;
}
void LayoutComponent::drawProxy(Renderer* renderer)
{
if (clip())
{
renderer->save();
renderer->clipPath(m_worldPath.renderPath(this));
}
for (auto shapePaint : m_ShapePaints)
{
if (!shapePaint->shouldDraw())
{
continue;
}
auto shapePaintPath = shapePaint->pickPath(this);
if (shapePaintPath == nullptr)
{
continue;
}
shapePaint->draw(renderer, shapePaintPath, worldTransform());
}
}
void LayoutComponent::draw(Renderer* renderer)
{
// Restore clip before drawing stroke so we don't clip the stroke
if (clip())
{
renderer->restore();
}
}
void LayoutComponent::updateRenderPath()
{
if (isHidden())
{
return;
}
if (m_ShapePaints.empty() && !clip() && !m_hasForegroundDrawable)
{
return;
}
float tl = 0.0f, tr = 0.0f, br = 0.0f, bl = 0.0f;
if (style() != nullptr)
{
const bool isLTR = actualDirection() != LayoutDirection::rtl;
if (style()->linkCornerRadius())
{
tl = tr = br = bl = style()->cornerRadiusTL();
}
else
{
tl = isLTR ? style()->cornerRadiusTL() : style()->cornerRadiusTR();
tr = isLTR ? style()->cornerRadiusTR() : style()->cornerRadiusTL();
bl = isLTR ? style()->cornerRadiusBL() : style()->cornerRadiusBR();
br = isLTR ? style()->cornerRadiusBR() : style()->cornerRadiusBL();
}
}
m_backgroundRawPath.rewind();
Path::addRoundedRect(m_backgroundRawPath,
AABB{0.0f, 0.0f, m_layout.width(), m_layout.height()},
tl,
tr,
br,
bl);
m_localPath.rewind();
m_localPath.addPath(m_backgroundRawPath);
m_worldPath.rewind(false, FillRule::clockwise);
m_worldPath.addPath(m_backgroundRawPath, &m_WorldTransform);
for (auto shapePaint : m_ShapePaints)
{
if (!shapePaint->shouldDraw())
{
continue;
}
if (shapePaint->is<ShapePaint>())
{
shapePaint->as<ShapePaint>()->invalidateEffects();
}
}
}
static YGSize measureFunc(YGNode* node,
float width,
YGMeasureMode widthMode,
float height,
YGMeasureMode heightMode)
{
Vec2D size = ((LayoutComponent*)node->getContext())
->measureLayout(width,
(LayoutMeasureMode)widthMode,
height,
(LayoutMeasureMode)heightMode);
return YGSize{size.x, size.y};
}
Vec2D LayoutComponent::measureLayout(float width,
LayoutMeasureMode widthMode,
float height,
LayoutMeasureMode heightMode)
{
Vec2D size = Vec2D();
for (auto child : children())
{
if (child->is<LayoutComponent>())
{
continue;
}
auto sizeableChild = IntrinsicallySizeable::from(child);
if (sizeableChild != nullptr)
{
Vec2D measured = sizeableChild->measureLayout(width,
widthMode,
height,
heightMode);
size = Vec2D(std::max(size.x, measured.x),
std::max(size.y, measured.y));
}
}
return size;
}
bool LayoutComponent::effectiveParentIsRow()
{
if (canHaveOverrides())
{
return m_parentIsRow;
}
return layoutParent() != nullptr ? layoutParent()->mainAxisIsRow() : true;
}
bool LayoutComponent::mainAxisIsRow()
{
if (style() == nullptr)
{
return true;
}
return style()->flexDirection() == YGFlexDirectionRow ||
style()->flexDirection() == YGFlexDirectionRowReverse;
}
bool LayoutComponent::mainAxisIsColumn()
{
if (style() == nullptr)
{
return false;
}
return style()->flexDirection() == YGFlexDirectionColumn ||
style()->flexDirection() == YGFlexDirectionColumnReverse;
}
bool LayoutComponent::isLeaf()
{
bool leaf = true;
forEachLayoutProvider(this, [&leaf](Component*, LayoutNodeProvider*) {
leaf = false;
});
return leaf;
}
void* LayoutComponent::layoutNode(int index)
{
if (m_layoutData != nullptr)
{
return static_cast<void*>(&m_layoutData->node);
}
return nullptr;
}
#ifdef WITH_RIVE_LAYOUT
// Tracks come from GridTrack children, which the style cannot walk. A stack has
// no authored tracks — the style sets up its implicit cell instead.
void LayoutComponent::applyContainerStyle(YGStyle& ygStyle,
const LayoutSyncContext& context)
{
if (m_style == nullptr || m_style->isStack())
{
return;
}
GridTrack::syncContainerStyle(ygStyle, this, m_style->justifyItemsValue());
}
#endif
#ifdef WITH_RIVE_LAYOUT
// This layout's own size and how it sits in its parent's flow. On the
// component, not the style, because it reads width()/height(), the
// NestedArtboardLayout overrides and the forced dimensions.
//
// Not shared with LayoutParticipant::applyBaseStyle: the participant has no
// flexBasis, no overrides and its own fractionalWidth, so merging would change
// behaviour.
void LayoutComponent::applyBaseStyle(YGStyle& ygStyle,
const LayoutSyncContext& context)
{
if (m_style == nullptr)
{
return;
}
const bool parentIsRow = context.parentIsRow;
const bool parentIsGridLike = context.parentIsGrid;
// Derive the unit the layout engine needs from the persisted unit + scale
// type.
// - fill/hug always map to YGUnitAuto (regardless of position type) —
// these scale modes don't have a meaningful unit; the layout engine
// decides the size.
// - fixed uses the stored unit. Absolute layouts trust whatever's
// stored (e.g. the editor freezes a point value at the moment
// positionType becomes absolute, since absolute children sit outside
// flex flow).
bool isAbsolute = m_style->positionType() == YGPositionTypeAbsolute;
bool isLegacyHugEncoding =
m_style->widthScaleType() == LayoutScaleType::fixed &&
m_style->heightScaleType() == LayoutScaleType::fixed &&
m_style->intrinsicallySized() && isLeaf();
auto effectiveUnits = [isAbsolute,
isLegacyHugEncoding](LayoutScaleType scaleType,
YGUnit storedUnits) {
if (isAbsolute && scaleType != LayoutScaleType::hug)
{
return storedUnits;
}
if (scaleType != LayoutScaleType::fixed)
{
return YGUnitAuto;
}
if (storedUnits == YGUnitPoint || storedUnits == YGUnitPercent)
{
return storedUnits;
}
return isLegacyHugEncoding ? YGUnitAuto : YGUnitPoint;
};
auto realWidth = width();
auto realWidthScaleType = m_style->widthScaleType();
auto realWidthUnits =
effectiveUnits(realWidthScaleType, m_style->widthUnits());
auto realHeight = height();
auto realHeightScaleType = m_style->heightScaleType();
auto realHeightUnits =
effectiveUnits(realHeightScaleType, m_style->heightUnits());
// If we have override width/height values, use those.
// Currently we only use these for Artboards that are part of a
// NestedArtboardLayout but perhaps there will be other use cases for
// overriding in the future?
if (canHaveOverrides())
{
if (!std::isnan(m_widthOverride))
{
realWidth = m_widthOverride;
}
if (!std::isnan(m_heightOverride))
{
realHeight = m_heightOverride;
}
if (m_widthUnitValueOverride != -1)
{
realWidthUnits = YGUnit(m_widthUnitValueOverride);
switch (realWidthUnits)
{
case YGUnitPoint:
case YGUnitPercent:
realWidthScaleType = LayoutScaleType::fixed;
break;
case YGUnitAuto:
if (m_widthIntrinsicallySizeOverride)
{
realWidthScaleType = LayoutScaleType::hug;
}
else
{
realWidthScaleType = LayoutScaleType::fill;
}
break;
default:
break;
}
}
if (m_heightUnitValueOverride != -1)
{
realHeightUnits = YGUnit(m_heightUnitValueOverride);
switch (realHeightUnits)
{
case YGUnitPoint:
case YGUnitPercent:
realHeightScaleType = LayoutScaleType::fixed;
break;
case YGUnitAuto:
if (m_heightIntrinsicallySizeOverride)
{
realHeightScaleType = LayoutScaleType::hug;
}
else
{
realHeightScaleType = LayoutScaleType::fill;
}
break;
default:
break;
}
}
}
if (!std::isnan(m_forcedWidth))
{
ygStyle.dimensions()[YGDimensionWidth] =
YGValue{std::max(0.0f, m_forcedWidth), YGUnitPoint};
}
else
{
ygStyle.dimensions()[YGDimensionWidth] =
YGValue{std::max(0.0f, realWidth), realWidthUnits};
}
if (!std::isnan(m_forcedHeight))
{
ygStyle.dimensions()[YGDimensionHeight] =
YGValue{std::max(0.0f, m_forcedHeight), YGUnitPoint};
}
else
{
ygStyle.dimensions()[YGDimensionHeight] =
YGValue{std::max(0.0f, realHeight), realHeightUnits};
}
if (parentIsGridLike)
{
// Grid/stack: size per-axis. fill stretches that axis (width ->
// justifySelf after grid placement below, height -> alignSelf);
// non-fill stays auto so the container alignment positions us. flexGrow
// ignored.
ygStyle.flexGrow() = YGFloatOptional(0);
ygStyle.flexShrink() = YGFloatOptional(0);
ygStyle.alignSelf() = realHeightScaleType == LayoutScaleType::fill
? YGAlignStretch
: YGAlignAuto;
}
else
{
switch (realWidthScaleType)
{
case LayoutScaleType::fixed:
if (parentIsRow)
{
ygStyle.flexGrow() = YGFloatOptional(0);
ygStyle.flexShrink() = YGFloatOptional(0);
ygStyle.flexBasis() =
YGValue{m_style->flexBasis(), YGUnitAuto};
}
else
{
ygStyle.alignSelf() = YGAlignAuto;
}
break;
case LayoutScaleType::fill:
if (parentIsRow)
{
ygStyle.flexGrow() = YGFloatOptional(fractionalWidth());
ygStyle.flexShrink() = YGFloatOptional(fractionalWidth());
ygStyle.flexBasis() = YGValue{m_style->flexBasis(),
m_style->flexBasisUnits()};
}
else
{
ygStyle.alignSelf() = YGAlignStretch;
}
break;
case LayoutScaleType::hug:
if (parentIsRow)
{
ygStyle.flexGrow() = YGFloatOptional(0);
ygStyle.flexShrink() = YGFloatOptional(0);
ygStyle.flexBasis() =
YGValue{m_style->flexBasis(), YGUnitAuto};
}
else
{
ygStyle.alignSelf() = YGAlignAuto;
}
break;
default:
break;
}
switch (realHeightScaleType)
{
case LayoutScaleType::fixed:
if (!parentIsRow)
{
ygStyle.flexGrow() = YGFloatOptional(0);
ygStyle.flexShrink() = YGFloatOptional(0);
ygStyle.flexBasis() =
YGValue{m_style->flexBasis(), YGUnitAuto};
}
else
{
ygStyle.alignSelf() = YGAlignAuto;
}
break;
case LayoutScaleType::fill:
if (!parentIsRow)
{
ygStyle.flexGrow() = YGFloatOptional(fractionalHeight());
ygStyle.flexShrink() = YGFloatOptional(fractionalHeight());
ygStyle.flexBasis() = YGValue{m_style->flexBasis(),
m_style->flexBasisUnits()};
}
else
{
ygStyle.alignSelf() = YGAlignStretch;
}
break;
case LayoutScaleType::hug:
if (!parentIsRow)
{
ygStyle.flexGrow() = YGFloatOptional(0);
ygStyle.flexShrink() = YGFloatOptional(0);
ygStyle.flexBasis() =
YGValue{m_style->flexBasis(), YGUnitAuto};
}
else
{
ygStyle.alignSelf() = YGAlignAuto;
}
break;
default:
break;
}
}
}
#endif
void LayoutComponent::syncStyle()
{
if (m_style == nullptr || m_layoutData == nullptr)
{
return;
}
YGNode& ygNode = m_layoutData->node;
YGStyle& ygStyle = m_layoutData->style;
if (m_style->intrinsicallySized() && isLeaf())
{
ygNode.setContext(this);
ygNode.setMeasureFunc(measureFunc);
}
else
{
ygNode.setMeasureFunc(nullptr);
}
// Our owning layout may sit above a transparent container (group/Solo), so
// walk up for it rather than reading the immediate parent — a grouped item
// is still laid out by the container and needs its context. Cached because
// layoutParent() walks the chain on each call.
auto* parentLayout = layoutParent();
auto* parentLayoutStyle =
parentLayout != nullptr ? parentLayout->style() : nullptr;
bool parentIsGridLike =
parentLayoutStyle != nullptr && parentLayoutStyle->isGrid();
bool parentIsStack =
parentLayoutStyle != nullptr && parentLayoutStyle->isStack();
uint32_t containerJustifyItems =
parentLayoutStyle != nullptr ? parentLayoutStyle->justifyItemsValue()
: (uint32_t)YGJustifyStretch;
// Every style write is an applier now; resolve the context and run them.
LayoutSyncContext syncContext;
syncContext.parentIsGrid = parentIsGridLike;
syncContext.parentIsStack = parentIsStack;
syncContext.containerJustifyItems = containerJustifyItems;
syncContext.inlineHugs = m_style->widthScaleType() == LayoutScaleType::hug;
syncContext.parentIsRow = effectiveParentIsRow();
syncContext.isLTR = actualDirection() != LayoutDirection::rtl;
syncContext.hasLayoutParent = layoutParent() != nullptr;
m_layoutData->applyLayoutStyles(ygStyle, syncContext);
ygNode.setStyle(ygStyle);
// Sync the styles of participant children that provide their
// own layout node (including any nested inside transparent groups).
// LayoutComponent children sync their own styles through the dirty-layout
// set; these have no such entry.
forEachLayoutProvider(this,
[](Component* child, LayoutNodeProvider* provider) {
switch (child->coreType())
{
case LayoutComponentBase::typeKey:
case NestedArtboardLayoutBase::typeKey:
case ArtboardComponentListBase::typeKey:
break;
default:
provider->syncStyleChanges();
break;
}
});
}
void LayoutComponent::clearLayoutChildren()
{
YGNode& ourNode = m_layoutData->node;
YGNodeRemoveAllChildren(&ourNode);
}
void LayoutComponent::syncLayoutChildren()
{
YGNode& ourNode = m_layoutData->node;
YGNodeRemoveAllChildren(&ourNode);
int index = 0;
forEachLayoutProvider(
this,
[&ourNode, &index](Component*, LayoutNodeProvider* provider) {
for (size_t i = 0; i < provider->numLayoutNodes(); i++)
{
auto* node = static_cast<YGNode*>(provider->layoutNode((int)i));
if (node != nullptr)
{
ourNode.insertChild(node, index++);
node->setOwner(&ourNode);
ourNode.markDirtyAndPropagate();
}
}
});
markLayoutNodeDirty();
}
void LayoutComponent::propagateSize() { propagateSizeToChildren(this); }
void LayoutComponent::propagateSizeToChildren(ContainerComponent* component)
{
if (isHidden())
{
return;
}
for (auto child : component->children())
{
// Don't content-size nested layouts, or containers transparent to
// layout (groups/Solos) — their contents are free content or
// participants, never content-sized from here.
if (child->is<LayoutComponent>() || isTransparentLayoutContainer(child))
{
continue;
}
// A participating child owns its own layout node and is sized by the
// layout engine, not by controlSize — skip it.
if (LayoutNodeProvider::from(child) != nullptr)
{
continue;
}
auto sizeableChild = IntrinsicallySizeable::from(child);
if (sizeableChild != nullptr && m_style != nullptr)
{
LayoutScaleType widthScaleType = m_style->widthScaleType();
LayoutScaleType heightScaleType = m_style->heightScaleType();
sizeableChild->controlSize(
Vec2D(m_layout.width(), m_layout.height()),
widthScaleType,
heightScaleType,
actualDirection());
if (!sizeableChild->shouldPropagateSizeToChildren())
{
// Do not propagate to its children
continue;
}
}
if (child->is<ContainerComponent>())
{
propagateSizeToChildren(child->as<ContainerComponent>());
}
}
}
void LayoutComponent::calculateLayoutInternal(float availableWidth,
float availableHeight)
{
auto actualAvailWidth =
std::isnan(availableWidth) && m_style && m_style->intrinsicallySized()
? availableWidth
: width();
auto actualAvailHeight =
std::isnan(availableHeight) && m_style && m_style->intrinsicallySized()
? availableHeight
: height();
YGNodeCalculateLayout(&m_layoutData->node,
actualAvailWidth,
actualAvailHeight,
YGDirection::YGDirectionInherit);
}
bool LayoutComponent::styleDisplayHidden() const
{
if (m_style == nullptr)
{
return false;
}
return m_style->display() == YGDisplayNone;
}
LayoutDirection LayoutComponent::actualDirection()
{
if (m_style == nullptr)
{
return m_inheritedDirection;
}
switch (m_style->direction())
{
case YGDirectionLTR:
return LayoutDirection::ltr;
case YGDirectionRTL:
return LayoutDirection::rtl;
default:
return m_inheritedDirection;
}
}
void LayoutComponent::onDirty(ComponentDirt value)
{
Super::onDirty(value);
if ((value & ComponentDirt::WorldTransform) ==
ComponentDirt::WorldTransform &&
clip())
{
addDirt(ComponentDirt::Path);
}
}
static Layout layoutFromYoga(const YGLayout& layout)
{
return Layout(layout.position[YGEdgeLeft],
layout.position[YGEdgeTop],
layout.dimensions[YGDimensionWidth],
layout.dimensions[YGDimensionHeight]);
}
static LayoutPadding layoutPaddingFromYoga(const YGLayout& layout)
{
return LayoutPadding(layout.padding[YGEdgeLeft],
layout.padding[YGEdgeTop],
layout.padding[YGEdgeRight],
layout.padding[YGEdgeBottom]);
}
void LayoutComponent::updateLayoutBounds(bool animate)
{
YGNode& node = m_layoutData->node;
bool updated = node.getHasNewLayout();
if (!updated)
{
return;
}
node.setHasNewLayout(false);
forEachLayoutProvider(this,
[animate](Component*, LayoutNodeProvider* provider) {
provider->updateLayoutBounds(animate);
});
const auto& yogaLayout = node.getLayout();
Layout newLayout = layoutFromYoga(yogaLayout);
m_layoutPadding = layoutPaddingFromYoga(yogaLayout);
if (m_justAddedToHost)
{
m_justAddedToHost = false;
// In cases were we have a host (ie, Component List, etc), we
// don't want to animate the x/y/width/height because the initial
// x/y/width/height will have been 0,0,0,0 within the parent host.
//
// Instead, we want to the position/size to start at whatever this
// layout's computed position is within its host.
//
// We'll keep this as an entirely seperate code path because
// this may be useful in adding support for animate in/out of
// items in an ArtboardHost.
m_layout = newLayout;
auto animationData = currentAnimationData();
animationData->from = newLayout;
animationData->to = newLayout;
animationData->elapsedSeconds = 0.0f;
propagateSize();
markWorldTransformDirty();
m_forceUpdateLayoutBounds = false;
return;
}
if (animate && animates())
{
auto animationData = currentAnimationData();
if (newLayout != animationData->to || m_forceUpdateLayoutBounds)
{
if (animationData->elapsedSeconds != 0.0f)
{
if (m_isSmoothingAnimation)
{
// we were already smoothening.
m_animationDataA.copy(m_animationDataB);
}
m_isSmoothingAnimation = true;
}
else
{
m_isSmoothingAnimation = false;
}
animationData = currentAnimationData();
animationData->from = m_layout;
animationData->to = newLayout;
animationData->elapsedSeconds = 0.0f;
propagateSize();
markWorldTransformDirty();
}
}
else if (newLayout != m_layout || m_forceUpdateLayoutBounds)
{
if (m_layout.width() != newLayout.width() ||
m_layout.height() != newLayout.height())
{
// Width changed, we need to rebuild the path.
addDirt(ComponentDirt::Path);
}
m_animationDataA.to = m_layout = newLayout;
propagateSize();
markWorldTransformDirty();
}
m_forceUpdateLayoutBounds = false;
}
bool LayoutComponent::advanceComponent(float elapsedSeconds, AdvanceFlags flags)
{
if ((flags & AdvanceFlags::NewFrame) != AdvanceFlags::NewFrame ||
isCollapsed())
{
return false;
}
return applyInterpolation(elapsedSeconds,
(flags & AdvanceFlags::Animate) ==
AdvanceFlags::Animate ||
(flags & AdvanceFlags::AdvanceNested) ==
AdvanceFlags::AdvanceNested);
}
bool LayoutComponent::animates()
{
if (m_style == nullptr)
{
return false;
}
return m_style->animationStyle() != LayoutAnimationStyle::none &&
interpolation() != LayoutStyleInterpolation::hold &&
interpolationTime() > 0;
}
LayoutAnimationStyle LayoutComponent::animationStyle()
{
if (m_style == nullptr)
{
return LayoutAnimationStyle::none;
}
return m_style->animationStyle();
}
KeyFrameInterpolator* LayoutComponent::interpolator()
{
if (m_style == nullptr)
{
return nullptr;
}
switch (m_style->animationStyle())
{
case LayoutAnimationStyle::inherit:
return m_inheritedInterpolator != nullptr ? m_inheritedInterpolator
: m_style->interpolator();
case LayoutAnimationStyle::custom:
return m_style->interpolator();
default:
return nullptr;
}
}
LayoutStyleInterpolation LayoutComponent::interpolation()
{
auto defaultInterpolation = LayoutStyleInterpolation::hold;
if (m_style == nullptr)
{
return defaultInterpolation;
}
switch (m_style->animationStyle())
{
case LayoutAnimationStyle::inherit:
return m_inheritedInterpolation;
case LayoutAnimationStyle::custom:
return m_style->interpolation();
default:
return defaultInterpolation;
}
}
float LayoutComponent::interpolationTime()
{
if (m_style == nullptr)
{
return 0;
}
switch (m_style->animationStyle())
{
case LayoutAnimationStyle::inherit:
return m_inheritedInterpolationTime;
case LayoutAnimationStyle::custom:
return m_style->interpolationTime();
default:
return 0;
}
}
bool LayoutComponent::cascadeLayoutStyle(
LayoutStyleInterpolation inheritedInterpolation,
KeyFrameInterpolator* inheritedInterpolator,
float inheritedInterpolationTime,
LayoutDirection direction)
{
bool updated = false;
if (m_style != nullptr &&
m_style->animationStyle() == LayoutAnimationStyle::inherit)
{
updated = setInheritedInterpolation(inheritedInterpolation,
inheritedInterpolator,
inheritedInterpolationTime);
}
else
{
clearInheritedInterpolation();
}
auto oldDirection = m_inheritedDirection;
if (direction == LayoutDirection::inherit ||
(m_style != nullptr && m_style->direction() != YGDirectionInherit))
{
m_inheritedDirection = LayoutDirection::inherit;
}
else
{
m_inheritedDirection = direction;
}
if (m_inheritedDirection != oldDirection)
{
markLayoutNodeDirty(true);
addDirt(ComponentDirt::Path);
updated = true;
}
forEachLayoutProvider(this,
[this](Component*, LayoutNodeProvider* provider) {
provider->cascadeLayoutStyle(interpolation(),
interpolator(),
interpolationTime(),
actualDirection());
});
return updated;
}
bool LayoutComponent::setInheritedInterpolation(
LayoutStyleInterpolation inheritedInterpolation,
KeyFrameInterpolator* inheritedInterpolator,
float inheritedInterpolationTime)
{
if (inheritedInterpolation != m_inheritedInterpolation ||
inheritedInterpolator != m_inheritedInterpolator ||
inheritedInterpolationTime != m_inheritedInterpolationTime)
{
m_inheritedInterpolation = inheritedInterpolation;
m_inheritedInterpolator = inheritedInterpolator;
m_inheritedInterpolationTime = inheritedInterpolationTime;
return true;
}
return false;
}
void LayoutComponent::clearInheritedInterpolation()
{
m_inheritedInterpolation = LayoutStyleInterpolation::hold;
m_inheritedInterpolator = nullptr;
m_inheritedInterpolationTime = 0;
}
LayoutAnimationData* LayoutComponent::currentAnimationData()
{
return m_isSmoothingAnimation ? &m_animationDataB : &m_animationDataA;
}
void LayoutAnimationData::copy(const LayoutAnimationData& source)
{
from = source.from;
to = source.to;
elapsedSeconds = source.elapsedSeconds;
}
bool LayoutComponent::applyInterpolation(float elapsedSeconds, bool animate)
{
auto animationData = currentAnimationData();
if (!animate || !animates() || m_style == nullptr ||
animationData->to == m_layout)
{
return false;
}
if (m_isSmoothingAnimation)
{
float f = std::fmin(1.0f,
interpolationTime() > 0.0f
? m_animationDataA.elapsedSeconds /
interpolationTime()
: 1.0f);
if (interpolation() != LayoutStyleInterpolation::linear &&
interpolator() != nullptr)
{
f = interpolator()->transform(f);
}
m_animationDataB.from = m_animationDataA.interpolate(f);
if (f == 1.0f)
{
m_animationDataA.copy(m_animationDataB);
m_isSmoothingAnimation = false;
}
else
{
m_animationDataA.elapsedSeconds += elapsedSeconds;
}
}
if ((animationData = currentAnimationData())->elapsedSeconds >=
interpolationTime())
{
Layout newLayout = animationData->to;
if (m_layout.width() != newLayout.width() ||
m_layout.height() != newLayout.height())
{
addDirt(ComponentDirt::Path);
}
m_layout = animationData->to;
if (m_isSmoothingAnimation)
{
m_isSmoothingAnimation = false;
m_animationDataA.copy(m_animationDataB);
m_animationDataA.elapsedSeconds = m_animationDataB.elapsedSeconds =
0.0f;
}
else
{
m_animationDataA.elapsedSeconds = 0.0f;
}
propagateSize();
markWorldTransformDirty();
return false;
}
float f =
std::fmin(1.0f,
interpolationTime() > 0
? animationData->elapsedSeconds / interpolationTime()
: 1.0f);
if (interpolation() != LayoutStyleInterpolation::linear &&
interpolator() != nullptr)
{
f = interpolator()->transform(f);
}
auto current = animationData->interpolate(f);
if (m_layout != current)
{
bool sizeChanged = m_layout.width() != current.width() ||
m_layout.height() != current.height();
m_layout = current;
if (sizeChanged)
{
propagateSize();
}
markWorldTransformDirty();
}
animationData->elapsedSeconds += elapsedSeconds;
if (f != 1)
{
// Do we really need to mark the layout node dirty!!??
markLayoutNodeDirty();
return true;
}
return false;
}
void LayoutComponent::interruptAnimation()
{
if (animates())
{
m_layout = currentAnimationData()->to;
propagateSize();
}
}
void LayoutComponent::markLayoutNodeDirty(bool shouldForceUpdateLayoutBounds)
{
if (shouldForceUpdateLayoutBounds == true)
{
m_forceUpdateLayoutBounds = shouldForceUpdateLayoutBounds;
}
m_layoutData->node.markDirtyAndPropagate();
artboard()->markLayoutDirty(this);
}
void LayoutComponent::markLayoutStyleDirty()
{
clearInheritedInterpolation();
addDirt(ComponentDirt::LayoutStyle);
if (artboard() != this)
{
artboard()->markLayoutStyleDirty();
}
}
void LayoutComponent::positionTypeChanged()
{
if (m_style == nullptr)
{
return;
}
if (m_style->positionType() == YGPositionTypeAbsolute)
{
// Preserve computed position only if left/top were not explicitly keyed
// this frame. If keyed, honor those values and only set units
// appropriately.
if (!m_positionLeftChanged)
{
m_style->positionLeft(layoutBounds().left());
}
if (!m_positionTopChanged)
{
m_style->positionTop(layoutBounds().top());
}
m_style->positionRight(0);
m_style->positionBottom(0);
m_style->positionLeftUnitsValue(YGUnitPoint);
m_style->positionTopUnitsValue(YGUnitPoint);
m_style->positionRightUnitsValue(YGUnitUndefined);
m_style->positionBottomUnitsValue(YGUnitUndefined);
}
else
{
m_style->positionLeft(0);
m_style->positionTop(0);
m_style->positionRight(0);
m_style->positionBottom(0);
m_style->positionLeftUnitsValue(YGUnitUndefined);
m_style->positionTopUnitsValue(YGUnitUndefined);
m_style->positionRightUnitsValue(YGUnitUndefined);
m_style->positionBottomUnitsValue(YGUnitUndefined);
}
markLayoutNodeDirty();
}
void LayoutComponent::scaleTypeChanged()
{
if (m_style == nullptr)
{
return;
}
m_style->intrinsicallySizedValue(
m_style->widthScaleType() == LayoutScaleType::hug ||
m_style->heightScaleType() == LayoutScaleType::hug);
markLayoutNodeDirty();
}
void LayoutComponent::displayChanged()
{
if (m_style == nullptr)
{
return;
}
propagateCollapse(isCollapsed());
markLayoutNodeDirty();
}
void LayoutComponent::syncChildProviderStyles()
{
forEachLayoutProvider(this, [](Component*, LayoutNodeProvider* provider) {
provider->syncStyleChanges();
provider->markLayoutNodeDirty();
});
}
void LayoutComponent::flexDirectionChanged()
{
markLayoutNodeDirty();
syncChildProviderStyles();
}
// Switching grid/flex/stack changes how each child's style maps to yoga.
void LayoutComponent::layoutTypeChanged()
{
markLayoutNodeDirty();
syncChildProviderStyles();
}
void LayoutComponent::directionChanged()
{
markLayoutStyleDirty();
markLayoutNodeDirty(true);
}
#else
LayoutComponent::LayoutComponent() :
m_layoutData(new LayoutData()), m_proxy(this)
{}
float LayoutComponent::gapHorizontal() { return 0; }
float LayoutComponent::gapVertical() { return 0; }
void LayoutComponent::drawProxy(Renderer* renderer) {}
void LayoutComponent::draw(Renderer* renderer) {}
void LayoutComponent::updateRenderPath() {}
Vec2D LayoutComponent::measureLayout(float width,
LayoutMeasureMode widthMode,
float height,
LayoutMeasureMode heightMode)
{
return Vec2D();
}
bool LayoutComponent::advanceComponent(float elapsedSeconds, AdvanceFlags flags)
{
return false;
}
void LayoutComponent::markLayoutNodeDirty(bool shouldForceUpdateLayoutBounds) {}
void LayoutComponent::markLayoutStyleDirty() {}
void LayoutComponent::onDirty(ComponentDirt value) {}
bool LayoutComponent::mainAxisIsRow() { return true; }
bool LayoutComponent::mainAxisIsColumn() { return false; }
void LayoutComponent::calculateLayoutInternal(float availableWidth,
float availableHeight)
{}
#endif
LayoutComponent::~LayoutComponent()
{
if (artboard() != nullptr)
{
artboard()->cleanLayout(this);
}
#ifdef WITH_RIVE_TOOLS
m_layoutData->unref();
#else
delete m_layoutData;
#endif
}
void LayoutComponent::clipChanged()
{
markLayoutNodeDirty();
addDirt(ComponentDirt::Path);
}
void LayoutComponent::widthChanged() { markLayoutNodeDirty(); }
void LayoutComponent::heightChanged() { markLayoutNodeDirty(); }
void LayoutComponent::styleIdChanged() { markLayoutNodeDirty(); }
void LayoutComponent::fractionalWidthChanged() { markLayoutNodeDirty(); }
void LayoutComponent::fractionalHeightChanged() { markLayoutNodeDirty(); }
ShapePaintPath* LayoutComponent::worldPath() { return &m_worldPath; }
ShapePaintPath* LayoutComponent::localPath() { return &m_localPath; }
ShapePaintPath* LayoutComponent::localClockwisePath() { return &m_localPath; }
Component* LayoutComponent::pathBuilder() { return this; }