blob: 15fcbe658ec203644fde21db9d9a1e47fec02aba [file]
#include "rive/nested_artboard.hpp"
#include "rive/component_origin.hpp"
#include "rive/artboard.hpp"
#include "rive/backboard.hpp"
#include "rive/file.hpp"
#include "rive/importers/import_stack.hpp"
#include "rive/importers/backboard_importer.hpp"
#include "rive/data_bind/data_bind.hpp"
#include "rive/data_bind/data_values/data_type.hpp"
#include "rive/focus_data.hpp"
#include "rive/input/focus_manager.hpp"
#include "rive/input/focusable.hpp"
#include "rive/nested_animation.hpp"
#include "rive/animation/nested_state_machine.hpp"
#include "rive/data_bind/data_bind_path.hpp"
#include "rive/clip_result.hpp"
#include "rive/text/text_input.hpp"
#include "rive/view_model_type.hpp"
#include "rive/viewmodel/viewmodel.hpp"
#include <limits>
#include <cassert>
using namespace rive;
namespace
{
std::vector<rcp<ViewModelInstance>> buildVMIList(
const rcp<ViewModelInstance>& primary,
const std::vector<rcp<ViewModelInstance>>& globals)
{
std::vector<rcp<ViewModelInstance>> list;
list.reserve((primary != nullptr ? 1 : 0) + globals.size());
if (primary != nullptr)
{
list.push_back(primary);
}
for (auto& g : globals)
{
if (g != nullptr)
{
list.push_back(g);
}
}
return list;
}
} // namespace
NestedArtboard::NestedArtboard() {}
NestedArtboard::~NestedArtboard()
{
// Release dependencies of nested animations BEFORE m_Instance is destroyed.
// The nested animations (like NestedStateMachine) hold
// StateMachineInstances that reference m_Instance. If we don't release them
// here, their destructors (called later when the parent artboard destroys
// them from m_Objects) will try to access the already-freed m_Instance.
for (auto& animation : m_NestedAnimations)
{
animation->releaseDependencies();
}
// Also release the bound state machine's dependencies if it exists
if (m_boundNestedStateMachine)
{
m_boundNestedStateMachine->releaseDependencies();
}
m_viewModelInstance = nullptr;
// Release the owned bound VMI. The borrowed stateful child VMI is cleaned
// up by the parent Artboard's m_Objects teardown.
if (m_ownsActiveVmi && m_activeViewModelInstance != nullptr)
{
m_activeViewModelInstance->unref();
}
m_activeViewModelInstance = nullptr;
m_ownsActiveVmi = false;
// Release our extra refs on the global VMI children (the artboard's
// m_Objects still holds its own ref for each).
m_globalViewModelInstances.clear();
// Scope outlives nested instance swaps; remove from parent FocusManager
// when the host component is destroyed.
if (m_focusScope != nullptr && m_focusScope->manager() != nullptr)
{
m_focusScope->manager()->removeChild(m_focusScope);
}
m_focusScope = nullptr;
}
Core* NestedArtboard::clone() const
{
NestedArtboard* nestedArtboard =
static_cast<NestedArtboard*>(NestedArtboardBase::clone());
nestedArtboard->file(file());
// Carry the swap-slot flag to clones. It is detected on the SOURCE
// artboard's host (import populates the source's data binds before
// onAddedClean)
if (isArtboardDataBound())
{
nestedArtboard->m_hostFlags |=
NestedArtboardHostFlags::artboardDataBound;
}
if (m_referencedArtboard == nullptr)
{
return nestedArtboard;
}
auto ni = m_referencedArtboard->instance();
nestedArtboard->referencedArtboard(ni.release());
return nestedArtboard;
}
void NestedArtboard::nest(Artboard* artboard)
{
m_referencedArtboard = artboard;
if (!m_referencedArtboard->isInstance())
{
// We're just marking the source artboard so we can later instance from
// it. No need to advance it or change any of its properties.
// E.g. at import time, we return here.
return;
}
m_referencedArtboard->frameOrigin(false);
m_referencedArtboard->hostOpacity(renderOpacity());
m_referencedArtboard->volume(artboard->volume());
m_Instance = nullptr;
if (artboard->isInstance())
{
m_Instance.reset(
static_cast<ArtboardInstance*>(artboard)); // take ownership
}
// This allows for swapping after initial load (after onAddedClean has
// already been called).
m_referencedArtboard->host(this);
// Re-push any authored origin override onto the freshly mounted instance.
// On initial import the override child may not be linked yet; onAddedClean
// covers that case.
applyOriginOverride();
}
void NestedArtboard::applyOriginOverride()
{
if (m_referencedArtboard == nullptr || !m_referencedArtboard->isInstance())
{
return;
}
for (auto child : children())
{
if (child->is<ComponentOrigin>())
{
auto origin = child->as<ComponentOrigin>();
m_referencedArtboard->originX(origin->originX());
m_referencedArtboard->originY(origin->originY());
return;
}
}
}
bool NestedArtboard::tryScheduleBindStateful()
{
if (m_activeViewModelInstance != nullptr && artboardInstance())
{
m_hostFlags |= NestedArtboardHostFlags::pendingStatefulBinding;
return true;
}
return false;
}
void NestedArtboard::bindStateful()
{
m_hostFlags &= ~NestedArtboardHostFlags::pendingStatefulBinding;
if (artboardInstance() == nullptr)
{
return;
}
// Active VMI is the local-root binding; append the global VMI children.
auto list = buildVMIList(ref_rcp(m_activeViewModelInstance),
m_globalViewModelInstances);
artboardInstance()->bindViewModelInstances(std::move(list), m_dataContext);
for (auto& animation : m_NestedAnimations)
{
if (animation->is<NestedStateMachine>())
{
animation->as<NestedStateMachine>()->dataContext(
artboardInstance()->dataContext());
}
}
}
ViewModelInstance* NestedArtboard::findStatefulChildVmi() const
{
for (auto child : children())
{
if (child->is<ViewModelInstance>())
{
return child->as<ViewModelInstance>();
}
}
return nullptr;
}
void NestedArtboard::setActiveViewModelInstance(ViewModelInstance* vmi,
bool owns)
{
if (m_activeViewModelInstance == vmi)
{
// Same pointer; tolerate an ownership refresh only when both states
// agree it's still owned (idempotent reuse of the bound instance).
return;
}
if (m_ownsActiveVmi && m_activeViewModelInstance != nullptr)
{
m_activeViewModelInstance->unref();
}
m_activeViewModelInstance = vmi;
m_ownsActiveVmi = owns;
}
void NestedArtboard::clearNestedAnimations()
{
for (auto& animation : m_NestedAnimations)
{
// Release the nested animation dependencies. The file will take care of
// destroying the nested animation itself.
animation->releaseDependencies();
}
m_NestedAnimations.clear();
}
void NestedArtboard::updateArtboard(
ViewModelInstanceArtboard* viewModelInstanceArtboard)
{
// A VM artboard swap means this host is data-bound.
m_hostFlags |= NestedArtboardHostFlags::artboardDataBound;
// Resolve the swap target BEFORE tearing anything down. asset == nullptr
// and propertyValue == -1 is the user explicitly clearing the artboard (a
// real "set to null"); any OTHER unresolvable id must leave the currently
// displayed artboard — and its focus tree — untouched, rather than blanking
// focus for content that stays on screen. findArtboard has no side effects,
// so resolving it early is safe.
bool explicitNull = viewModelInstanceArtboard != nullptr &&
viewModelInstanceArtboard->asset() == nullptr &&
viewModelInstanceArtboard->propertyValue() == -1;
Artboard* artboard =
explicitNull
? nullptr
: findArtboard(viewModelInstanceArtboard, parentArtboard(), m_file);
if (!explicitNull && artboard == nullptr)
{
// Unresolved target — keep the outgoing instance and its focus intact.
return;
}
// Detach the outgoing instance's focus tree BEFORE teardown, while every
// component is still alive: clearing focus runs blur callbacks
// (FocusData::blurred walks parents/siblings), which must act on live
// components rather than partially-destroyed ones.
auto* outgoing = artboardInstance(0);
if (outgoing != nullptr)
{
outgoing->cleanupFocusTree();
}
clearDataContext();
clearNestedAnimations();
m_boundNestedStateMachine = nullptr;
if (explicitNull)
{
// Only detach a non-owned reference; an owned instance must be
// destroyed with m_host intact so ~LayoutComponent can clean the
// hosting artboard's dirty layout set.
if (m_referencedArtboard != nullptr && m_Instance == nullptr)
{
m_referencedArtboard->host(nullptr);
}
m_referencedArtboard = nullptr;
m_Instance = nullptr;
setActiveViewModelInstance(nullptr, false);
return;
}
if (artboard != nullptr)
{
auto artboardInstance = artboard->instance();
if (artboard->stateMachineCount() > 0)
{
auto nestedStateMachine = new NestedStateMachine();
nestedStateMachine->animationId(0);
nestedStateMachine->initializeAnimation(artboardInstance.get());
addNestedAnimation(nestedStateMachine);
m_boundNestedStateMachine.reset(static_cast<NestedStateMachine*>(
nestedStateMachine)); // take ownership
}
referencedArtboard(artboardInstance.release());
if (isStateful())
{
auto statefulChild = findStatefulChildVmi();
if (statefulChild != nullptr &&
statefulChild->viewModelId() == artboard->viewModelId())
{
// Source artboard uses the same VM as the stateful child:
// borrow the child instance.
setActiveViewModelInstance(statefulChild, false);
}
else if (m_ownsActiveVmi && m_activeViewModelInstance != nullptr &&
m_activeViewModelInstance->viewModelId() ==
artboard->viewModelId())
{
// Already holding a bound instance for this VM; reuse.
}
else
{
auto vm = m_file != nullptr
? m_file->viewModel(artboard->viewModelId())
: nullptr;
rcp<ViewModelInstance> newBound =
vm != nullptr ? m_file->createDefaultViewModelInstance(vm)
: nullptr;
ViewModelInstance* raw = newBound.release();
setActiveViewModelInstance(raw, /*owns=*/raw != nullptr);
if (raw != nullptr)
{
m_file->completeViewModelProperties(raw);
}
}
}
else
{
setActiveViewModelInstance(nullptr, false);
}
if (viewModelInstanceArtboard->boundViewModelInstance())
{
bindViewModelInstance(
viewModelInstanceArtboard->boundViewModelInstance(),
m_dataContext);
}
else if (tryScheduleBindStateful())
{
bindStateful();
}
else if (m_dataContext != nullptr && m_viewModelInstance == nullptr)
{
internalDataContext(m_dataContext);
}
else if (m_viewModelInstance != nullptr)
{
bindViewModelInstance(m_viewModelInstance, m_dataContext);
}
// TODO: @hernan review what dirt to add
addDirt(ComponentDirt::Filthy);
// Share the parent's focus manager with the swapped-in artboard's
// state machine BEFORE the focus sync: its listener groups (pointer
// focus, key/text input) must act on the same focus state Tab
// traversal uses. The wiring in
// NestedStateMachine::initializeAnimation is skipped here because the
// runtime-created bound state machine is unparented.
auto* parentAb = parentArtboard();
auto* parentFM =
parentAb != nullptr ? parentAb->focusManager() : nullptr;
if (parentFM != nullptr && m_boundNestedStateMachine != nullptr)
{
auto* smi = m_boundNestedStateMachine->stateMachineInstance();
if (smi != nullptr && smi->focusManager() != parentFM)
{
smi->setExternalFocusManager(parentFM);
}
}
// Re-home the nested instance's focus tree under the host's scope.
// setExternalFocusManager above rebuilds the nested tree at the
// manager root, so scope placement must be the final write.
syncNestedFocusTree(FocusData::findClosestFocusNode(this));
}
}
// Focus tree integration for data-bound nested artboards.
//
// When artboardId is bound to a VM artboard property the nested instance can be
// swapped at runtime (e.g. Plain -> Focusable), so its FocusData leaves must
// live in the *parent* StateMachine's FocusManager for Tab order to work across
// the main artboard and nested content. Each data-bound host owns one
// structural m_focusScope (see registerFocusScope) that persists across swaps;
// static nested artboards get no scope and zero extra FocusNode cost.
void NestedArtboard::detectArtboardDataBinding()
{
// Marks hosts that can receive runtime
// artboard swaps so a focus scope is allocated later at tree-build time.
// Only a direct bind of this host's artboardId counts. Match on the
// bind's target property key: it is static file data, available at
// import time on the source artboard
if (isArtboardDataBound())
{
return;
}
auto* parent = artboard();
if (parent != nullptr)
{
for (auto* dataBind : parent->dataBinds())
{
if (dataBind->target() == this &&
dataBind->propertyKey() ==
NestedArtboardBase::artboardIdPropertyKey)
{
m_hostFlags |= NestedArtboardHostFlags::artboardDataBound;
return;
}
}
}
}
void NestedArtboard::registerFocusScope(FocusManager* focusManager,
rcp<FocusNode> parentNode,
bool place)
{
// No-op for static hosts; data-bound hosts lazily get one persistent scope.
if (focusManager == nullptr || !isArtboardDataBound())
{
return;
}
if (m_focusScope == nullptr)
{
m_focusScope = FocusNode::makeStructuralScope();
}
if (!place && m_focusScope->manager() == focusManager)
{
// Ensure-only callers (state machine init, artboard swap) must never
// move an already-registered scope: the full build pass is the only
// ordering authority.
return;
}
// Build pass placement, or first registration. During the depth-first
// build pass every sibling that should precede this scope has already been
// re-appended by the same walk, so appending yields hierarchy order. A
// first registration outside a pass is best-effort (appended under the
// caller's fallback parent) and is normalized by the next build pass.
focusManager->addChild(std::move(parentNode), m_focusScope);
}
void NestedArtboard::syncNestedFocusTree(rcp<FocusNode> fallbackParent,
bool placeScope,
bool forceRebuild)
{
auto* parentAb = artboard();
auto* parentFM = parentAb != nullptr ? parentAb->focusManager() : nullptr;
if (parentFM == nullptr)
{
return;
}
// Place the structural scope first — even with no nested instance yet — so
// a data-bound host whose artboard starts null still has its scope at the
// authored tab position for a later swap to build under. Placement is a
// non-destructive move (addChild), so it never clears focus.
registerFocusScope(parentFM, fallbackParent, placeScope);
auto* nestedInstance = artboardInstance(0);
if (nestedInstance == nullptr)
{
return;
}
// Skip the destructive teardown+rebuild when the subtree already shares
// this manager and was not just re-wired (forceRebuild): rebuilding an
// untouched nested instance clears focus resting inside it, while the scope
// placement above already keeps tab order correct. forceRebuild is set
// after setExternalFocusManager (which rebuilds at the manager root) or on
// a swap.
if (!forceRebuild && nestedInstance->focusManager() == parentFM)
{
return;
}
nestedInstance->cleanupFocusTree();
// Under the scope when data-bound, else under fallbackParent (the caller's
// resolved closest ancestor).
nestedInstance->buildFocusTree(parentFM,
m_focusScope != nullptr ? m_focusScope
: fallbackParent);
}
static Mat2D makeTranslate(const Artboard* artboard)
{
return Mat2D::fromTranslate(-artboard->originX() * artboard->width(),
-artboard->originY() * artboard->height());
}
void NestedArtboard::draw(Renderer* renderer)
{
if (m_needsSaveOperation)
{
renderer->save();
}
renderer->transform(worldTransform());
m_referencedArtboard->drawInternal(renderer);
if (m_needsSaveOperation)
{
renderer->restore();
}
}
bool NestedArtboard::willDraw()
{
return Super::willDraw() && m_referencedArtboard != nullptr;
}
Core* NestedArtboard::hitTest(HitInfo* hinfo, const Mat2D& xform)
{
if (m_referencedArtboard == nullptr)
{
return nullptr;
}
hinfo->mounts.push_back(this);
auto mx = xform * worldTransform() * makeTranslate(m_referencedArtboard);
if (auto c = m_referencedArtboard->hitTest(hinfo, mx))
{
return c;
}
hinfo->mounts.pop_back();
return nullptr;
}
bool NestedArtboard::hitTestHost(const Vec2D& position,
bool skipOnUnclipped,
ArtboardInstance* artboard)
{
return parent()->hitTestPoint(worldTransform() * position,
skipOnUnclipped,
false);
}
Vec2D NestedArtboard::hostTransformPoint(const Vec2D& vec,
ArtboardInstance* artboardInstance)
{
auto localVec = Vec2D::transformMat2D(vec, worldTransform());
auto ab = artboard();
return ab ? ab->rootTransform(localVec) : localVec;
}
Mat2D NestedArtboard::worldTransformForArtboard(ArtboardInstance*)
{
return worldTransform();
}
StatusCode NestedArtboard::import(ImportStack& importStack)
{
importDataBindPath(importStack);
auto backboardImporter =
importStack.latest<BackboardImporter>(Backboard::typeKey);
if (backboardImporter == nullptr)
{
return StatusCode::MissingObject;
}
backboardImporter->addArtboardReferencer(this);
return Super::import(importStack);
}
void NestedArtboard::addNestedAnimation(NestedAnimation* nestedAnimation)
{
m_NestedAnimations.push_back(nestedAnimation);
}
StatusCode NestedArtboard::onAddedClean(CoreContext* context)
{
// N.B. The nested instance will be null here for the source artboards.
// Instances will have a nestedInstance available. This is a good thing as
// it ensures that we only instance animations in artboard instances. It
// does require that we always use an artboard instance (not just the source
// artboard) when working with nested artboards, but in general this is good
// practice for any loaded Rive file.
assert(m_referencedArtboard == nullptr ||
m_referencedArtboard == m_Instance.get());
if (m_Instance)
{
for (auto animation : m_NestedAnimations)
{
animation->initializeAnimation(m_Instance.get());
}
m_referencedArtboard->host(this);
// Children are linked by now, so an authored origin override child is
// resolvable; push it onto the mounted instance.
applyOriginOverride();
}
// ViewModelInstance children belong to NestedArtboards that wrap a
// stateful component Artboard. A single "standard" VMI becomes the active
// local-root binding (borrowed; the parent Artboard's m_Objects teardown
// owns its lifetime), and any number of "global" VMIs are appended to the
// data context passed down to the wrapped artboard.
for (auto child : children())
{
if (!child->is<ViewModelInstance>())
{
continue;
}
auto vmi = child->as<ViewModelInstance>();
auto vm = vmi->viewModel();
auto type = vm != nullptr
? static_cast<ViewModelType>(vm->viewModelType())
: ViewModelType::standard;
m_file->completeViewModelProperties(vmi);
if (type == ViewModelType::global)
{
// The artboard's m_Objects owns the construction ref; take an
// extra ref so the list outlives any teardown ordering.
m_globalViewModelInstances.push_back(ref_rcp(vmi));
}
else if (m_activeViewModelInstance == nullptr)
{
// Borrow the first standard child VMI as the active binding.
m_activeViewModelInstance = vmi;
m_ownsActiveVmi = false;
}
}
tryScheduleBindStateful();
detectArtboardDataBinding();
return Super::onAddedClean(context);
}
void NestedArtboard::update(ComponentDirt value)
{
Super::update(value);
if (m_referencedArtboard == nullptr)
{
return;
}
if (hasDirt(value, ComponentDirt::WorldTransform))
{
// Mark semantic bounds dirty for nodes inside the nested artboard.
// Their root-space bounds depend on the host's world transform.
if (m_Instance != nullptr)
{
m_Instance->markSemanticBoundaryTransformDirty();
}
}
if (hasDirt(value, ComponentDirt::RenderOpacity))
{
m_referencedArtboard->hostOpacity(renderOpacity());
}
// Also flush when the referenced artboard itself is dirty (not just the
// host). The RenderOpacity branch above writes the host opacity onto the
// referenced artboard, which marks its own Components dirt. When the nested
// artboard is paused, advanceComponent short-circuits and never lifts that
// dirt onto the host, so without this the opacity change would never reach
// the child's paints. updatePass recomputes render state from the current
// (frozen) values without advancing animations, preserving pause.
if (hasDirt(value, ComponentDirt::Components) ||
(isPaused() &&
m_referencedArtboard->hasDirt(ComponentDirt::Components)))
{
// We intentionally discard whether or not this updated because by the
// end of the pass all the dirt is removed and only another advance of
// animations/statemachines can re-add it.
m_referencedArtboard->updatePass(false);
}
}
bool NestedArtboard::collapse(bool value)
{
if (!Super::collapse(value))
{
return false;
}
auto* nestedInstance = artboardInstance();
if (nestedInstance == nullptr)
{
return true;
}
// Semantic-only collapse via the artboard boundary node. Only touches
// SemanticData nodes — non-semantic components stay untouched.
nestedInstance->collapseSemanticBoundary(value);
return true;
}
bool NestedArtboard::hasNestedStateMachines() const
{
for (auto animation : m_NestedAnimations)
{
if (animation->is<NestedStateMachine>())
{
return true;
}
}
return false;
}
Span<NestedAnimation*> NestedArtboard::nestedAnimations()
{
return m_NestedAnimations;
}
NestedArtboard* NestedArtboard::nestedArtboard(std::string name) const
{
if (m_Instance != nullptr)
{
return m_Instance->nestedArtboard(name);
}
return nullptr;
}
NestedStateMachine* NestedArtboard::stateMachine(std::string name) const
{
for (auto animation : m_NestedAnimations)
{
if (animation->is<NestedStateMachine>() && animation->name() == name)
{
return animation->as<NestedStateMachine>();
}
}
return nullptr;
}
NestedInput* NestedArtboard::input(std::string name) const
{
return input(name, "");
}
NestedInput* NestedArtboard::input(std::string name,
std::string stateMachineName) const
{
if (!stateMachineName.empty())
{
auto nestedSM = stateMachine(stateMachineName);
if (nestedSM != nullptr)
{
return nestedSM->input(name);
}
}
else
{
for (auto animation : m_NestedAnimations)
{
if (animation->is<NestedStateMachine>())
{
auto input = animation->as<NestedStateMachine>()->input(name);
if (input != nullptr)
{
return input;
}
}
}
}
return nullptr;
}
bool NestedArtboard::worldToLocal(Vec2D world, Vec2D* local)
{
assert(local != nullptr);
if (m_referencedArtboard == nullptr)
{
return false;
}
Mat2D toMountedArtboard;
if (!worldTransform().invert(&toMountedArtboard))
{
return false;
}
*local = toMountedArtboard * world;
return true;
}
Vec2D NestedArtboard::measureLayout(float width,
LayoutMeasureMode widthMode,
float height,
LayoutMeasureMode heightMode)
{
return Vec2D(std::min(widthMode == LayoutMeasureMode::undefined
? std::numeric_limits<float>::max()
: width,
m_Instance ? m_Instance->width() : 0.0f),
std::min(heightMode == LayoutMeasureMode::undefined
? std::numeric_limits<float>::max()
: height,
m_Instance ? m_Instance->height() : 0.0f));
}
void NestedArtboard::controlSize(Vec2D size,
LayoutScaleType widthScaleType,
LayoutScaleType heightScaleType,
LayoutDirection direction)
{}
void NestedArtboard::decodeDataBindPathIds(Span<const uint8_t> value)
{
decodeDataBindPath(value);
}
void NestedArtboard::copyDataBindPathIds(const NestedArtboardBase& object)
{
copyDataBindPath(object.as<NestedArtboard>()->dataBindPath());
}
void NestedArtboard::internalDataContext(rcp<DataContext> value)
{
m_dataContext = value;
m_viewModelInstance = nullptr;
if (artboardInstance() == nullptr)
{
return;
}
// If we have a stateful ViewModelInstance, bind it (with globals) via
// the scheduled stateful path.
if (tryScheduleBindStateful())
{
return;
}
if (!m_globalViewModelInstances.empty())
{
auto list = buildVMIList(nullptr, m_globalViewModelInstances);
artboardInstance()->bindViewModelInstances(std::move(list), value);
for (auto& animation : m_NestedAnimations)
{
if (animation->is<NestedStateMachine>())
{
animation->as<NestedStateMachine>()->dataContext(
artboardInstance()->dataContext());
}
}
return;
}
// Non-stateful path with no globals: just propagate the data context.
artboardInstance()->internalDataContext(value);
for (auto& animation : m_NestedAnimations)
{
if (animation->is<NestedStateMachine>())
{
animation->as<NestedStateMachine>()->dataContext(value);
}
}
}
void NestedArtboard::relinkDataContext(rcp<ViewModelInstance> viewModelInstance)
{
m_viewModelInstance = viewModelInstance;
auto instance = artboardInstance(0);
if (instance && !isStateful())
{
auto dataContext = instance->dataContext();
if (dataContext != nullptr)
{
if (dataContext->mainViewModelInstance() != viewModelInstance)
{
dataContext->viewModelInstance(viewModelInstance);
}
}
instance->relinkDataContext();
}
}
void NestedArtboard::clearDataContext()
{
if (artboardInstance() != nullptr)
{
artboardInstance()->clearDataContext();
for (auto& animation : m_NestedAnimations)
{
if (animation->is<NestedStateMachine>())
{
animation->as<NestedStateMachine>()->clearDataContext();
}
}
}
}
void NestedArtboard::unbind()
{
if (artboardInstance() != nullptr)
{
artboardInstance()->unbind();
}
}
void NestedArtboard::updateDataBinds()
{
if (artboardInstance() != nullptr && !isPaused())
{
artboardInstance()->updateDataBinds();
}
}
void NestedArtboard::bindViewModelInstance(
rcp<ViewModelInstance> viewModelInstance,
rcp<DataContext> parent)
{
m_dataContext = parent;
m_viewModelInstance = viewModelInstance;
if (artboardInstance() == nullptr)
{
return;
}
// Stateful: local root is the active VMI; append nested globals.
if (m_activeViewModelInstance != nullptr)
{
auto list = buildVMIList(ref_rcp(m_activeViewModelInstance),
m_globalViewModelInstances);
artboardInstance()->bindViewModelInstances(std::move(list), parent);
for (auto& animation : m_NestedAnimations)
{
if (animation->is<NestedStateMachine>())
{
animation->as<NestedStateMachine>()->dataContext(
artboardInstance()->dataContext());
}
}
return;
}
// Non-stateful with a passed VMI or nested globals: build composite DC.
if (viewModelInstance != nullptr || !m_globalViewModelInstances.empty())
{
auto list = buildVMIList(viewModelInstance, m_globalViewModelInstances);
artboardInstance()->bindViewModelInstances(std::move(list), parent);
for (auto& animation : m_NestedAnimations)
{
if (animation->is<NestedStateMachine>())
{
animation->as<NestedStateMachine>()->dataContext(
artboardInstance()->dataContext());
}
}
return;
}
// Nothing to merge in — propagate parent context unchanged (legacy
// passthrough). Binding a null instance would call
// Artboard::bindViewModelInstance(nullptr, ...), which now triggers
// unbind(); use internalDataContext to keep the parent context wired.
artboardInstance()->internalDataContext(parent);
for (auto& animation : m_NestedAnimations)
{
if (animation->is<NestedStateMachine>())
{
animation->as<NestedStateMachine>()->dataContext(parent);
}
}
}
float NestedArtboard::calculateLocalElapsedSeconds(float elapsedSeconds)
{
auto localElapsedSeconds = elapsedSeconds * (speed() >= 0 ? speed() : 1);
if (quantize() >= 0)
{
m_cumulatedSeconds += localElapsedSeconds;
auto quantizedSeconds = 1 / quantize();
if (m_cumulatedSeconds > quantizedSeconds)
{
localElapsedSeconds =
std::floor(m_cumulatedSeconds / quantizedSeconds) *
quantizedSeconds;
m_cumulatedSeconds -= localElapsedSeconds;
}
else
{
localElapsedSeconds = 0;
}
}
return localElapsedSeconds;
}
bool NestedArtboard::advanceComponent(float elapsedSeconds, AdvanceFlags flags)
{
if (m_referencedArtboard == nullptr || isCollapsed() || isPaused())
{
return false;
}
if (enums::is_flag_set(m_hostFlags,
NestedArtboardHostFlags::pendingStatefulBinding))
{
bindStateful();
}
bool keepGoing = false;
bool advanceNested =
(flags & AdvanceFlags::AdvanceNested) == AdvanceFlags::AdvanceNested;
auto localElapsedSeconds = calculateLocalElapsedSeconds(elapsedSeconds);
bool newFrame = (flags & AdvanceFlags::NewFrame) == AdvanceFlags::NewFrame;
// If the elapsed time is 0 because of quantization, we still want to
// continue advancing until the cumulated time is flushed
if (localElapsedSeconds == 0 && quantize() >= 0 && newFrame)
{
return true;
}
if (advanceNested)
{
for (auto animation : m_NestedAnimations)
{
// If it is not a new frame, we only advance state machines. And we
// first validate whether their state has changed. Then and only
// then we advance the state machine. This avoids triggering dirt
// from advances that make intermediate value changes but finally
// settle in the same value
if (!newFrame)
{
if (animation->is<NestedStateMachine>())
{
if (animation->as<NestedStateMachine>()->tryChangeState())
{
if (animation->advance(localElapsedSeconds, newFrame))
{
keepGoing = true;
}
}
}
}
else
{
if (animation->advance(localElapsedSeconds, newFrame))
{
keepGoing = true;
}
}
}
}
auto advancingFlags = flags & ~AdvanceFlags::IsRoot;
if (m_referencedArtboard->advanceInternal(localElapsedSeconds,
advancingFlags))
{
keepGoing = true;
}
if (m_referencedArtboard->hasDirt(ComponentDirt::Components))
{
// The animation(s) caused the artboard to need an update.
addDirt(ComponentDirt::Components);
}
return keepGoing;
}
void NestedArtboard::reset()
{
if (m_referencedArtboard)
{
m_referencedArtboard->reset();
}
if (m_activeViewModelInstance != nullptr)
{
m_activeViewModelInstance->advanced();
}
}
void NestedArtboard::file(File* value) { m_file = value; }
File* NestedArtboard::file() const { return m_file; }
int NestedArtboard::referencedArtboardId() { return artboardId(); }
void NestedArtboard::referencedArtboard(Artboard* artboard)
{
assert(artboard != nullptr);
ArtboardReferencer::referencedArtboard(artboard);
nest(artboard);
tryScheduleBindStateful();
}