| #ifndef _RIVE_COMPONENT_HPP_ |
| #define _RIVE_COMPONENT_HPP_ |
| #include "rive/component_dirt.hpp" |
| #include "rive/generated/component_base.hpp" |
| #include "rive/dependency_helper.hpp" |
| #include "rive/lazy_vector.hpp" |
| #include "rive/math/vec2d.hpp" |
| #ifdef WITH_RIVE_EDITOR |
| #include "rive/editor/object_arena.hpp" |
| #endif |
| |
| #include <vector> |
| #include <functional> |
| |
| namespace rive |
| { |
| class ContainerComponent; |
| class Artboard; |
| class DataBind; |
| |
| class Component : public ComponentBase, |
| public DependencyHelper<Artboard, Component, Component> |
| { |
| friend class Artboard; |
| |
| private: |
| // Raw parent pointer — sole storage in runtime build, fallback |
| // in editor build for Cores not allocated via `arena.allocate` |
| // (e.g., pump-runtime instances loaded via `File::import`, |
| // whose `editorArena()` is null). |
| ContainerComponent* m_Parent = nullptr; |
| #ifdef WITH_RIVE_EDITOR |
| // Slice 6 Phase B: generational handle path. Used for editor- |
| // flow Cores (top-level + asset + per-instance clones — all |
| // arena-allocated, all with `editorArena()` set). Stale handle |
| // (parent slot freed + generation bumped) → resolves to nullptr; |
| // iteration is naturally null-safe. |
| // |
| // `parent()` and `setParentForEditor()` dispatch on |
| // `editorArena()`: non-null → use this handle; null → fall back |
| // to `m_Parent` raw. Slice 7-revised killed the runtime |
| // importer in editor flow, so every editor-flow Core IS arena- |
| // allocated; the raw fallback only catches pump-runtime |
| // instances which have atomic lifetime (no mid-frame deletion) |
| // and don't need handle protection. |
| CoreHandle m_ParentHandle; |
| |
| public: |
| // Editor-installable hook fired from `parentIdChanged()` after |
| // the generated setter swaps `m_ParentId`. editor_native |
| // registers a function that does the actual re-parenting work |
| // (typed-list swap, m_Artboard re-resolve, cycle prevention). |
| // Runtime never installs anything → call no-ops. |
| using OnParentIdChangedCallback = void (*)(Component*); |
| |
| private: |
| static OnParentIdChangedCallback s_onParentIdChanged; |
| #endif |
| |
| /// Position in the owning artboard's dependency order, written by |
| /// Artboard::sortDependencies. Starts at a sentinel no order can hand out: |
| /// sortDependencies only stamps the components it actually placed, and an |
| /// artboard holds plenty it does not -- reading an uninitialized value for |
| /// those is undefined behavior, and a garbage stamp that happens to land |
| /// inside a real order's range is indistinguishable from a real position. |
| static constexpr unsigned int unsortedGraphOrder = ~0u; |
| unsigned int m_GraphOrder = unsortedGraphOrder; |
| Artboard* m_Artboard = nullptr; |
| LazyVector<DataBind*> m_collapsables; |
| |
| protected: |
| ComponentDirt m_Dirt = ComponentDirt::Filthy; |
| void updateCollapsables(); |
| |
| public: |
| // Required by DependencyHelper's CRTP base — the artboard IS the |
| // dependency root for component graphs. |
| Artboard* dependencyRoot() const { return m_Artboard; } |
| |
| virtual bool collapse(bool value); |
| inline Artboard* artboard() const { return m_Artboard; } |
| bool validate(CoreContext* context) override; |
| StatusCode onAddedDirty(CoreContext* context) override; |
| #ifdef WITH_RIVE_EDITOR |
| // Body in `editor_native/native/src/editor/component_editor.cpp` |
| // — needs the full `ContainerComponent` definition for the |
| // `as<>` upcast, which would create a circular include here |
| // (ContainerComponent extends Component). |
| ContainerComponent* parent() const; |
| #else |
| inline ContainerComponent* parent() const { return m_Parent; } |
| #endif |
| |
| #ifdef WITH_RIVE_EDITOR |
| // Edit-time parent-transition hook. Mirrors Dart's parent-setter |
| // chain (parentIdChanged → parent.internalAddX): subclasses with |
| // a typed child list on the parent (e.g. Path::m_Vertices, |
| // Mesh::m_Vertices, TransformComponent::m_Constraints) override |
| // this to register/unregister themselves on parent transitions. |
| // |
| // Either pointer can be null: |
| // from == nullptr, to != nullptr → first registration (initial |
| // coop hydration or recovery |
| // from orphan). |
| // from != nullptr, to == nullptr → unregister (parent deleted |
| // or child being deleted). |
| // from != nullptr, to != nullptr → re-parent (user-driven edit |
| // or undo/redo). |
| // |
| // Called from three places: |
| // 1. Dispatcher Pass 4.5 after `onAddedClean` for newly- |
| // hydrated, non-orphan Components (initial registration). |
| // 2. `Component::parentIdChanged()` editor override, when the |
| // generated `parentId(value)` setter fires post-validation |
| // (edit-time re-parent). |
| // 3. `EditorFile::removeObject` before the arena frees a Core |
| // (clean unregister). |
| // |
| // Subclasses should be idempotent on `to` (no double-add) and |
| // tolerant of `from == nullptr` (no removal). The runtime |
| // import path doesn't compile or link this hook. |
| virtual void editorParentChanged(ContainerComponent* /*from*/, |
| ContainerComponent* /*to*/) |
| {} |
| |
| // Edit-time re-parent dispatch (Slice 6 Phase G). The generated |
| // setter `parentId(value)` calls `parentIdChanged()` after the |
| // new value lands; we override it here to fan out into a Core- |
| // installed callback. editor_native installs the callback at |
| // library load (see proprietary/core_hook.cpp) — the actual |
| // re-parenting work (resolve new parent, fire |
| // editorParentChanged, swap m_Parent + m_Artboard, update |
| // generic + typed child lists, cycle prevention) lives there. |
| // |
| // Runtime never installs a callback, so this override no-ops at |
| // runtime — same observable behavior as the empty default. |
| void parentIdChanged() override; |
| |
| static void installParentIdChangedCallback( |
| OnParentIdChangedCallback callback) |
| { |
| s_onParentIdChanged = callback; |
| } |
| |
| // Mirror of Dart `Component.validate()` (rive_core/component.dart:409): |
| // `(parent != null && artboard != null) || canBeOrphaned` |
| // Used by the dispatcher's cull pass (post-onAddedClean) to |
| // remove Cores whose ancestor chain is broken — typically because |
| // an unknown typeKey (forward-compat features the editor build |
| // doesn't recognize) returned null from `createObject`, so |
| // descendants of that core have unresolvable parentIds and stay |
| // permanently orphaned. Culling them before `buildDependencies` |
| // / `updatePass` prevents `update()` derefs on `parent()` / |
| // `artboard()` / `renderPaint()` from crashing. |
| // |
| // Default: parent and artboard both non-null. Subclasses can |
| // override to opt into orphan-tolerance (e.g., the Artboard |
| // itself returns true unconditionally — it has no parent). |
| virtual bool editorIsResolved() const |
| { |
| return parent() != nullptr && artboard() != nullptr; |
| } |
| |
| // Editor-only: clear the back-pointer to parent. Called by |
| // `EditorFile::removeObject` on every direct child of a Core |
| // being freed, BEFORE the free runs, so subsequent cull |
| // iterations don't deref a freed parent pointer when they |
| // process the now-orphan child. Body in `component_editor.cpp`. |
| void editorClearParent(); |
| #endif |
| void addCollapsable(DataBind* collapsable); |
| #ifdef WITH_RIVE_EDITOR |
| /// Editor-only: scrub `component` from this Component's |
| /// dependents list. Used by `editorOrphanCore` BEFORE freeing |
| /// the dying Component's arena slot — without this, every |
| /// live Component that recorded the dying one as a dependent |
| /// keeps a dangling pointer, and the next |
| /// `Artboard::sortDependencies` walk segfaults on |
| /// `dying->dependents()`. |
| void editorRemoveDependent(Component* component) |
| { |
| removeDependent(component); |
| } |
| /// Editor-only: drop every entry from this component's dependents |
| /// list. Used by `EditorFile::finalizeBatch` to wipe stale |
| /// cross-batch references before re-running `buildDependencies` |
| /// — coop hydration can add a Component as a dependent in batch |
| /// 1 and then orphan its parent in batch 2; without a fresh- |
| /// start clear, `sortDependencies` walks the orphan back into |
| /// `m_DependencyOrder` and `updatePass` crashes on its now-null |
| /// `parent()` / `artboard()`. |
| void editorClearDependents() { clearDependentsForEditor(); } |
| #endif |
| |
| // TODO: re-evaluate when more of the lib is complete... |
| // These could be pure virtual but we define them empty here to avoid |
| // having to implement them in a bunch of concrete classes that |
| // currently don't use this logic. |
| virtual void buildDependencies() {} |
| virtual void onDirty(ComponentDirt dirt) {} |
| virtual void update(ComponentDirt value) {} |
| |
| unsigned int graphOrder() const { return m_GraphOrder; } |
| bool addDirt(ComponentDirt value, bool recurse = false); |
| inline bool hasDirt(ComponentDirt flag) const |
| { |
| return (m_Dirt & flag) == flag; |
| } |
| static inline bool hasDirt(ComponentDirt value, ComponentDirt flag) |
| { |
| return (value & flag) != ComponentDirt::None; |
| } |
| |
| StatusCode import(ImportStack& importStack) override; |
| |
| virtual bool isCollapsed() const |
| { |
| return (m_Dirt & ComponentDirt::Collapsed) == ComponentDirt::Collapsed; |
| } |
| virtual bool hitTestPoint(const Vec2D& position, |
| bool skipOnUnclipped, |
| bool isPrimaryHit); |
| #ifdef TESTING |
| ComponentDirt dirt() { return m_Dirt; } |
| #endif |
| #ifdef WITH_RIVE_EDITOR |
| /// Editor-only: re-wire `m_Artboard` + `m_Parent` post-hydration. |
| /// Pass 3 `onAddedDirty` may run before parents are in the arena |
| /// for deeply-nested components, leaving `m_Artboard` null and |
| /// the CRTP `dependencyRoot()` (which reads `m_Artboard`) null — |
| /// silently breaking dirt propagation into the artboard's |
| /// component pass. |
| /// `EditorFile::finalizeBatch` calls this after every Pass 1-5 |
| /// completes, when the parent chain is fully hydrated. |
| void setArtboardForEditor(Artboard* ab) { m_Artboard = ab; } |
| // Body in `editor_native/native/src/editor/component_editor.cpp`. |
| // Resolves through `editorArena()` to convert pointer ↔ handle. |
| void setParentForEditor(ContainerComponent* p); |
| /// Editor-only: clear `m_Dirt` to `None`. Components default to |
| /// `Filthy` at construction; the runtime `.riv` path runs an |
| /// initial `updateComponents` pass via `Artboard::initialize()` |
| /// that consumes the Filthy state on first frame. editor_native |
| /// has no equivalent — coop-loaded Cores stay Filthy after |
| /// finalizeBatch, and any that later acquire the Collapsed bit |
| /// (Solo's propagateCollapse runs in Pass E) get |
| /// `Filthy | Collapsed = 0xFFFF`. updateComponents then skips |
| /// them forever, so m_Dirt never clears and subsequent |
| /// `addDirt` calls early-out — the dirty cascade silently |
| /// breaks for collapsed subtrees and any sibling that shares |
| /// dependents with them. |
| void clearDirtForEditor() { m_Dirt = ComponentDirt::None; } |
| |
| /// Editor-only `ComponentFlags` bit positions. Mirrors dart's |
| /// [packages/rive_core/lib/component_flags.dart](packages/rive_core/lib/component_flags.dart) |
| /// 1:1 so the `flags()` byte read by the hierarchy panel decodes |
| /// the same on both sides. The bits are unused at runtime — the |
| /// guards that read them (`Drawable::isHidden`, hit-test locked |
| /// skip, isolation filter) all live under `WITH_RIVE_EDITOR`. |
| enum EditorFlag : uint32_t |
| { |
| EditorFlagHidden = 1u << 0, |
| EditorFlagLocked = 1u << 1, |
| EditorFlagIsolated = 1u << 5, |
| // Inverse overrides (dart's ComponentFlags.visibleOverride / |
| // unlockedOverride): break a hidden/locked state inherited |
| // from an ancestor for this subtree. Mutually exclusive with |
| // the paired flag on the same component — the hierarchy |
| // panel's cycle keeps them coherent. |
| EditorFlagVisibleOverride = 1u << 6, |
| EditorFlagUnlockedOverride = 1u << 7, |
| }; |
| |
| /// True iff [flag] is set on this Component or any ancestor in |
| /// the parent chain. Matches dart's |
| /// `Component.isFlaggedRecursive` — the hidden/locked/isolated |
| /// bits don't physically cascade to children (toggling a parent |
| /// doesn't mass-set the kids), so the cascade gets evaluated on |
| /// every check. Cheap walk — the chain is small. |
| /// |
| /// Body in `editor_native/native/src/editor/component_editor.cpp` |
| /// — needs the full `ContainerComponent` definition for the |
| /// upcast inside the loop, which would create a circular include |
| /// here (ContainerComponent extends Component). |
| bool isFlaggedRecursive(EditorFlag flag) const; |
| |
| /// Walk the PARENT chain (not this) for [flag] paired with its |
| /// inverse [overrideFlag]: nearest override resolves false, |
| /// nearest flag resolves true. Mirrors dart's |
| /// `Component.parentResolvesFlag`. Body in component_editor.cpp. |
| bool editorParentResolvesFlag(uint32_t flag, uint32_t overrideFlag) const; |
| #endif |
| }; |
| } // namespace rive |
| |
| #endif |