| #include "rive/layout/layout_participant.hpp" |
| #include "rive/layout_component.hpp" |
| #include "rive/layout/layout_component_style.hpp" |
| #include "rive/layout/layout_node_style.hpp" |
| #include "rive/layout/grid_track.hpp" |
| #include "rive/layout/layout_data.hpp" |
| #include "rive/layout/layout_style_applier.hpp" |
| #include "rive/transform_component.hpp" |
| #include "rive/intrinsically_sizeable.hpp" |
| #include "rive/component.hpp" |
| #include "rive/solo.hpp" |
| #include "rive/artboard.hpp" |
| #include "rive/animation/keyframe_interpolator.hpp" |
| #include <algorithm> |
| #include <cmath> |
| |
| using namespace rive; |
| |
| #ifdef WITH_RIVE_LAYOUT |
| void LayoutParticipant::addLayoutStyleApplier(LayoutStyleApplier* applier) |
| { |
| if (m_layoutData != nullptr) |
| { |
| m_layoutData->addApplier(applier); |
| } |
| } |
| #endif |
| |
| namespace rive |
| { |
| // Lazily-allocated animation state — only exists while a participant is under |
| // an animated layout, so a non-animating participant pays only the pointer. |
| struct ParticipantAnimation |
| { |
| Layout animatedLayout; |
| LayoutAnimationData a; |
| LayoutAnimationData b; |
| bool isSmoothing = false; |
| LayoutStyleInterpolation interpolation = LayoutStyleInterpolation::hold; |
| KeyFrameInterpolator* interpolator = nullptr; |
| float interpolationTime = 0.0f; |
| }; |
| } // namespace rive |
| |
| #ifdef WITH_RIVE_LAYOUT |
| static YGSize participantMeasureFunc(YGNode* node, |
| float width, |
| YGMeasureMode widthMode, |
| float height, |
| YGMeasureMode heightMode) |
| { |
| auto* component = static_cast<Component*>(node->getContext()); |
| auto* sizeable = IntrinsicallySizeable::from(component); |
| Vec2D size = sizeable != nullptr |
| ? sizeable->measureLayout(width, |
| (LayoutMeasureMode)widthMode, |
| height, |
| (LayoutMeasureMode)heightMode) |
| : Vec2D(); |
| return YGSize{size.x, size.y}; |
| } |
| #endif |
| |
| LayoutParticipant::~LayoutParticipant() |
| { |
| delete m_animation; |
| #ifdef WITH_RIVE_LAYOUT |
| releaseLayoutData(); |
| #endif |
| } |
| |
| TransformComponent* LayoutParticipant::transformComponent() |
| { |
| auto* p = parent(); |
| return (p != nullptr && p->is<TransformComponent>()) |
| ? p->as<TransformComponent>() |
| : nullptr; |
| } |
| |
| LayoutComponent* LayoutParticipant::owningLayout() |
| { |
| for (Component* c = parent(); c != nullptr; c = c->parent()) |
| { |
| if (c->is<LayoutComponent>()) |
| { |
| return c->as<LayoutComponent>(); |
| } |
| } |
| return nullptr; |
| } |
| |
| bool LayoutParticipant::isParticipatingInLayout() const |
| { |
| #ifdef WITH_RIVE_LAYOUT |
| return m_layoutData != nullptr; |
| #else |
| return false; |
| #endif |
| } |
| |
| StatusCode LayoutParticipant::onAddedClean(CoreContext* context) |
| { |
| StatusCode code = Super::onAddedClean(context); |
| if (code != StatusCode::Ok) |
| { |
| return code; |
| } |
| #ifdef WITH_RIVE_LAYOUT |
| resync(); |
| #endif |
| return StatusCode::Ok; |
| } |
| |
| #ifdef WITH_RIVE_LAYOUT |
| void LayoutParticipant::releaseLayoutData() |
| { |
| if (m_layoutData == nullptr) |
| { |
| return; |
| } |
| #ifdef WITH_RIVE_TOOLS |
| m_layoutData->unref(); |
| #else |
| delete m_layoutData; |
| #endif |
| m_layoutData = nullptr; |
| } |
| |
| // Releases a fill axis from any content-based minimum, guarding against CSS's |
| // min-width:auto. Yoga has no such minimum today, so this is currently inert — |
| // see layout_fill_content_floor_test.dart, which fails if that changes. |
| // LayoutComponent has no equivalent. |
| // |
| // Overrides rather than sits inline so it lands after the min/max Super writes. |
| void LayoutParticipant::applyBaseStyle(YGStyle& style, |
| const LayoutSyncContext& context) |
| { |
| LayoutSizingStyle::applyBaseStyle(style, context); |
| |
| const LayoutScaleType widthScale = (LayoutScaleType)layoutWidthScaleType(); |
| const LayoutScaleType heightScale = |
| (LayoutScaleType)layoutHeightScaleType(); |
| const bool parentIsRow = context.parentIsRow; |
| const bool parentIsGridLike = context.parentIsGrid; |
| const bool widthFill = widthScale == LayoutScaleType::fill; |
| const bool heightFill = heightScale == LayoutScaleType::fill; |
| |
| style.dimensions()[YGDimensionWidth] = |
| widthScale == LayoutScaleType::fixed |
| ? YGValue{std::max(0.0f, width()), (YGUnit)widthUnitsValue()} |
| : YGValue{YGUndefined, YGUnitAuto}; |
| style.dimensions()[YGDimensionHeight] = |
| heightScale == LayoutScaleType::fixed |
| ? YGValue{std::max(0.0f, height()), (YGUnit)heightUnitsValue()} |
| : YGValue{YGUndefined, YGUnitAuto}; |
| |
| if (parentIsGridLike) |
| { |
| style.flexGrow() = YGFloatOptional(0.0f); |
| style.flexShrink() = YGFloatOptional(0.0f); |
| style.alignSelf() = heightFill ? YGAlignStretch : YGAlignAuto; |
| } |
| else |
| { |
| bool mainFill = parentIsRow ? widthFill : heightFill; |
| float mainFraction = |
| parentIsRow ? fractionalWidth() : fractionalHeight(); |
| style.flexGrow() = YGFloatOptional(mainFill ? mainFraction : 0.0f); |
| style.flexShrink() = YGFloatOptional(mainFill ? mainFraction : 0.0f); |
| style.flexBasis() = mainFill ? YGValue{0.0f, YGUnitPoint} |
| : YGValue{YGUndefined, YGUnitAuto}; |
| bool crossFill = parentIsRow ? heightFill : widthFill; |
| style.alignSelf() = crossFill ? YGAlignStretch : YGAlignAuto; |
| } |
| |
| if (layoutWidthScaleType() == (uint32_t)LayoutScaleType::fill && |
| (YGUnit)minWidthUnitsValue() == YGUnitUndefined) |
| { |
| style.minDimensions()[YGDimensionWidth] = YGValue{0.0f, YGUnitPoint}; |
| } |
| if (layoutHeightScaleType() == (uint32_t)LayoutScaleType::fill && |
| (YGUnit)minHeightUnitsValue() == YGUnitUndefined) |
| { |
| style.minDimensions()[YGDimensionHeight] = YGValue{0.0f, YGUnitPoint}; |
| } |
| } |
| |
| void LayoutParticipant::resync() |
| { |
| auto* host = transformComponent(); |
| if (host == nullptr) |
| { |
| return; |
| } |
| if (m_layoutData == nullptr) |
| { |
| m_layoutData = new LayoutData(); |
| m_layoutData->node.getConfig()->setPointScaleFactor(0); |
| // Measure our host's intrinsic (hug) size via IntrinsicallySizeable. |
| m_layoutData->node.setContext(host); |
| m_layoutData->node.setMeasureFunc(participantMeasureFunc); |
| // We are our own sizing style. |
| addLayoutStyleApplier(this); |
| } |
| syncStyleChanges(); |
| if (auto* lc = owningLayout()) |
| { |
| lc->syncLayoutChildren(); |
| } |
| host->addDirt(ComponentDirt::WorldTransform, true); |
| markLayoutNodeDirty(true); |
| } |
| |
| void* LayoutParticipant::layoutNode(int index) |
| { |
| return m_layoutData != nullptr ? static_cast<void*>(&m_layoutData->node) |
| : nullptr; |
| } |
| |
| Layout LayoutParticipant::solvedLayout() const |
| { |
| if (m_layoutData == nullptr) |
| { |
| return Layout(); |
| } |
| const auto& l = m_layoutData->node.getLayout(); |
| return Layout(definedOrZero(l.position[YGEdgeLeft]), |
| definedOrZero(l.position[YGEdgeTop]), |
| definedOrZero(l.dimensions[YGDimensionWidth]), |
| definedOrZero(l.dimensions[YGDimensionHeight])); |
| } |
| |
| // While animating, the resolved slot is the interpolated animatedLayout; |
| // otherwise it's read straight from the yoga node (no per-participant cache). |
| float LayoutParticipant::resolvedLeft() const |
| { |
| return m_animation != nullptr ? m_animation->animatedLayout.left() |
| : solvedLayout().left(); |
| } |
| float LayoutParticipant::resolvedTop() const |
| { |
| return m_animation != nullptr ? m_animation->animatedLayout.top() |
| : solvedLayout().top(); |
| } |
| float LayoutParticipant::resolvedWidth() const |
| { |
| return m_animation != nullptr ? m_animation->animatedLayout.width() |
| : solvedLayout().width(); |
| } |
| float LayoutParticipant::resolvedHeight() const |
| { |
| return m_animation != nullptr ? m_animation->animatedLayout.height() |
| : solvedLayout().height(); |
| } |
| |
| void LayoutParticipant::applyResolvedLayoutSize() |
| { |
| auto* sizeable = IntrinsicallySizeable::from(transformComponent()); |
| if (sizeable == nullptr) |
| { |
| return; |
| } |
| auto* lc = owningLayout(); |
| LayoutDirection direction = |
| lc != nullptr ? lc->actualDirection() : LayoutDirection::inherit; |
| // Read the resolved slot once (animated slot, or a single yoga-node read) |
| // rather than calling resolvedWidth()/resolvedHeight() separately. |
| const Layout resolved = |
| m_animation != nullptr ? m_animation->animatedLayout : solvedLayout(); |
| sizeable->controlSize(Vec2D(resolved.width(), resolved.height()), |
| (LayoutScaleType)layoutWidthScaleType(), |
| (LayoutScaleType)layoutHeightScaleType(), |
| direction); |
| } |
| #else |
| float LayoutParticipant::resolvedLeft() const { return 0.0f; } |
| float LayoutParticipant::resolvedTop() const { return 0.0f; } |
| float LayoutParticipant::resolvedWidth() const { return 0.0f; } |
| float LayoutParticipant::resolvedHeight() const { return 0.0f; } |
| #endif |
| |
| size_t LayoutParticipant::numLayoutNodes() |
| { |
| #ifdef WITH_RIVE_LAYOUT |
| return m_layoutData != nullptr ? 1 : 0; |
| #else |
| return 0; |
| #endif |
| } |
| |
| AABB LayoutParticipant::layoutBounds() |
| { |
| #ifdef WITH_RIVE_LAYOUT |
| // Read the resolved slot once (animated slot, or a single yoga-node read) |
| // instead of resolvedLeft/Top/Width/Height each re-reading it. |
| const Layout resolved = |
| m_animation != nullptr ? m_animation->animatedLayout : solvedLayout(); |
| return AABB::fromLTWH(resolved.left(), |
| resolved.top(), |
| resolved.width(), |
| resolved.height()); |
| #else |
| return AABB::fromLTWH(0.0f, 0.0f, 0.0f, 0.0f); |
| #endif |
| } |
| |
| AABB LayoutParticipant::layoutBoundsForNode(int index) |
| { |
| #ifdef WITH_RIVE_LAYOUT |
| #endif |
| return layoutBounds(); |
| } |
| |
| bool LayoutParticipant::syncStyleChanges() |
| { |
| #ifdef WITH_RIVE_LAYOUT |
| if (m_layoutData == nullptr) |
| { |
| return false; |
| } |
| YGNode& node = m_layoutData->node; |
| YGStyle& ygStyle = m_layoutData->style; |
| |
| LayoutScaleType widthScale = (LayoutScaleType)layoutWidthScaleType(); |
| LayoutScaleType heightScale = (LayoutScaleType)layoutHeightScaleType(); |
| |
| auto* lc = owningLayout(); |
| bool parentIsRow = lc != nullptr ? lc->mainAxisIsRow() : true; |
| bool parentIsGridLike = |
| lc != nullptr && lc->style() != nullptr && lc->style()->isGrid(); |
| |
| bool needsMeasure = widthScale == LayoutScaleType::hug || |
| heightScale == LayoutScaleType::hug; |
| if (needsMeasure) |
| { |
| node.setContext(transformComponent()); |
| node.setMeasureFunc(participantMeasureFunc); |
| } |
| else |
| { |
| node.setMeasureFunc(nullptr); |
| } |
| |
| bool parentIsStack = |
| lc != nullptr && lc->style() != nullptr && lc->style()->isStack(); |
| uint32_t containerJustifyItems = (lc != nullptr && lc->style() != nullptr) |
| ? lc->style()->justifyItemsValue() |
| : (uint32_t)YGJustifyStretch; |
| // Appliers last, before the style reaches the node. A participant's |
| // GridItemPlacement hangs off the same Node that owns this participant. |
| LayoutSyncContext syncContext; |
| syncContext.parentIsGrid = parentIsGridLike; |
| syncContext.parentIsStack = parentIsStack; |
| syncContext.containerJustifyItems = containerJustifyItems; |
| syncContext.inlineHugs = widthScale == LayoutScaleType::hug; |
| syncContext.parentIsRow = parentIsRow; |
| syncContext.isLTR = |
| lc == nullptr || lc->actualDirection() != LayoutDirection::rtl; |
| syncContext.hasLayoutParent = lc != nullptr; |
| if (m_layoutData != nullptr) |
| { |
| m_layoutData->applyLayoutStyles(ygStyle, syncContext); |
| } |
| |
| node.setStyle(ygStyle); |
| node.markDirtyAndPropagate(); |
| // Fold display:none into the host's collapse so it stops drawing (it's |
| // already removed from the layout flow via the yoga display above). |
| if (auto* host = transformComponent()) |
| { |
| auto* p = host->parent(); |
| bool parentHidesHost = p != nullptr && p->isCollapsed(); |
| // A Solo hides its non-active children; mirror that here so folding our |
| // display doesn't reveal an inactive Solo child. (Dart re-dispatches |
| // through the parent's collapse; C++ has no host mixin, so we check |
| // it.) |
| if (p != nullptr && p->is<Solo>()) |
| { |
| auto* solo = p->as<Solo>(); |
| auto* ab = solo->artboard(); |
| Core* active = ab != nullptr |
| ? ab->resolve(solo->activeComponentId()) |
| : nullptr; |
| if (active != host) |
| { |
| parentHidesHost = true; |
| } |
| } |
| host->collapse(parentHidesHost || |
| (YGDisplay)displayValue() == YGDisplayNone); |
| } |
| return true; |
| #else |
| return false; |
| #endif |
| } |
| |
| void LayoutParticipant::updateLayoutBounds(bool animate) |
| { |
| #ifdef WITH_RIVE_LAYOUT |
| if (m_layoutData == nullptr) |
| { |
| return; |
| } |
| YGNode& node = m_layoutData->node; |
| if (!node.getHasNewLayout()) |
| { |
| return; |
| } |
| node.setHasNewLayout(false); |
| |
| Layout newLayout = solvedLayout(); |
| // Animate only when under an animated layout (m_animation allocated), the |
| // animate flag is set, and we've solved before (so we snap on first |
| // appearance instead of animating in from 0,0). |
| if (m_animation != nullptr && animate && m_hasSolvedLayout) |
| { |
| // Retarget: animate from where we are now to the newly solved slot, |
| // smoothing over any in-flight animation (mirrors LayoutComponent). |
| auto* animationData = currentAnimationData(); |
| if (newLayout != animationData->to) |
| { |
| if (animationData->elapsedSeconds != 0.0f) |
| { |
| if (m_animation->isSmoothing) |
| { |
| m_animation->a.copy(m_animation->b); |
| } |
| m_animation->isSmoothing = true; |
| } |
| else |
| { |
| m_animation->isSmoothing = false; |
| } |
| animationData = currentAnimationData(); |
| animationData->from = m_animation->animatedLayout; |
| animationData->to = newLayout; |
| animationData->elapsedSeconds = 0.0f; |
| } |
| } |
| else if (m_animation != nullptr) |
| { |
| // Snap the animated slot (first solve, or the animate flag is off). |
| m_animation->animatedLayout = newLayout; |
| m_animation->a.to = newLayout; |
| } |
| // else: not animating — resolvedLeft etc. read the yoga node directly. |
| m_hasSolvedLayout = true; |
| applyResolvedLayoutSize(); |
| if (auto* host = transformComponent()) |
| { |
| host->addDirt(ComponentDirt::WorldTransform, true); |
| } |
| #endif |
| } |
| |
| void LayoutParticipant::markLayoutNodeDirty(bool shouldForceUpdateLayoutBounds) |
| { |
| #ifdef WITH_RIVE_LAYOUT |
| if (m_layoutData != nullptr) |
| { |
| m_layoutData->node.markDirtyAndPropagate(); |
| } |
| if (auto* lc = owningLayout()) |
| { |
| lc->markLayoutNodeDirty(shouldForceUpdateLayoutBounds); |
| } |
| #endif |
| } |
| |
| void LayoutParticipant::onSizingChanged() |
| { |
| #ifdef WITH_RIVE_LAYOUT |
| syncStyleChanges(); |
| markLayoutNodeDirty(); |
| #endif |
| } |
| |
| // ── Layout animation. The participant has no animation style of its own; it |
| // inherits the parent layout's (stored via cascadeLayoutStyle) and interpolates |
| // its resolved slot toward each newly solved layout, advanced each frame as an |
| // AdvancingComponent. |
| |
| LayoutAnimationData* LayoutParticipant::currentAnimationData() |
| { |
| // Only called while animating (m_animation != nullptr). |
| return m_animation->isSmoothing ? &m_animation->b : &m_animation->a; |
| } |
| |
| bool LayoutParticipant::animates() const { return m_animation != nullptr; } |
| |
| LayoutStyleInterpolation LayoutParticipant::interpolation() const |
| { |
| return m_animation != nullptr ? m_animation->interpolation |
| : LayoutStyleInterpolation::hold; |
| } |
| |
| float LayoutParticipant::interpolationTime() const |
| { |
| return m_animation != nullptr ? m_animation->interpolationTime : 0.0f; |
| } |
| |
| KeyFrameInterpolator* LayoutParticipant::interpolator() const |
| { |
| return m_animation != nullptr ? m_animation->interpolator : nullptr; |
| } |
| |
| bool LayoutParticipant::advanceComponent(float elapsedSeconds, |
| AdvanceFlags flags) |
| { |
| #ifdef WITH_RIVE_LAYOUT |
| if (m_animation == nullptr || |
| (flags & AdvanceFlags::NewFrame) != AdvanceFlags::NewFrame) |
| { |
| return false; |
| } |
| return applyInterpolation(elapsedSeconds, |
| (flags & AdvanceFlags::Animate) == |
| AdvanceFlags::Animate || |
| (flags & AdvanceFlags::AdvanceNested) == |
| AdvanceFlags::AdvanceNested); |
| #else |
| return false; |
| #endif |
| } |
| |
| #ifdef WITH_RIVE_LAYOUT |
| bool LayoutParticipant::cascadeLayoutStyle( |
| LayoutStyleInterpolation inheritedInterpolation, |
| KeyFrameInterpolator* inheritedInterpolator, |
| float inheritedInterpolationTime, |
| LayoutDirection direction) |
| { |
| // A participant has no animation style of its own; it inherits the parent |
| // layout's. Allocate the animation state only while it actually animates. |
| bool willAnimate = |
| inheritedInterpolation != LayoutStyleInterpolation::hold && |
| inheritedInterpolationTime > 0.0f; |
| if (willAnimate) |
| { |
| if (m_animation == nullptr) |
| { |
| m_animation = new ParticipantAnimation(); |
| // Seed from the current resolved slot so enabling animation |
| // mid-life doesn't animate in from 0. |
| Layout current = solvedLayout(); |
| m_animation->animatedLayout = current; |
| m_animation->a.from = current; |
| m_animation->a.to = current; |
| } |
| m_animation->interpolation = inheritedInterpolation; |
| m_animation->interpolator = inheritedInterpolator; |
| m_animation->interpolationTime = inheritedInterpolationTime; |
| } |
| else if (m_animation != nullptr) |
| { |
| // Parent no longer animates: drop the state and snap from here on. |
| delete m_animation; |
| m_animation = nullptr; |
| } |
| return willAnimate; |
| } |
| |
| bool LayoutParticipant::applyInterpolation(float elapsedSeconds, bool animate) |
| { |
| if (m_animation == nullptr) |
| { |
| return false; |
| } |
| auto* animationData = currentAnimationData(); |
| if (!animate || animationData->to == m_animation->animatedLayout) |
| { |
| return false; |
| } |
| if (m_animation->isSmoothing) |
| { |
| float f = |
| std::fmin(1.0f, |
| interpolationTime() > 0.0f |
| ? m_animation->a.elapsedSeconds / interpolationTime() |
| : 1.0f); |
| if (interpolation() != LayoutStyleInterpolation::linear && |
| interpolator() != nullptr) |
| { |
| f = interpolator()->transform(f); |
| } |
| m_animation->b.from = m_animation->a.interpolate(f); |
| if (f == 1.0f) |
| { |
| m_animation->a.copy(m_animation->b); |
| m_animation->isSmoothing = false; |
| } |
| else |
| { |
| m_animation->a.elapsedSeconds += elapsedSeconds; |
| } |
| } |
| |
| animationData = currentAnimationData(); |
| if (animationData->elapsedSeconds >= interpolationTime()) |
| { |
| m_animation->animatedLayout = animationData->to; |
| if (m_animation->isSmoothing) |
| { |
| m_animation->isSmoothing = false; |
| m_animation->a.copy(m_animation->b); |
| m_animation->a.elapsedSeconds = m_animation->b.elapsedSeconds = |
| 0.0f; |
| } |
| else |
| { |
| m_animation->a.elapsedSeconds = 0.0f; |
| } |
| applyResolvedLayoutSize(); |
| if (auto* host = transformComponent()) |
| { |
| host->addDirt(ComponentDirt::WorldTransform, true); |
| } |
| return false; |
| } |
| |
| float f = |
| std::fmin(1.0f, |
| interpolationTime() > 0.0f |
| ? animationData->elapsedSeconds / interpolationTime() |
| : 1.0f); |
| if (interpolation() != LayoutStyleInterpolation::linear && |
| interpolator() != nullptr) |
| { |
| f = interpolator()->transform(f); |
| } |
| auto current = animationData->interpolate(f); |
| if (m_animation->animatedLayout != current) |
| { |
| m_animation->animatedLayout = current; |
| applyResolvedLayoutSize(); |
| if (auto* host = transformComponent()) |
| { |
| host->addDirt(ComponentDirt::WorldTransform, true); |
| } |
| } |
| animationData->elapsedSeconds += elapsedSeconds; |
| return f != 1.0f; |
| } |
| #endif |