blob: 465a7c5e7a98e4f5d881106db68ba4944d75f7a1 [file]
#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