| #ifndef _RIVE_LAYOUT_COMPONENT_HPP_ |
| #define _RIVE_LAYOUT_COMPONENT_HPP_ |
| #include "rive/advance_flags.hpp" |
| #include "rive/animation/keyframe_interpolator.hpp" |
| #include "rive/drawable.hpp" |
| #include "rive/generated/layout_component_base.hpp" |
| #include "rive/layout/layout_measure_mode.hpp" |
| #include "rive/layout/layout_node_provider.hpp" |
| #include "rive/layout/layout_style_applier.hpp" |
| #include "rive/math/raw_path.hpp" |
| #include "rive/shapes/shape_paint_container.hpp" |
| #include "rive/shapes/shape_paint_path.hpp" |
| #include "rive/advancing_component.hpp" |
| #include "rive/layout/layout_enums.hpp" |
| #include <memory> |
| |
| namespace rive |
| { |
| class AABB; |
| class KeyFrameInterpolator; |
| class LayoutData; |
| class LayoutComponentStyle; |
| class LayoutConstraint; |
| class ComponentOrigin; |
| |
| // 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. |
| bool isTransparentLayoutContainer(Component* component); |
| |
| // Whether a provider reached *through* a transparent container still joins the |
| // layout. An ArtboardComponentList provides a layout node unconditionally — it |
| // never opted in the way a participant does — so a group between one and its |
| // layout is how a file asks for free-form items placed by x/y or a follow-path |
| // constraint. It joins only when explicitly flagged. |
| bool joinsLayoutThroughContainer(Component* component); |
| |
| // Boolean state of a LayoutComponent, packed into one word instead of a dozen |
| // separate bytes. Gains the bitwise operators + rive::enums helpers for free by |
| // having a None == 0 member (see rive/enums.hpp). |
| enum class LayoutComponentFlags : uint16_t |
| { |
| None = 0, |
| ParentIsRow = 1 << 0, // default ON |
| ParentIsStack = 1 << 1, // |
| WidthIntrinsicallySizeOverride = 1 << 2, |
| HeightIntrinsicallySizeOverride = 1 << 3, |
| ForceUpdateLayoutBounds = 1 << 4, |
| PositionLeftChanged = 1 << 5, // default ON |
| PositionTopChanged = 1 << 6, // default ON |
| HasForegroundDrawable = 1 << 7, |
| HasComponentOrigin = 1 << 8, |
| ComposeTransform = 1 << 9, // default ON |
| IsSmoothingAnimation = 1 << 10, |
| JustAddedToHost = 1 << 11, |
| // Forces a DrawableProxy into the draw order even when the layout's current |
| // paint/clip state wouldn't require one, so the proxy can draw or (more |
| // often) be sorted as a hit target. Set for any of: a clip a keyframe/data |
| // bind can toggle on at runtime, a scroll/drag interaction target, or a |
| // state-machine pointer listener target. See the mark* helpers below. |
| ForceDrawableProxy = 1 << 12, |
| // Transient per-draw flag: drawProxy() issued a renderer->save() for |
| // clipping, so draw() owes the matching restore(). Guards against restoring |
| // when drawProxy() never ran (e.g. a pure container whose proxy isn't in |
| // the draw order but whose clip() turned on at runtime). |
| ClipSaved = 1 << 13, |
| }; |
| |
| class Layout |
| { |
| public: |
| Layout() : m_left(0.0f), m_top(0.0f), m_width(0.0f), m_height(0.0f) {} |
| Layout(float left, float top, float width, float height) : |
| m_left(left), m_top(top), m_width(width), m_height(height) |
| {} |
| |
| bool operator==(const Layout& o) const |
| { |
| return m_left == o.m_left && m_top == o.m_top && m_width == o.m_width && |
| m_height == o.m_height; |
| } |
| bool operator!=(const Layout& o) const { return !(*this == o); } |
| |
| static Layout lerp(const Layout& from, const Layout& to, float f) |
| { |
| float fi = 1.0f - f; |
| return Layout(to.m_left * f + from.m_left * fi, |
| to.m_top * f + from.m_top * fi, |
| to.m_width * f + from.m_width * fi, |
| to.m_height * f + from.m_height * fi); |
| } |
| |
| float left() const { return m_left; } |
| float top() const { return m_top; } |
| float width() const { return m_width; } |
| float height() const { return m_height; } |
| |
| private: |
| float m_left; |
| float m_top; |
| float m_width; |
| float m_height; |
| }; |
| |
| class LayoutPadding |
| { |
| public: |
| LayoutPadding() : m_left(0.0f), m_top(0.0f), m_right(0.0f), m_bottom(0.0f) |
| {} |
| LayoutPadding(float left, float top, float right, float bottom) : |
| m_left(left), m_top(top), m_right(right), m_bottom(bottom) |
| {} |
| |
| bool operator==(const LayoutPadding& o) const |
| { |
| return m_left == o.m_left && m_top == o.m_top && m_right == o.m_right && |
| m_bottom == o.m_bottom; |
| } |
| bool operator!=(const LayoutPadding& o) const { return !(*this == o); } |
| |
| float left() const { return m_left; } |
| float top() const { return m_top; } |
| float right() const { return m_right; } |
| float bottom() const { return m_bottom; } |
| |
| private: |
| float m_left; |
| float m_top; |
| float m_right; |
| float m_bottom; |
| }; |
| |
| struct LayoutAnimationData |
| { |
| float elapsedSeconds = 0.0f; |
| Layout from; |
| Layout to; |
| Layout interpolate(float f) const { return Layout::lerp(from, to, f); } |
| void copy(const LayoutAnimationData& from); |
| }; |
| |
| // The render-path buffers a LayoutComponent needs only when it actually paints, |
| // clips, has a foreground drawable, or is an Artboard. These are ~240 bytes |
| // together; heap-allocating them on first use keeps a plain container layout |
| // (which never paints) from carrying them inline. `background` is scratch for |
| // building the rounded-rect; `local`/`world` match the old member defaults |
| // (both default-constructed local, world switched at rewind time). |
| struct LayoutRenderPaths |
| { |
| RawPath background; |
| ShapePaintPath local; |
| ShapePaintPath world; |
| }; |
| |
| class LayoutComponent : public LayoutComponentBase, |
| public ProxyDrawing, |
| public ShapePaintContainer, |
| public AdvancingComponent, |
| public InterpolatorHost, |
| public LayoutNodeProvider, |
| public LayoutStyleApplier |
| { |
| protected: |
| LayoutComponentStyle* m_style = nullptr; |
| LayoutData* m_layoutData; |
| |
| Layout m_layout; |
| LayoutPadding m_layoutPadding; |
| |
| LayoutAnimationData m_animationDataA; |
| LayoutAnimationData m_animationDataB; |
| KeyFrameInterpolator* m_inheritedInterpolator; |
| // The two 1-byte enums and the 2-byte flags word are kept adjacent so they |
| // pack into a single 4-byte slot with no padding. |
| LayoutStyleInterpolation m_inheritedInterpolation = |
| LayoutStyleInterpolation::hold; |
| LayoutDirection m_inheritedDirection = LayoutDirection::inherit; |
| LayoutComponentFlags m_layoutFlags = |
| LayoutComponentFlags::ParentIsRow | |
| LayoutComponentFlags::PositionLeftChanged | |
| LayoutComponentFlags::PositionTopChanged | |
| LayoutComponentFlags::ComposeTransform; |
| float m_inheritedInterpolationTime = 0; |
| // Null until this layout first paints/clips/has a foreground drawable, or |
| // (for Artboard) builds its background/clip path. See LayoutRenderPaths. |
| std::unique_ptr<LayoutRenderPaths> m_renderPaths; |
| // Lazily created: only layouts that actually paint, clip, or can gain a |
| // clip at runtime (see needsDrawableProxy) — or that are referenced as a |
| // scroll/listener target — ever allocate a proxy. A pure container layout |
| // never pays for one. See DrawableProxy for why the object is needed at |
| // all. |
| std::unique_ptr<DrawableProxy> m_proxy; |
| |
| bool hasLayoutFlag(LayoutComponentFlags f) const |
| { |
| return (m_layoutFlags & f) != LayoutComponentFlags::None; |
| } |
| void setLayoutFlag(LayoutComponentFlags f, bool on) |
| { |
| if (on) |
| { |
| m_layoutFlags |= f; |
| } |
| else |
| { |
| m_layoutFlags &= ~f; |
| } |
| } |
| |
| // Allocates the render-path buffers on first use. Callers that build or |
| // hand out a path go through this; the ShapePaintContainer accessors |
| // (worldPath/localPath/localClockwisePath) instead return null while unset, |
| // which is a valid contract and keeps plain containers allocation-free. |
| LayoutRenderPaths& mutableRenderPaths() |
| { |
| if (m_renderPaths == nullptr) |
| { |
| m_renderPaths = std::make_unique<LayoutRenderPaths>(); |
| } |
| return *m_renderPaths; |
| } |
| |
| Artboard* getArtboard() override { return artboard(); } |
| LayoutAnimationData* currentAnimationData(); |
| |
| LayoutComponent* layoutParent() |
| { |
| auto p = parent(); |
| while (p != nullptr) |
| { |
| if (p->is<LayoutComponent>()) |
| { |
| return p->as<LayoutComponent>(); |
| } |
| p = p->parent(); |
| } |
| return nullptr; |
| } |
| bool isCollapsed() const override; |
| void propagateCollapse(bool collapse); |
| bool collapse(bool value) override; |
| float computedLocalX() override { return computedOriginLocal().x; }; |
| float computedLocalY() override { return computedOriginLocal().y; }; |
| float computedWorldX() override |
| { |
| return (worldTransform() * localAnchor()).x; |
| }; |
| float computedWorldY() override |
| { |
| return (worldTransform() * localAnchor()).y; |
| }; |
| float computedRootX() override; |
| float computedRootY() override; |
| float computedWidth() override { return m_layout.width(); }; |
| float computedHeight() override { return m_layout.height(); }; |
| void calculateLayoutInternal(float availableWidth, float availableHeight); |
| |
| private: |
| float m_widthOverride = NAN; |
| float m_heightOverride = NAN; |
| float m_forcedWidth = NAN; |
| float m_forcedHeight = NAN; |
| // Yoga YGUnit values (0..3) plus a -1 sentinel; int8_t is ample. |
| int8_t m_widthUnitValueOverride = -1; |
| int8_t m_heightUnitValueOverride = -1; |
| // Remaining boolean state now lives in m_layoutFlags. Two flags carry the |
| // notable defaults documented here: |
| // - ComposeTransform (default ON): files exported before 7.3 never composed |
| // a layout's own rotation/scale, so any stored value was ignored. Import |
| // clears it for those files; the default keeps current behavior so a |
| // layout built outside of import isn't stuck on the legacy path. See |
| // File::minorVersion. |
| |
| /// Where the layout engine placed us, in the parent's transform frame. |
| Vec2D layoutTranslation() const; |
| /// The stored x/y offset plus rotation/scale about the pivot. |
| Mat2D buildOwnTransform() const; |
| /// The origin's position in the parent's frame. |
| Vec2D computedOriginLocal() const |
| { |
| return layoutTranslation() + m_Transform * localAnchor(); |
| } |
| |
| #ifdef WITH_RIVE_LAYOUT |
| protected: |
| void propagateSizeToChildren(ContainerComponent* component); |
| bool applyInterpolation(float elapsedSeconds, bool animate = true); |
| bool styleDisplayHidden() const; |
| #endif |
| |
| public: |
| // Implemented for ShapePaintContainer. |
| const Mat2D& shapeWorldTransform() const override |
| { |
| return worldTransform(); |
| } |
| |
| StatusCode import(ImportStack& importStack) override; |
| Core* clone() const override; |
| |
| LayoutComponentStyle* style() { return m_style; } |
| void style(LayoutComponentStyle* style) { m_style = style; } |
| |
| void draw(Renderer* renderer) override; |
| void drawProxy(Renderer* renderer) override; |
| bool isProxyHidden() override { return isHidden(); } |
| Core* hitTest(HitInfo*, const Mat2D&) override; |
| bool hitTestPoint(const Vec2D& position, |
| bool skipOnUnclipped, |
| bool isPrimaryHit) override; |
| DrawableProxy* proxy() |
| { |
| if (m_proxy == nullptr) |
| { |
| m_proxy = std::make_unique<DrawableProxy>(this); |
| } |
| return m_proxy.get(); |
| }; |
| // True if this layout has (or can gain) something for its proxy to draw or |
| // hit-test: a background/stroke, an active clip, or one of the deferred |
| // reasons stamped as ForceDrawableProxy (runtime-toggleable clip, scroll |
| // interaction target, or pointer-listener target). When true the artboard |
| // injects the proxy into the draw order so it can be sorted for both |
| // drawing and hit-testing. |
| bool needsDrawableProxy() const |
| { |
| return clip() || !shapePaints().empty() || |
| hasLayoutFlag(LayoutComponentFlags::ForceDrawableProxy); |
| } |
| // The mark* helpers below are the distinct call sites that all set the one |
| // ForceDrawableProxy bit; they differ only in why the proxy is needed. |
| |
| // Called when a keyframe or data bind that targets this layout's `clip` |
| // resolves, guaranteeing a proxy before the artboard's first sort. |
| void markClipMayBeDynamic() |
| { |
| setLayoutFlag(LayoutComponentFlags::ForceDrawableProxy, true); |
| } |
| // Called from a scroll/scrollbar constraint's buildDependencies() (which |
| // runs before the artboard's one-time proxy injection, on every instance) |
| // when this layout is its viewport/thumb/track. Without the proxy in the |
| // draw order, the drag hit target can't be sorted and opaque content on top |
| // would swallow the gesture before the drag ever starts. |
| void markInteractionTarget() |
| { |
| setLayoutFlag(LayoutComponentFlags::ForceDrawableProxy, true); |
| } |
| // Called from StateMachineListener::onAddedClean (which runs on the source |
| // artboard before its one-time proxy injection) when a pointer listener |
| // targets this layout. Carried to instances by clone(), since the listener |
| // hook only runs on the source. Without the proxy in the draw order the hit |
| // target can't be sorted and overlapping opaque content would swallow it. |
| void markListenerTarget() |
| { |
| setLayoutFlag(LayoutComponentFlags::ForceDrawableProxy, true); |
| } |
| virtual void updateRenderPath(); |
| void update(ComponentDirt value) override; |
| void updateTransform() override; |
| void composeWorldTransform() override; |
| /// Only our stored x/y — where the layout put us is not something we |
| /// offset by. Files too old to compose x/y keep reporting their solved |
| /// position. |
| Vec2D composedTranslation() const override |
| { |
| return composesLayoutOffset() ? Vec2D(x(), y()) : layoutTranslation(); |
| } |
| /// Our box starts at local zero, so the origin is the anchor. Zero for |
| /// artboards, which already draw about theirs. In the .cpp: is<Artboard> |
| /// needs the complete type. |
| Vec2D localAnchor() const override; |
| bool composesLayoutOffset() const; |
| void onDirty(ComponentDirt value) override; |
| AABB layoutBounds() override |
| { |
| return AABB::fromLTWH(m_layout.left(), |
| m_layout.top(), |
| m_layout.width(), |
| m_layout.height()); |
| } |
| size_t numLayoutNodes() override { return 1; } |
| #ifdef WITH_RIVE_TOOLS |
| /// Whether the collection walk reaches [child] and takes its node. Not the |
| /// same as [child] finding us by walking up, which passes through anything. |
| bool collectsForLayout(Component* child); |
| #endif |
| AABB constraintBounds() const override { return localBounds(); } |
| AABB localBounds() const override |
| { |
| return AABB::fromLTWH(0.0f, 0.0f, m_layout.width(), m_layout.height()); |
| } |
| virtual AABB worldBounds() const |
| { |
| auto transform = worldTransform(); |
| return AABB::fromLTWH(transform[4], |
| transform[5], |
| m_layout.width(), |
| m_layout.height()); |
| } |
| |
| float layoutX() const { return m_layout.left(); } |
| float layoutY() const { return m_layout.top(); } |
| /// The origin as a fraction of our size, from the optional |
| /// ComponentOrigin child; 0 when absent. Named apart from Artboard's |
| /// inline originX/originY so the two never shadow; Artboard overrides. |
| virtual float pivotOriginX() const; |
| virtual float pivotOriginY() const; |
| void markHasComponentOrigin() |
| { |
| setLayoutFlag(LayoutComponentFlags::HasComponentOrigin, true); |
| } |
| Vec2D originOffset() const; |
| float layoutWidth() { return m_layout.width(); } |
| float layoutHeight() { return m_layout.height(); } |
| float innerWidth() |
| { |
| return m_layout.width() - m_layoutPadding.left() - |
| m_layoutPadding.right(); |
| } |
| float innerHeight() |
| { |
| return m_layout.height() - m_layoutPadding.top() - |
| m_layoutPadding.bottom(); |
| } |
| float paddingLeft() { return m_layoutPadding.left(); } |
| float paddingRight() { return m_layoutPadding.right(); } |
| float paddingTop() { return m_layoutPadding.top(); } |
| float paddingBottom() { return m_layoutPadding.bottom(); } |
| |
| float gapHorizontal(); |
| float gapVertical(); |
| |
| // We provide a way for nested artboards (or other objects) to override this |
| // layout's width/height and unit values. |
| void widthOverride(float width, int unitValue = 1, bool isRow = true); |
| void heightOverride(float height, int unitValue = 1, bool isRow = true); |
| void parentIsRow(bool isRow); |
| void parentIsStack(bool isStack); |
| void widthIntrinsicallySizeOverride(bool intrinsic); |
| void heightIntrinsicallySizeOverride(bool intrinsic); |
| virtual bool canHaveOverrides() { return false; } |
| // Honours the NestedArtboardLayout override; shared via LayoutSyncContext. |
| bool effectiveParentIsRow(); |
| #ifdef WITH_RIVE_LAYOUT |
| // The scale type this layout is actually sized by: the host's override |
| // when it has one (a hosted artboard's own style is not what the user set |
| // on the NestedArtboardLayout), else its own style's. |
| LayoutScaleType effectiveWidthScaleType(); |
| LayoutScaleType effectiveHeightScaleType(); |
| void applyBaseStyle(YGStyle& style, |
| const LayoutSyncContext& context) override; |
| void applyContainerStyle(YGStyle& style, |
| const LayoutSyncContext& context) override; |
| #endif |
| bool mainAxisIsRow(); |
| bool mainAxisIsColumn(); |
| // Whether this layout stacks its children, for hosts that push the |
| // container's state into a hosted artboard. |
| bool isStackContainer(); |
| bool overridesKeyedInterpolation(int propertyKey) override; |
| bool hasShapePaints() const { return m_ShapePaints.size() > 0; } |
| bool advanceComponent(float elapsedSeconds, |
| AdvanceFlags flags = AdvanceFlags::Animate | |
| AdvanceFlags::NewFrame) override; |
| bool isHidden() const override; |
| float forcedWidth() { return m_forcedWidth; } |
| float forcedHeight() { return m_forcedHeight; } |
| void forcedWidth(float width); |
| void forcedHeight(float height); |
| void updateConstraints() override; |
| TransformComponent* transformComponent() override |
| { |
| return this->as<TransformComponent>(); |
| } |
| |
| LayoutComponent(); |
| ~LayoutComponent(); |
| #ifdef WITH_RIVE_LAYOUT |
| |
| void* layoutNode(int index) override; |
| void syncStyle(); |
| void syncLayoutChildren(); |
| void clearLayoutChildren(); |
| virtual void propagateSize(); |
| void updateLayoutBounds(bool animate = true) override; |
| StatusCode onAddedDirty(CoreContext* context) override; |
| StatusCode onAddedClean(CoreContext* context) override; |
| bool advance(float elapsedSeconds); |
| bool animates(); |
| LayoutAnimationStyle animationStyle(); |
| KeyFrameInterpolator* interpolator(); |
| LayoutStyleInterpolation interpolation(); |
| float interpolationTime(); |
| |
| bool cascadeLayoutStyle(LayoutStyleInterpolation inheritedInterpolation, |
| KeyFrameInterpolator* inheritedInterpolator, |
| float inheritedInterpolationTime, |
| LayoutDirection direction) override; |
| bool setInheritedInterpolation( |
| LayoutStyleInterpolation inheritedInterpolation, |
| KeyFrameInterpolator* inheritedInterpolator, |
| float inheritedInterpolationTime); |
| void clearInheritedInterpolation(); |
| void interruptAnimation(); |
| bool isLeaf(); |
| void positionTypeChanged(); |
| void scaleTypeChanged(); |
| void displayChanged(); |
| void flexDirectionChanged(); |
| void layoutTypeChanged(); |
| void directionChanged(); |
| LayoutDirection actualDirection(); |
| // Re-run each child provider's style sync: a participant's yoga style |
| // (flexGrow vs alignSelf/justifySelf/grid placement) is computed against |
| // our grid/flex type + main axis, so switching those must re-sync, not just |
| // mark nodes dirty. |
| void syncChildProviderStyles(); |
| #endif |
| |
| void markPositionLeftChanged() |
| { |
| setLayoutFlag(LayoutComponentFlags::PositionLeftChanged, true); |
| } |
| void markPositionTopChanged() |
| { |
| setLayoutFlag(LayoutComponentFlags::PositionTopChanged, true); |
| } |
| void buildDependencies() override; |
| |
| #ifdef WITH_RIVE_LAYOUT |
| void addLayoutStyleApplier(LayoutStyleApplier* applier) override; |
| #endif |
| void markLayoutNodeDirty( |
| bool shouldForceUpdateLayoutBounds = false) override; |
| void markLayoutStyleDirty(); |
| void clipChanged() override; |
| void registerForegroundDrawable() |
| { |
| setLayoutFlag(LayoutComponentFlags::HasForegroundDrawable, true); |
| } |
| void widthChanged() override; |
| void heightChanged() override; |
| void styleIdChanged() override; |
| void fractionalWidthChanged() override; |
| void fractionalHeightChanged() override; |
| |
| Vec2D measureLayout(float width, |
| LayoutMeasureMode widthMode, |
| float height, |
| LayoutMeasureMode heightMode) override; |
| |
| ShapePaintPath* worldPath() override; |
| ShapePaintPath* localPath() override; |
| ShapePaintPath* localClockwisePath() override; |
| Component* pathBuilder() override; |
| }; |
| |
| // The two upward walks a component can ask about its layout, mirroring the two |
| // downward ones. contentSizingLayout mirrors propagateSizeToChildren: for |
| // content, and a group stops it. owningLayout mirrors forEachLayoutProvider: |
| // for anything in the layout tree, and nothing stops it. |
| // |
| // Both end at the artboard, which is a LayoutComponent whose own parent is |
| // null. So a component directly in the artboard resolves to it, and neither |
| // walk can escape an ArtboardInstance into the artboard hosting it. |
| LayoutComponent* contentSizingLayout(Component* parent); |
| LayoutComponent* owningLayout(Component* component); |
| } // namespace rive |
| |
| #endif |