| #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/importers/import_stack.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 |
| |
| bool rive::isTransparentLayoutContainer(Component* component) |
| { |
| return component->coreType() == NodeBase::typeKey || component->is<Solo>(); |
| } |
| |
| bool rive::joinsLayoutThroughContainer(Component* component) |
| { |
| if (!component->is<ArtboardComponentList>()) |
| { |
| return true; |
| } |
| auto flags = |
| static_cast<DrawableFlag>(component->as<Drawable>()->drawableFlags()); |
| return (flags & DrawableFlag::ParticipatesInLayout) == |
| DrawableFlag::ParticipatesInLayout; |
| } |
| |
| namespace |
| { |
| // Whether content-sizing stops here. Narrower than |
| // isTransparentLayoutContainer: a plain group is a barrier (its contents are |
| // free content), but a Solo is not — its children content-size just as direct |
| // children of the layout do. |
| bool stopsContentSizing(Component* component) |
| { |
| return component->coreType() == NodeBase::typeKey; |
| } |
| |
| // 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); |
| } |
| } |
| } |
| |
| // Only reached when the HasComponentOrigin flag says one exists, so the common |
| // case never walks. |
| ComponentOrigin* originChild(const ContainerComponent* owner) |
| { |
| for (auto* child : owner->children()) |
| { |
| if (child->is<ComponentOrigin>()) |
| { |
| return child->as<ComponentOrigin>(); |
| } |
| } |
| return nullptr; |
| } |
| } // namespace |
| |
| LayoutComponent* rive::contentSizingLayout(Component* parent) |
| { |
| for (Component* p = parent; p != nullptr; p = p->parent()) |
| { |
| if (p->is<LayoutComponent>()) |
| { |
| return p->as<LayoutComponent>(); |
| } |
| // A group stops content sizing; a provider is sized by the engine. |
| if (stopsContentSizing(p) || LayoutNodeProvider::from(p) != nullptr) |
| { |
| return nullptr; |
| } |
| auto* sizeable = IntrinsicallySizeable::from(p); |
| if (sizeable != nullptr && !sizeable->shouldPropagateSizeToChildren()) |
| { |
| return nullptr; |
| } |
| } |
| return nullptr; |
| } |
| |
| LayoutComponent* rive::owningLayout(Component* component) |
| { |
| for (Component* p = component; p != nullptr; p = p->parent()) |
| { |
| if (p->is<LayoutComponent>()) |
| { |
| return p->as<LayoutComponent>(); |
| } |
| } |
| return nullptr; |
| } |
| |
| #if defined(WITH_RIVE_LAYOUT) && defined(WITH_RIVE_TOOLS) && defined(DEBUG) |
| uint32_t LayoutData::count = 0; |
| #endif |
| |
| float LayoutComponent::pivotOriginX() const |
| { |
| if (!hasLayoutFlag(LayoutComponentFlags::HasComponentOrigin)) |
| { |
| return 0.0f; |
| } |
| auto* origin = originChild(this); |
| return origin != nullptr ? origin->originX() : 0.0f; |
| } |
| |
| float LayoutComponent::pivotOriginY() const |
| { |
| if (!hasLayoutFlag(LayoutComponentFlags::HasComponentOrigin)) |
| { |
| return 0.0f; |
| } |
| 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; |
| } |
| |
| StatusCode LayoutComponent::import(ImportStack& importStack) |
| { |
| // Files exported before 7.3 composed a layout's transform from the solved |
| // slot alone, so any stored rotation/scale was written but never applied. |
| // Keep that legacy behavior for those files; newer files compose it on top |
| // of the slot. See File::minorVersion. |
| int major = importStack.majorVersion(); |
| int minor = importStack.minorVersion(); |
| setLayoutFlag(LayoutComponentFlags::ComposeTransform, |
| major > 7 || (major == 7 && minor >= 3)); |
| return Super::import(importStack); |
| } |
| |
| Core* LayoutComponent::clone() const |
| { |
| LayoutComponent* twin = LayoutComponentBase::clone()->as<LayoutComponent>(); |
| twin->setLayoutFlag(LayoutComponentFlags::ComposeTransform, |
| hasLayoutFlag(LayoutComponentFlags::ComposeTransform)); |
| // The clip-may-be-dynamic (KeyedObject::onAddedDirty) and listener-target |
| // (StateMachineListener::onAddedClean) reasons for ForceDrawableProxy are |
| // stamped only on the source, since instances share animations and state |
| // machines. Carry the flag so an instance's proxy is still injected before |
| // its first sort. (The scroll-interaction reason re-marks per instance in |
| // buildDependencies, so carrying it here is a harmless no-op for that |
| // case.) |
| twin->setLayoutFlag( |
| LayoutComponentFlags::ForceDrawableProxy, |
| hasLayoutFlag(LayoutComponentFlags::ForceDrawableProxy)); |
| return twin; |
| } |
| |
| bool LayoutComponent::composesLayoutOffset() const |
| { |
| return hasLayoutFlag(LayoutComponentFlags::ComposeTransform) && |
| !is<Artboard>(); |
| } |
| |
| Vec2D LayoutComponent::localAnchor() const |
| { |
| if (!hasLayoutFlag(LayoutComponentFlags::HasComponentOrigin) || |
| is<Artboard>()) |
| { |
| return Vec2D(); |
| } |
| return originOffset(); |
| } |
| |
| Vec2D LayoutComponent::originOffset() const |
| { |
| return Vec2D(pivotOriginX() * m_layout.width(), |
| pivotOriginY() * m_layout.height()); |
| } |
| |
| Vec2D LayoutComponent::layoutTranslation() const |
| { |
| // Our own origin deliberately does not enter here: contents sit within |
| // our box whatever it is, so nothing inside has to compensate. An |
| // artboard's origin does define where its local zero sits, so step back |
| // by it to align to its box. |
| auto location = Vec2D(m_layout.left(), m_layout.top()); |
| if (parent() != nullptr && parent()->is<Artboard>()) |
| { |
| auto art = parent()->as<Artboard>(); |
| location -= Vec2D(art->layoutWidth() * art->originX(), |
| art->layoutHeight() * art->originY()); |
| } |
| return location; |
| } |
| |
| Mat2D LayoutComponent::buildOwnTransform() const |
| { |
| // Outermost, matching TransformComponent's T * R * S, so the offset is |
| // not rotated or scaled by our own transform. |
| Mat2D own = |
| composesLayoutOffset() |
| ? Mat2D::fromTranslation(Vec2D(NodeBase::x(), NodeBase::y())) |
| : Mat2D(); |
| |
| // Pivot about the origin. The box stays put, so wrap rather than fold |
| // into the frame. |
| if (hasLayoutFlag(LayoutComponentFlags::ComposeTransform) && |
| (rotation() != 0.0f || scaleX() != 1.0f || scaleY() != 1.0f)) |
| { |
| Mat2D local = |
| rotation() != 0.0f ? Mat2D::fromRotation(rotation()) : Mat2D(); |
| local.scaleByValues(scaleX(), scaleY()); |
| auto pivot = originOffset(); |
| if (pivot.x != 0.0f || pivot.y != 0.0f) |
| { |
| local = Mat2D::multiply( |
| Mat2D::fromTranslate(pivot.x, pivot.y), |
| Mat2D::multiply(local, |
| Mat2D::fromTranslate(-pivot.x, -pivot.y))); |
| } |
| own = Mat2D::multiply(own, local); |
| } |
| return own; |
| } |
| |
| // Same unrooted guard as Node's — nothing to be relative to yet. |
| float LayoutComponent::computedRootX() |
| { |
| if (artboard() == nullptr) |
| { |
| return 0.0f; |
| } |
| return artboard()->rootTransform(worldTransform() * localAnchor()).x; |
| } |
| |
| float LayoutComponent::computedRootY() |
| { |
| if (artboard() == nullptr) |
| { |
| return 0.0f; |
| } |
| return artboard()->rootTransform(worldTransform() * localAnchor()).y; |
| } |
| |
| void LayoutComponent::updateTransform() { m_Transform = buildOwnTransform(); } |
| |
| void LayoutComponent::composeWorldTransform() |
| { |
| Mat2D parentWorld = |
| parent() != nullptr && parent()->is<WorldTransformComponent>() |
| ? parent()->as<WorldTransformComponent>()->worldTransform() |
| : Mat2D(); |
| // Insert where the layout placed us, the same shape Shape/Text/Image |
| // use for a participant. |
| Mat2D base = Mat2D::fromTranslation(layoutTranslation()); |
| m_WorldTransform = |
| Mat2D::multiply(Mat2D::multiply(parentWorld, base), m_Transform); |
| } |
| |
| 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)) |
| { |
| // Not left to Super's Transform-dirt pass: the pivot scales by the |
| // solved size, so a re-solve alone can stale it. |
| m_Transform = buildOwnTransform(); |
| composeWorldTransform(); |
| updateConstraints(); |
| } |
| if (hasDirt(value, |
| ComponentDirt::Path | ComponentDirt::WorldTransform | |
| ComponentDirt::LayoutStyle)) |
| { |
| updateRenderPath(); |
| } |
| |
| setLayoutFlag(LayoutComponentFlags::PositionLeftChanged, false); |
| setLayoutFlag(LayoutComponentFlags::PositionTopChanged, false); |
| } |
| |
| void LayoutComponent::widthOverride(float width, int unitValue, bool isRow) |
| { |
| m_widthOverride = width; |
| m_widthUnitValueOverride = static_cast<int8_t>(unitValue); |
| setLayoutFlag(LayoutComponentFlags::ParentIsRow, isRow); |
| markLayoutNodeDirty(); |
| } |
| |
| void LayoutComponent::heightOverride(float height, int unitValue, bool isRow) |
| { |
| m_heightOverride = height; |
| m_heightUnitValueOverride = static_cast<int8_t>(unitValue); |
| setLayoutFlag(LayoutComponentFlags::ParentIsRow, isRow); |
| markLayoutNodeDirty(); |
| } |
| |
| void LayoutComponent::parentIsRow(bool isRow) |
| { |
| setLayoutFlag(LayoutComponentFlags::ParentIsRow, isRow); |
| markLayoutNodeDirty(); |
| } |
| |
| #ifdef WITH_RIVE_LAYOUT |
| LayoutScaleType LayoutComponent::effectiveWidthScaleType() |
| { |
| if (canHaveOverrides() && m_widthUnitValueOverride != -1) |
| { |
| switch (YGUnit(m_widthUnitValueOverride)) |
| { |
| case YGUnitPoint: |
| case YGUnitPercent: |
| return LayoutScaleType::fixed; |
| case YGUnitAuto: |
| return hasLayoutFlag( |
| LayoutComponentFlags::WidthIntrinsicallySizeOverride) |
| ? LayoutScaleType::hug |
| : LayoutScaleType::fill; |
| default: |
| break; |
| } |
| } |
| return m_style != nullptr ? m_style->widthScaleType() |
| : LayoutScaleType::fixed; |
| } |
| |
| LayoutScaleType LayoutComponent::effectiveHeightScaleType() |
| { |
| if (canHaveOverrides() && m_heightUnitValueOverride != -1) |
| { |
| switch (YGUnit(m_heightUnitValueOverride)) |
| { |
| case YGUnitPoint: |
| case YGUnitPercent: |
| return LayoutScaleType::fixed; |
| case YGUnitAuto: |
| return hasLayoutFlag(LayoutComponentFlags:: |
| HeightIntrinsicallySizeOverride) |
| ? LayoutScaleType::hug |
| : LayoutScaleType::fill; |
| default: |
| break; |
| } |
| } |
| return m_style != nullptr ? m_style->heightScaleType() |
| : LayoutScaleType::fixed; |
| } |
| #endif |
| |
| void LayoutComponent::parentIsStack(bool isStack) |
| { |
| if (hasLayoutFlag(LayoutComponentFlags::ParentIsStack) == isStack) |
| { |
| return; |
| } |
| setLayoutFlag(LayoutComponentFlags::ParentIsStack, isStack); |
| markLayoutNodeDirty(); |
| } |
| |
| void LayoutComponent::widthIntrinsicallySizeOverride(bool intrinsic) |
| { |
| setLayoutFlag(LayoutComponentFlags::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) |
| { |
| setLayoutFlag(LayoutComponentFlags::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_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; |
| } |
| #ifdef WITH_RIVE_EDITOR |
| // Coop-apply hydration: skip the render/layout side effects — the |
| // graph is partial here and they re-fire when the renderer attaches |
| // and the first pump runs. Runtime .riv imports are marked validated |
| // at read time and never see a pump, so they must run the wiring now |
| // or Yoga layout stays NaN. |
| if (hasValidated()) |
| #endif |
| { |
| markLayoutStyleDirty(); |
| syncLayoutChildren(); |
| propagateCollapse(isCollapsed()); |
| } |
| return StatusCode::Ok; |
| } |
| |
| void LayoutComponent::drawProxy(Renderer* renderer) |
| { |
| // Pair the save/clip here with the restore in draw(). Record whether we |
| // actually saved so draw() never restores a save that wasn't issued. Set OR |
| // clear it every time the proxy draws: the proxy is injected once and stays |
| // in the draw order, so a later pass where clip() is off must clear a stale |
| // true from an earlier pass. (When the proxy isn't in the draw order at |
| // all, this never runs, the flag stays false, and draw() skips the |
| // restore.) |
| const bool saveForClip = clip(); |
| if (saveForClip) |
| { |
| renderer->save(); |
| renderer->clipPath(mutableRenderPaths().world.renderPath(this)); |
| } |
| setLayoutFlag(LayoutComponentFlags::ClipSaved, saveForClip); |
| 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. Pair the |
| // restore with drawProxy()'s save via ClipSaved rather than clip(): if the |
| // proxy wasn't in the draw order (so drawProxy() never saved) but clip() is |
| // true, restoring here would underflow the renderer's save stack. |
| if (hasLayoutFlag(LayoutComponentFlags::ClipSaved)) |
| { |
| setLayoutFlag(LayoutComponentFlags::ClipSaved, false); |
| renderer->restore(); |
| } |
| } |
| |
| void LayoutComponent::updateRenderPath() |
| { |
| if (isHidden()) |
| { |
| return; |
| } |
| if (m_ShapePaints.empty() && !clip() && |
| !hasLayoutFlag(LayoutComponentFlags::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(); |
| } |
| } |
| |
| auto& renderPaths = mutableRenderPaths(); |
| renderPaths.background.rewind(); |
| Path::addRoundedRect( |
| renderPaths.background, |
| AABB::fromLTWH(0.0f, 0.0f, m_layout.width(), m_layout.height()), |
| tl, |
| tr, |
| br, |
| bl); |
| |
| renderPaths.local.rewind(); |
| renderPaths.local.addPath(renderPaths.background); |
| |
| renderPaths.world.rewind(false, FillRule::clockwise); |
| renderPaths.world.addPath(renderPaths.background, &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 hasLayoutFlag(LayoutComponentFlags::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::isStackContainer() |
| { |
| return style() != nullptr && style()->isStack(); |
| } |
| |
| 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); |
| realWidthScaleType = effectiveWidthScaleType(); |
| } |
| if (m_heightUnitValueOverride != -1) |
| { |
| realHeightUnits = YGUnit(m_heightUnitValueOverride); |
| realHeightScaleType = effectiveHeightScaleType(); |
| } |
| } |
| 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; |
| // Appliers write straight into the node's own style; no scratch copy. |
| 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; |
| // A hosted artboard (list item, nested artboard layout) has no parent() to |
| // walk, so its host pushes the container's stack state in. Only stack is |
| // pushed: a grid already auto-places these nodes correctly, and claiming |
| // grid here would move them off the flex sizing path they use today. |
| bool parentIsStack = |
| parentLayoutStyle != nullptr |
| ? parentLayoutStyle->isStack() |
| : (canHaveOverrides() && |
| hasLayoutFlag(LayoutComponentFlags::ParentIsStack)); |
| bool parentIsGridLike = parentLayoutStyle != nullptr |
| ? parentLayoutStyle->isGrid() |
| : parentIsStack; |
| 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; |
| auto widthScale = effectiveWidthScaleType(); |
| syncContext.widthFills = widthScale == LayoutScaleType::fill; |
| syncContext.inlineHugs = widthScale == LayoutScaleType::hug; |
| syncContext.parentIsRow = effectiveParentIsRow(); |
| syncContext.isLTR = actualDirection() != LayoutDirection::rtl; |
| syncContext.hasLayoutParent = layoutParent() != nullptr; |
| m_layoutData->applyLayoutStyles(ygStyle, syncContext); |
| |
| // 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(); |
| } |
| |
| #ifdef WITH_RIVE_TOOLS |
| bool LayoutComponent::collectsForLayout(Component* child) |
| { |
| // Runs the real walk rather than a copy of the rule. |
| bool found = false; |
| forEachLayoutProvider(this, |
| [child, &found](Component* c, LayoutNodeProvider*) { |
| if (c == child) |
| { |
| found = true; |
| } |
| }); |
| return found; |
| } |
| #endif |
| |
| 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 groups — a group's contents are |
| // free content, never content-sized from here. A Solo is not a barrier: |
| // its children content-size as direct children would. A participating |
| // child is caught by the provider check below. |
| if (child->is<LayoutComponent>() || stopsContentSizing(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 (hasLayoutFlag(LayoutComponentFlags::JustAddedToHost)) |
| { |
| setLayoutFlag(LayoutComponentFlags::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(); |
| setLayoutFlag(LayoutComponentFlags::ForceUpdateLayoutBounds, false); |
| return; |
| } |
| |
| if (animate && animates()) |
| { |
| auto animationData = currentAnimationData(); |
| if (newLayout != animationData->to || |
| hasLayoutFlag(LayoutComponentFlags::ForceUpdateLayoutBounds)) |
| { |
| if (animationData->elapsedSeconds != 0.0f) |
| { |
| if (hasLayoutFlag(LayoutComponentFlags::IsSmoothingAnimation)) |
| { |
| // we were already smoothening. |
| m_animationDataA.copy(m_animationDataB); |
| } |
| setLayoutFlag(LayoutComponentFlags::IsSmoothingAnimation, true); |
| } |
| else |
| { |
| setLayoutFlag(LayoutComponentFlags::IsSmoothingAnimation, |
| false); |
| } |
| animationData = currentAnimationData(); |
| animationData->from = m_layout; |
| animationData->to = newLayout; |
| animationData->elapsedSeconds = 0.0f; |
| propagateSize(); |
| markWorldTransformDirty(); |
| } |
| } |
| else if (newLayout != m_layout || |
| hasLayoutFlag(LayoutComponentFlags::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(); |
| } |
| setLayoutFlag(LayoutComponentFlags::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 hasLayoutFlag(LayoutComponentFlags::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 (hasLayoutFlag(LayoutComponentFlags::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); |
| setLayoutFlag(LayoutComponentFlags::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 (hasLayoutFlag(LayoutComponentFlags::IsSmoothingAnimation)) |
| { |
| setLayoutFlag(LayoutComponentFlags::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) |
| { |
| setLayoutFlag(LayoutComponentFlags::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 (!hasLayoutFlag(LayoutComponentFlags::PositionLeftChanged)) |
| { |
| m_style->positionLeft(layoutBounds().left()); |
| } |
| if (!hasLayoutFlag(LayoutComponentFlags::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()) {} |
| |
| 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; } |
| bool LayoutComponent::isStackContainer() { 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_renderPaths != nullptr ? &m_renderPaths->world : nullptr; |
| } |
| ShapePaintPath* LayoutComponent::localPath() |
| { |
| return m_renderPaths != nullptr ? &m_renderPaths->local : nullptr; |
| } |
| ShapePaintPath* LayoutComponent::localClockwisePath() |
| { |
| return m_renderPaths != nullptr ? &m_renderPaths->local : nullptr; |
| } |
| |
| Component* LayoutComponent::pathBuilder() { return this; } |