| #ifndef _RIVE_CORE_HPP_ | |
| #define _RIVE_CORE_HPP_ | |
| #include "rive/rive_types.hpp" | |
| #include "rive/core/binary_reader.hpp" | |
| #include "rive/status_code.hpp" | |
| #include "rive/core/editor_hooks.hpp" | |
| #ifdef WITH_RIVE_EDITOR | |
| #include "rive/core/fractional_index.hpp" | |
| #include <cstdio> | |
| #include <string> | |
| #endif | |
| #ifdef DEBUG | |
| #define DEBUG_PRINT(msg) fprintf(stderr, msg "\n"); | |
| #else | |
| #define DEBUG_PRINT(msg) | |
| #endif | |
| namespace rive | |
| { | |
| class CoreContext; | |
| class DataBind; | |
| class ImportStack; | |
| #ifdef WITH_RIVE_EDITOR | |
| class ObjectArena; | |
| #endif | |
| class Core | |
| { | |
| public: | |
| Core() = default; | |
| // m_firstObserver is intrusive — observers cache `this` as their target | |
| // and chain through `m_nextObserver` rooted at this Core. Cloning a Core | |
| // must NOT inherit the original's observer list head: the new instance | |
| // has no observers of its own, and propagating the head pointer would | |
| // cause notifyPropertyChanged to dirty binds that point at the original, | |
| // and ~Core() on the clone to wrongly detach them. | |
| // m_arena is not inherited either: membership is established by | |
| // ObjectArena::add, so a Core built by copy or move has not been added | |
| // anywhere and would otherwise resolve handles against an arena it is | |
| // not in. | |
| // Nothing needs it to survive a copy — `createInstance` / `clone()` | |
| // build their result through the generated `copy()`, never the copy | |
| // constructor, and `copy()` is also where m_hasValidated propagates | |
| // (RIVE_EDITOR_COPY_VALIDATED). | |
| #ifdef WITH_RIVE_EDITOR | |
| Core(const Core& other) : | |
| m_firstObserver(nullptr), | |
| m_arena(nullptr), | |
| m_hasValidated(other.m_hasValidated) | |
| {} | |
| Core& operator=(const Core& other) | |
| { | |
| // Observer list intentionally not copied — see the runtime | |
| // operator= below. m_arena stays the destination's: the slot | |
| // registration names this address, not the source's. | |
| m_hasValidated = other.m_hasValidated; | |
| return *this; | |
| } | |
| Core(Core&& other) noexcept : | |
| m_firstObserver(nullptr), | |
| m_arena(nullptr), | |
| m_hasValidated(other.m_hasValidated) | |
| {} | |
| Core& operator=(Core&& other) noexcept | |
| { | |
| m_hasValidated = other.m_hasValidated; | |
| return *this; | |
| } | |
| #else | |
| Core(const Core&) : m_firstObserver(nullptr) {} | |
| Core& operator=(const Core&) | |
| { | |
| // Intentional no-op for the observer list: our existing observers | |
| // continue to point at us; theirs continue to point at them. Any | |
| // other state that's safe to copy lives in derived classes. | |
| return *this; | |
| } | |
| // Move ops follow the same rule: the observer chain is bound to the | |
| // original Core's address, so moving cannot transfer it. Default-init | |
| // the new instance's head; the moved-from instance keeps its observers | |
| // (and the responsibility to detach them at destruction). | |
| Core(Core&&) noexcept : m_firstObserver(nullptr) {} | |
| Core& operator=(Core&&) noexcept { return *this; } | |
| #endif | |
| const uint32_t emptyId = -1; | |
| static const int invalidPropertyKey = 0; | |
| virtual ~Core(); | |
| virtual uint16_t coreType() const = 0; | |
| virtual bool isTypeOf(uint16_t typeKey) const = 0; | |
| virtual bool deserialize(uint16_t propertyKey, BinaryReader& reader) = 0; | |
| template <typename T> inline bool is() const | |
| { | |
| return isTypeOf(T::typeKey); | |
| } | |
| template <typename T> inline T* as() | |
| { | |
| assert(is<T>()); | |
| return static_cast<T*>(this); | |
| } | |
| /// Make a shallow copy of the object. | |
| virtual Core* clone() const { return nullptr; } | |
| template <typename T> inline const T* as() const | |
| { | |
| assert(is<T>()); | |
| return static_cast<const T*>(this); | |
| } | |
| /// Called to validate the object can be used at runtime. | |
| virtual bool validate(CoreContext* context) { return true; } | |
| /// Called when the object is first added to the context, other objects | |
| /// may not have resolved their dependencies yet. This is an opportunity | |
| /// to look up objects referenced by id, but not assume that they in | |
| /// turn have resolved their references yet. Called during | |
| /// load/instance. | |
| virtual StatusCode onAddedDirty(CoreContext* context) | |
| { | |
| return StatusCode::Ok; | |
| } | |
| /// Called when all the objects in the context have had onAddedDirty | |
| /// called. This is an opportunity to reference things referenced by | |
| /// dependencies. (A path should be able to find a Shape somewhere in | |
| /// its hierarchy, which may be multiple levels up). | |
| virtual StatusCode onAddedClean(CoreContext* context) | |
| { | |
| return StatusCode::Ok; | |
| } | |
| virtual StatusCode import(ImportStack& importStack) | |
| { | |
| return StatusCode::Ok; | |
| } | |
| // Push-notification hook for target→source data binds. Property setters | |
| // generated in *_base.hpp call this with the affected property key after a | |
| // value changes. Default impl walks the intrusive observer list and | |
| // dirties any DataBind that subscribed for `propertyKey` on this Core. | |
| // | |
| // Non-virtual: every Core inherits the same dispatch. The hot path | |
| // (no observers) is a single null-pointer test. | |
| void notifyPropertyChanged(uint16_t propertyKey); | |
| // Intrusive subscriber list management. Used by DataBind to subscribe to | |
| // target value changes without paying a per-Core map allocation. Each | |
| // Core holds the list head; each subscribing DataBind holds m_nextObserver | |
| // for the same (target, key) chain. Observer order is not guaranteed. | |
| void addPropertyObserver(DataBind* observer); | |
| void removePropertyObserver(DataBind* observer); | |
| private: | |
| DataBind* m_firstObserver = nullptr; | |
| #ifdef WITH_RIVE_EDITOR | |
| public: | |
| /// Called by every generated setter just before mutating the backing | |
| /// field. The runtime build never compiles this — the generator | |
| /// emits the call site under the same WITH_RIVE_EDITOR guard. | |
| /// | |
| /// `oldValue` / `newValue` are pointers to the property's storage in | |
| /// its native field type (float, uint32_t, std::string, etc.). | |
| /// | |
| /// Routing: the default implementation forwards to a static callback | |
| /// that editor_native installs at startup via | |
| /// installPropertyChangingCallback. This means every generated | |
| /// setter on every runtime type automatically participates in the | |
| /// editor's ChangeTracker + undo/redo + ObjectCache refresh — no | |
| /// proprietary override required on per-type Cores. | |
| /// | |
| /// A proprietary Core may still override this directly to special- | |
| /// case its own mutations before/instead of the callback. | |
| virtual void onPropertyChanging(uint16_t propertyKey, | |
| const void* oldValue, | |
| const void* newValue) | |
| { | |
| if (s_onPropertyChanging != nullptr) | |
| { | |
| s_onPropertyChanging(this, propertyKey, oldValue, newValue); | |
| } | |
| } | |
| /// Global callback installed by editor_native at process startup. | |
| /// Takes (core, propertyKey, oldValue, newValue). Null when no | |
| /// editor is attached — then the virtual dispatch above is a no-op | |
| /// and the runtime pays zero cost per setter call. | |
| using OnPropertyChangingCallback = void (*)(Core*, | |
| uint16_t, | |
| const void*, | |
| const void*); | |
| static void installPropertyChangingCallback( | |
| OnPropertyChangingCallback callback) | |
| { | |
| s_onPropertyChanging = callback; | |
| } | |
| /// Walk this object's stored, non-default properties and fire the | |
| /// onPropertyChanging hook (or its typed variants) once per | |
| /// property with old == new == current. Used by the dispatcher's | |
| /// delete path to snapshot the object's state into the active | |
| /// JournalBatch before the object is removed — undo of the delete | |
| /// recreates with defaults, then replays these entries to restore | |
| /// the snapshot. | |
| /// | |
| /// Mirrors packages/core/lib/core.dart's `changeNonDefault()` which | |
| /// the Dart editor's removeObject calls before issuing its removeKey | |
| /// entry. The generator emits overrides per *Base class, one-line- | |
| /// per-stored-property; every override chains up to its `Super::` | |
| /// (and top-level types chain to `Core::`) so this body bottoms | |
| /// out the recursion as a safe no-op for any property keys that | |
| /// don't belong to a registered *Base. | |
| virtual void captureStateForJournal() {} | |
| /// Typed variant for String-valued properties. | |
| /// | |
| /// Motivation: the generic void-pointer hook can't safely read bytes | |
| /// out of `std::string*` vs `Span<const uint8_t>*` given only a | |
| /// field-type id — they share CoreStringType::id == CoreBytesType::id | |
| /// (the file-format ToC packs field ids in 2 bits, and both have | |
| /// the same "varuint length + bytes" skip pattern, so the shared | |
| /// id is correct). Instead, the generator emits a typed hook call | |
| /// for String-typed setters: the std::string reference is captured | |
| /// by value at the call site, no void-cast indirection needed. | |
| /// | |
| /// Bytes (encoded properties) don't flow through this path — they | |
| /// go through the hand-written decodeXxx() pure virtuals. | |
| virtual void onStringChanging(uint16_t propertyKey, | |
| const std::string& oldValue, | |
| const std::string& newValue) | |
| { | |
| if (s_onStringChanging != nullptr) | |
| { | |
| s_onStringChanging(this, propertyKey, oldValue, newValue); | |
| } | |
| } | |
| using OnStringChangingCallback = void (*)(Core*, | |
| uint16_t, | |
| const std::string&, | |
| const std::string&); | |
| static void installStringChangingCallback(OnStringChangingCallback callback) | |
| { | |
| s_onStringChanging = callback; | |
| } | |
| /// Typed variant for FractionalIndex-valued properties (currently | |
| /// `Component.childOrder`). The generic void-pointer hook can't | |
| /// encode a two-field struct safely — the (numerator, denominator) | |
| /// pair has no single representative pointer to carry across the | |
| /// void*. The typed hook fires with both old and new struct values | |
| /// captured by value; editor_native's encoder writes the wire bytes | |
| /// (two varuints) on each side. | |
| virtual void onFractionalIndexChanging(uint16_t propertyKey, | |
| FractionalIndex oldValue, | |
| FractionalIndex newValue) | |
| { | |
| if (s_onFractionalIndexChanging != nullptr) | |
| { | |
| s_onFractionalIndexChanging(this, propertyKey, oldValue, newValue); | |
| } | |
| } | |
| using OnFractionalIndexChangingCallback = void (*)(Core*, | |
| uint16_t, | |
| FractionalIndex, | |
| FractionalIndex); | |
| static void installFractionalIndexChangingCallback( | |
| OnFractionalIndexChangingCallback callback) | |
| { | |
| s_onFractionalIndexChanging = callback; | |
| } | |
| /// Animation-context flag — editor_native's render loop flips | |
| /// this around `advanceAndApply` so the | |
| /// `handlePropertyChanging` hook (core_hook.cpp) can tell an | |
| /// animation-driven write apart from a user / coop / journal | |
| /// edit. The animation branch in the hook records a `(CoopId, | |
| /// propertyKey)` touch for the next frame's | |
| /// `revertTouchedClonesToMain` pass and skips the journal / | |
| /// coop / cross-clone propagation work — playback writes stay | |
| /// scoped to the playing clone. | |
| /// | |
| /// No setter branch in the generated code consults this any | |
| /// more (the override mechanism it used to gate was removed — | |
| /// see [[clone-only-playback-state]]). The flag lives here | |
| /// because the hook is editor-installed but it needs a | |
| /// process-wide signal to detect animation context across the | |
| /// runtime's per-Core setter dispatch. | |
| static bool isAnimationContextActive() { return s_animationContextActive; } | |
| static void setAnimationContextActive(bool active) | |
| { | |
| s_animationContextActive = active; | |
| } | |
| private: | |
| static inline OnPropertyChangingCallback s_onPropertyChanging = nullptr; | |
| static inline OnStringChangingCallback s_onStringChanging = nullptr; | |
| static inline OnFractionalIndexChangingCallback | |
| s_onFractionalIndexChanging = nullptr; | |
| static inline bool s_animationContextActive = false; | |
| public: | |
| #ifdef WITH_RIVE_EDITOR | |
| /// Pointer to the `ObjectArena` that owns this Core. Set by | |
| /// `ObjectArenaImpl::allocate` when the slot is filled. Used by | |
| /// `Core::editorArena()` so any Core method can resolve a | |
| /// `CoreHandle` to its target without threading the arena | |
| /// through every accessor signature. | |
| /// | |
| /// Stays null on Cores never allocated through an arena (test | |
| /// fixtures, or runtime-imported Cores held by `Artboard:: | |
| /// m_Objects`). Resolves on those Cores will fail, but those | |
| /// Cores also don't hold cross-Core `CoreHandle` references — | |
| /// the runtime keeps raw pointers via `#ifndef WITH_RIVE_EDITOR` | |
| /// branches, so the arena is never touched on the runtime hot | |
| /// path. | |
| void editorSetArena(ObjectArena* arena) { m_arena = arena; } | |
| ObjectArena* editorArena() const { return m_arena; } | |
| private: | |
| ObjectArena* m_arena = nullptr; | |
| public: | |
| /// Editor-mode validation flag — mirrors Dart's `Core._hasValidated` | |
| /// from packages/core/lib/core.dart:187. Generated setters in | |
| /// editor builds guard their `${name}Changed()` callback on this | |
| /// bool so that property mutations applied during coop-apply | |
| /// hydration (before `onAddedDirty` / `onAddedClean` have wired | |
| /// `m_Artboard` / `m_Parent`) don't invoke runtime side effects | |
| /// that assume those pointers are valid. | |
| /// | |
| /// editor_native flips this to `true` at the end of its five-pass | |
| /// coop-apply flow (post-`onAddedClean`) so subsequent peer | |
| /// mutations fire callbacks normally. Runtime-only builds compile | |
| /// these members out entirely — the generator emits the callback | |
| /// call unconditionally in the non-`WITH_RIVE_EDITOR` branch. | |
| bool hasValidated() const { return m_hasValidated; } | |
| void markValidated() { m_hasValidated = true; } | |
| private: | |
| bool m_hasValidated = false; | |
| public: | |
| #endif | |
| /// Apply a property change via the public setter path — fires | |
| /// ${name}Changed() and onPropertyChanging. Used by coop | |
| /// APPLY_COOP_CHANGES and SET_PROPERTY dispatch. | |
| /// | |
| /// Contrast with deserialize(): deserialize writes the private field | |
| /// directly during file load (no Changed/onPropertyChanging), which is | |
| /// correct for initial load but wrong for user/coop mutations. Always | |
| /// use applyChange for mutations arriving after load. | |
| /// | |
| /// Returns true if the property was recognized at this type's level | |
| /// or any super's; false if the key is unknown. Top-level `*Base:: | |
| /// applyChange` overrides chain here, giving this body the chance | |
| /// to handle Core-level keys (currently none) and otherwise return | |
| /// false so the caller can route the unknown key elsewhere. | |
| virtual bool applyChange(uint16_t propertyKey, BinaryReader& reader) | |
| { | |
| return false; | |
| } | |
| #endif | |
| }; | |
| } // namespace rive | |
| #endif |