blob: 2899b10d84f6627b714a99b20c1bbfa5a7d30252 [file]
#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