| #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(); |
| } |