blob: 186782d13e8e4741165803e765e8d9d5b687fbff [file]
#ifndef _RIVE_ID_HPP_
#define _RIVE_ID_HPP_
#include <cstdint>
namespace rive
{
// `Id` is the type used by `Core` / `CoreContext` for object references
// (parentId, targetId, styleId, etc.). Its shape depends on how the
// runtime is being compiled:
//
// - Without `WITH_RIVE_EDITOR`: `Id` is a flat `uint32_t` — the index
// of the referenced object in the containing artboard's
// `m_Objects` array (runtime `.riv` file layout).
//
// - With `WITH_RIVE_EDITOR` (editor build): `Id` is a `{client,
// object}` struct that represents BOTH kinds of references:
// a) `.rev` (coop) references: `(client, object)` pair — the
// stable per-peer identity the editor uses.
// b) `.riv` (runtime) references loaded at edit time:
// `(0, runtimeIndex)` — synthesized by
// `CoreIdType::runtimeDeserialize` so edit-time tooling can
// inspect a runtime-loaded object graph through the same
// resolver.
// The struct is implicitly constructible from `uint32_t` with
// `client == 0` so call sites that still hand back runtime-style
// `uint32_t` (generated `xxxId()` getters pre-regeneration) keep
// working without each being rewritten by hand — the resulting
// `Id{0, value}` resolves identically via `Artboard::resolve`. The
// one exception is the unset sentinel, which promotes to
// `kEmptyId` so `== -1` keeps meaning "not set".
#ifdef WITH_RIVE_EDITOR
struct Id
{
// The "unset" / "invalid" sentinel is `{UINT32_MAX, UINT32_MAX}` —
// not `{0, 0}`. Runtime `.riv` files use index `0` as a legitimate
// first-object reference (parentId=0 means "child of the artboard
// at m_Objects[0]"), and we want `Artboard::resolve(Id{0, 0})` to
// keep returning that object. The JSON defs use
// `initialValueRuntime: "-1"` for Id fields that really mean
// "unset"; `IdFieldType::convertCpp` rewrites both `-1` and `0`-
// with-`type:Id`+initial-matches-missing into `kEmptyId`.
static constexpr uint32_t kUnsetField = 0xFFFFFFFFu;
uint32_t client = kUnsetField;
uint32_t object = kUnsetField;
constexpr Id() = default;
constexpr Id(uint32_t c, uint32_t o) : client(c), object(o) {}
// Implicit conversion from runtime-index uint32_t. Still callable
// from code that treats `Id` as an integer (e.g. `id == 0`).
// `0xFFFFFFFF` is the defs' unset sentinel, so it has to land on
// `kEmptyId` rather than a client-0 index that far out.
constexpr Id(uint32_t runtimeIndex) :
client(runtimeIndex == kUnsetField ? kUnsetField : 0),
object(runtimeIndex)
{}
friend constexpr bool operator==(const Id& a, const Id& b)
{
return a.client == b.client && a.object == b.object;
}
friend constexpr bool operator!=(const Id& a, const Id& b)
{
return !(a == b);
}
constexpr bool empty() const
{
return client == kUnsetField && object == kUnsetField;
}
// Direct comparisons against integer literals — specifically
// disambiguate `id == 0 / id != 0` against the combined presence
// of the `Id(uint32_t)` promoting constructor and the implicit
// `operator uint32_t()` narrowing conversion (either path is a
// "valid" way to unify the operand types, and the compiler refuses
// to pick).
//
// Semantics (match pre-refactor runtime behavior):
// - `-1` (== UINT32_MAX as unsigned) is the "unset" sentinel
// per the JSON defs' `initialValueRuntime: "-1"` convention,
// so it compares equal to `kEmptyId` regardless of `client`.
// This preserves the runtime idiom `if (id != -1)` which is
// sprinkled through `animation/*.cpp` to check "is this Id
// field set at all?".
// - Any other integer matches an Id at `client == 0` with that
// `object` index — matching how `Id(uint32_t)` projects
// runtime indices into the struct.
friend constexpr bool operator==(const Id& a, int b)
{
if (b == -1)
return a.empty();
return a.client == 0 && a.object == static_cast<uint32_t>(b);
}
friend constexpr bool operator==(int a, const Id& b) { return b == a; }
friend constexpr bool operator!=(const Id& a, int b) { return !(a == b); }
friend constexpr bool operator!=(int a, const Id& b) { return !(a == b); }
friend constexpr bool operator==(const Id& a, uint32_t b)
{
if (b == 0xFFFFFFFFu)
return a.empty();
return a.client == 0 && a.object == b;
}
friend constexpr bool operator==(uint32_t a, const Id& b) { return b == a; }
friend constexpr bool operator!=(const Id& a, uint32_t b)
{
return !(a == b);
}
friend constexpr bool operator!=(uint32_t a, const Id& b)
{
return !(a == b);
}
// Implicit narrowing to `uint32_t` so runtime-shaped code that
// returns Ids from `uint32_t`-typed APIs (`animation/*.hpp`
// getters, `rive_binding.cpp` callback scaffolding, `size_t`
// bounds checks on vertex / object arrays, etc.) keeps compiling
// against the editor-mode struct. This drops the `client` field —
// which is meaningful only for coop (non-runtime) Ids. For every
// synthesized-from-runtime Id (`client == 0`) the object-field
// IS the runtime index, so the conversion is lossless; for true
// CoopIds it is lossy, and any path that cares about the client
// must take `const Id&` (or access `.client` / `.object`
// explicitly) rather than accept a `uint32_t` here. The pattern
// matches the Dart editor, where runtime-typed APIs use the
// `Id.object` field and editor code branches on `client`.
constexpr operator uint32_t() const { return object; }
};
constexpr Id kEmptyId = {};
#else
using Id = uint32_t;
// Matches the `initialValueRuntime: "-1"` convention used across all
// Id-typed fields in the JSON defs — UINT32_MAX as unsigned.
constexpr Id kEmptyId = 0xFFFFFFFFu;
#endif
} // namespace rive
#endif