| #include "rive/artboard.hpp" | |
| #include "rive/file.hpp" | |
| #ifdef WITH_RIVE_SCRIPTING_WASM | |
| #include "rive/wasm/wasm_scripting_vm.hpp" | |
| #endif | |
| #include "rive/animation/keyframe_interpolator.hpp" | |
| #include "rive/artboard_component_list.hpp" | |
| #include "rive/backboard.hpp" | |
| #include "rive/component.hpp" | |
| #include "rive/container_component.hpp" | |
| #include "rive/focus_data.hpp" | |
| #include "rive/input/focus_manager.hpp" | |
| #include "rive/semantic/semantic_data.hpp" | |
| #include "rive/semantic/semantic_manager.hpp" | |
| #include "rive/semantic/semantic_node.hpp" | |
| #include "rive/input/focusable.hpp" | |
| #include "rive/animation/linear_animation_instance.hpp" | |
| #include "rive/custom_property_trigger.hpp" | |
| #include "rive/dependency_sorter.hpp" | |
| #include "rive/data_bind/data_bind.hpp" | |
| #include "rive/data_bind/data_bind_context.hpp" | |
| #include "rive/draw_rules.hpp" | |
| #include "rive/draw_target.hpp" | |
| #include "rive/audio_event.hpp" | |
| #include "rive/draw_target_placement.hpp" | |
| #include "rive/drawable.hpp" | |
| #include "rive/animation/keyed_object.hpp" | |
| #include "rive/animation/keyframe.hpp" | |
| #include "rive/factory.hpp" | |
| #include "rive/renderer.hpp" | |
| #include "rive/shapes/paint/shape_paint.hpp" | |
| #include "rive/importers/import_stack.hpp" | |
| #include "rive/importers/backboard_importer.hpp" | |
| #include "rive/layout_component.hpp" | |
| #include "rive/node.hpp" | |
| #include "rive/foreground_layout_drawable.hpp" | |
| #include "rive/nested_artboard.hpp" | |
| #include "rive/nested_artboard_leaf.hpp" | |
| #include "rive/nested_artboard_layout.hpp" | |
| #include "rive/animation/nested_state_machine.hpp" | |
| #include "rive/joystick.hpp" | |
| #include "rive/data_bind/data_bind.hpp" | |
| #include "rive/data_bind_flags.hpp" | |
| #include "rive/animation/nested_bool.hpp" | |
| #include "rive/animation/nested_number.hpp" | |
| #include "rive/animation/nested_trigger.hpp" | |
| #include "rive/viewmodel/viewmodel_instance.hpp" | |
| #include "rive/viewmodel/viewmodel_instance_value.hpp" | |
| #include "rive/viewmodel/viewmodel.hpp" | |
| #include "rive/view_model_type.hpp" | |
| #include "rive/data_bind/data_context.hpp" | |
| #include "rive/animation/state_machine_input_instance.hpp" | |
| #include "rive/animation/state_machine_instance.hpp" | |
| #include "rive/shapes/shape.hpp" | |
| #include "rive/shapes/clipping_shape.hpp" | |
| #include "rive/text/text_value_run.hpp" | |
| #include "rive/event.hpp" | |
| #include "rive/assets/audio_asset.hpp" | |
| #include "rive/layout/layout_data.hpp" | |
| #include "rive/profiler/profiler_macros.h" | |
| #include "rive/scripted/scripted_object.hpp" | |
| #ifdef WITH_RIVE_SCRIPTING | |
| #ifdef WITH_RIVE_SCRIPTING_LUAU | |
| #include "rive/lua/rive_lua_libs.hpp" | |
| #endif | |
| #endif | |
| #include "rive/async/work_pool.hpp" | |
| #include <set> | |
| #include <unordered_map> | |
| using namespace rive; | |
| uint64_t Artboard::sm_frameId = 0; | |
| Artboard::Artboard() | |
| { | |
| // Artboards need to override default clip value to true. | |
| m_Clip = true; | |
| #ifdef WITH_RIVE_TOOLS | |
| callbackUserData = this; | |
| #endif | |
| } | |
| Artboard::~Artboard() | |
| { | |
| // Tear the focus tree down FIRST, while every component is still alive. | |
| // Detaching a focused node clears focus, and that runs blur callbacks | |
| // (FocusData::blurred walks parents and siblings) — which must not fire | |
| // once the m_Objects loop below has started deleting those siblings. | |
| // | |
| // Only when we own the manager: an adopted one belongs to a host artboard | |
| // or to Dart, either of which may already be gone by the time a finalizer | |
| // frees us, so m_activeFocusManager is not safe to touch. Those artboards | |
| // are cleaned up explicitly by whoever adopted them — | |
| // NestedArtboard::updateArtboard and ArtboardComponentList::removeArtboard | |
| // both call cleanupFocusTree() before teardown — and a host being torn | |
| // down has already run this same block against the shared manager, so | |
| // nothing in a nested subtree is still focused by the time we reach it. | |
| if (m_ownedFocusManager != nullptr && | |
| m_activeFocusManager == m_ownedFocusManager.get()) | |
| { | |
| #ifdef WITH_RIVE_TOOLS | |
| m_ownedFocusManager->setFocusChangedCallback(nullptr); | |
| m_ownedFocusManager->setScrollIntoViewCallback(nullptr); | |
| #endif | |
| cleanupFocusTree(); | |
| } | |
| #ifdef WITH_RIVE_AUDIO | |
| #ifdef EXTERNAL_RIVE_AUDIO_ENGINE | |
| auto audioEngine = m_audioEngine; | |
| #else | |
| auto audioEngine = AudioEngine::RuntimeEngine(false); | |
| #endif | |
| if (audioEngine) | |
| { | |
| audioEngine->stop(this); | |
| } | |
| #endif | |
| unbind(); | |
| // ViewModelInstance and ViewModelInstanceValue inherit from RefCnt. | |
| // | |
| // ViewModelInstance (VMI) ownership: the artboard releases every VMI in | |
| // its m_Objects via unref(). NestedArtboard borrows its stateful child VMI | |
| // without bumping the refcount; dynamically-created bound VMIs are not in | |
| // m_Objects and are released by NestedArtboard itself. | |
| // | |
| // ViewModelInstanceValue (VMV) ownership: always owned by their parent | |
| // ViewModelInstance via rcp<> in m_PropertyValues. When VMI is deleted, | |
| // its destructor clears m_PropertyValues, which unrefs and deletes VMVs. | |
| // | |
| // Strategy: | |
| // 1) Identify VMI/VMV objects by pointer (safe is<>() check BEFORE any | |
| // deletions). | |
| // 2) Delete everything else immediately, deferring VMI unref until after | |
| // hierarchy components are gone. | |
| std::set<Core*> vmObjects; | |
| std::set<ViewModelInstance*> deferredVmiUnrefs; | |
| // First pass: identify ViewModelInstance and ViewModelInstanceValue | |
| // objects while memory is valid (before any deletions). Precompute which | |
| // VMIs should be released so we never dereference pointers after deletes. | |
| auto gatherVmObjects = [&](Core* object) { | |
| if (object == nullptr || object == this) | |
| { | |
| return; | |
| } | |
| if (object->is<ViewModelInstance>()) | |
| { | |
| vmObjects.insert(object); | |
| deferredVmiUnrefs.insert(object->as<ViewModelInstance>()); | |
| return; | |
| } | |
| if (object->is<ViewModelInstanceValue>()) | |
| { | |
| vmObjects.insert(object); | |
| } | |
| }; | |
| for (auto object : m_Objects) | |
| { | |
| gatherVmObjects(object); | |
| } | |
| for (auto object : m_invalidObjects) | |
| { | |
| gatherVmObjects(object); | |
| } | |
| auto isVmObject = [&](Core* object) -> bool { | |
| return vmObjects.count(object) != 0; | |
| }; | |
| // Second pass: delete non-VM objects. | |
| for (auto object : m_Objects) | |
| { | |
| if (object == nullptr || object == this) | |
| { | |
| continue; | |
| } | |
| if (isVmObject(object)) | |
| { | |
| continue; | |
| } | |
| delete object; | |
| } | |
| for (auto object : m_invalidObjects) | |
| { | |
| if (object == nullptr) | |
| { | |
| continue; | |
| } | |
| if (isVmObject(object)) | |
| { | |
| continue; | |
| } | |
| delete object; | |
| } | |
| // Now release deferred ViewModelInstances after hierarchy components have | |
| // been destroyed. Releasing via unref() keeps RefCnt ownership semantics | |
| // intact and cascades to VMVs via m_PropertyValues rcps. | |
| for (auto* vmi : deferredVmiUnrefs) | |
| { | |
| vmi->unref(); | |
| } | |
| deleteDataBinds(); | |
| // Instances reference back to the original artboard's animations and state | |
| // machines, so don't delete them here, they'll get cleaned up when the | |
| // source is deleted. | |
| // TODO: move this logic into ArtboardInstance destructor??? | |
| if (!m_IsInstance) | |
| { | |
| for (auto object : m_Animations) | |
| { | |
| delete object; | |
| } | |
| for (auto object : m_StateMachines) | |
| { | |
| delete object; | |
| } | |
| } | |
| m_dirtyLayout.clear(); | |
| } | |
| static bool canContinue(StatusCode code) | |
| { | |
| // We currently only cease loading on invalid object. | |
| return code != StatusCode::InvalidObject; | |
| } | |
| bool Artboard::validateObjects() | |
| { | |
| auto size = m_Objects.size(); | |
| std::vector<bool> valid(size); | |
| // Max iterations.. | |
| for (int cycle = 0; cycle < 100; cycle++) | |
| { | |
| bool changed = false; | |
| for (size_t i = 1; i < size; i++) | |
| { | |
| auto object = m_Objects[i]; | |
| if (object == nullptr) | |
| { | |
| // objects can be null if they were not understood by this | |
| // runtime. | |
| continue; | |
| } | |
| bool wasValid = valid[i]; | |
| bool isValid = object->validate(this); | |
| if (wasValid != isValid) | |
| { | |
| changed = true; | |
| valid[i] = isValid; | |
| } | |
| } | |
| if (changed) | |
| { | |
| // Delete invalid objects. | |
| for (size_t i = 1; i < size; i++) | |
| { | |
| if (valid[i]) | |
| { | |
| continue; | |
| } | |
| // Instead of immediately deleting invalid objects, we keep them | |
| // around in case other objects are referencing them. One | |
| // example is the backboard_importer keeping a reference on its | |
| // m_FileAssetReferencers. So the invalid objects are taken out | |
| // of the objects list but only deleted when the artboard is | |
| // destroyed. | |
| m_invalidObjects.push_back(m_Objects[i]); | |
| m_Objects[i] = nullptr; | |
| } | |
| } | |
| else | |
| { | |
| break; | |
| } | |
| } | |
| return true; | |
| } | |
| void Artboard::reinstanceNestedArtboards(Factory* factory) | |
| { | |
| for (auto object : m_Objects) | |
| { | |
| if (object == nullptr || !object->is<NestedArtboard>()) | |
| { | |
| continue; | |
| } | |
| auto nested = object->as<NestedArtboard>(); | |
| Artboard* current = nested->sourceArtboard(); | |
| if (current == nullptr || !current->isInstance() || | |
| current->m_artboardSource == nullptr) | |
| { | |
| continue; | |
| } | |
| auto replacement = | |
| current->m_artboardSource->instance<ArtboardInstance>(factory); | |
| if (replacement != nullptr) | |
| { | |
| nested->referencedArtboard(replacement.release()); | |
| } | |
| } | |
| } | |
| StatusCode Artboard::initialize() | |
| { | |
| StatusCode code; | |
| // these will be re-built in update() -- are they needed here? | |
| m_layout = Layout(0.0f, 0.0f, width(), height()); | |
| #ifdef WITH_RIVE_LAYOUT | |
| markLayoutDirty(this); | |
| #endif | |
| // onAddedDirty guarantees that all objects are now available so they can be | |
| // looked up by index/id. This is where nodes find their parents, but they | |
| // can't assume that their parent's parent will have resolved yet. | |
| for (auto object : m_Objects) | |
| { | |
| if (object == nullptr) | |
| { | |
| // objects can be null if they were not understood by this runtime. | |
| continue; | |
| } | |
| if (!canContinue(code = object->onAddedDirty(this))) | |
| { | |
| return code; | |
| } | |
| } | |
| // Animations and StateMachines initialize only once on the source/origin | |
| // Artboard. Instances will hold references to the original Animations and | |
| // StateMachines, so running this code for instances will effectively | |
| // initialize them twice. This can lead to unpredictable behaviour. One such | |
| // example was that resolved objects like listener inputs were being added | |
| // to lists twice. | |
| if (!isInstance()) | |
| { | |
| for (auto object : m_Animations) | |
| { | |
| if (!canContinue(code = object->onAddedDirty(this))) | |
| { | |
| return code; | |
| } | |
| } | |
| for (auto object : m_StateMachines) | |
| { | |
| if (!canContinue(code = object->onAddedDirty(this))) | |
| { | |
| return code; | |
| } | |
| } | |
| if (m_Animations.size() == 0 && m_StateMachines.size() == 0) | |
| { | |
| auto sm = new StateMachine(); | |
| sm->name("Auto Generated State Machine"); | |
| m_StateMachines.push_back(sm); | |
| } | |
| } | |
| // Store a map of the drawRules to make it easier to lookup the matching | |
| // rule for a transform component. | |
| std::unordered_map<Core*, DrawRules*> componentDrawRules; | |
| #ifdef WITH_RIVE_EDITOR | |
| // Editor builds move typed-child registration (paths on shapes, | |
| // nested animations on NestedArtboard, skins, constraints, ...) out | |
| // of onAdded* and into editorParentChanged, driven by the coop | |
| // hydration passes. initialize() only ever runs for .riv-imported | |
| // artboards, which never see those passes — run the registration | |
| // sweep here or the typed lists stay empty (frozen rigs, zero | |
| // bounds, unresolvable nested inputs). Must happen BEFORE the | |
| // onAddedClean walk below: consumers like NestedArtboard mount | |
| // their nested state machines from the typed lists inside | |
| // onAddedClean. Parent pointers and child lists are wired by the | |
| // onAddedDirty pass above. Registrations are contractually | |
| // idempotent on `to`, so the pump's hydrateRuntimeArtboard doing | |
| // this again is harmless. | |
| for (auto object : m_Objects) | |
| { | |
| if (object != nullptr && object->is<Component>()) | |
| { | |
| auto* component = object->as<Component>(); | |
| if (auto* componentParent = component->parent()) | |
| { | |
| component->editorParentChanged(nullptr, componentParent); | |
| } | |
| } | |
| } | |
| #endif | |
| // onAddedClean is called when all individually referenced components have | |
| // been found and so components can look at other components' references and | |
| // assume that they have resolved too. This is where the whole hierarchy is | |
| // linked up and we can traverse it to find other references (my parent's | |
| // parent should be type X can be checked now). | |
| for (auto object : m_Objects) | |
| { | |
| if (object == nullptr) | |
| { | |
| continue; | |
| } | |
| if (!canContinue(code = object->onAddedClean(this))) | |
| { | |
| return code; | |
| } | |
| if (object->is<Component>()) | |
| { | |
| auto resettable = ResettingComponent::from(object->as<Component>()); | |
| if (resettable) | |
| { | |
| m_Resettables.push_back(resettable); | |
| } | |
| } | |
| switch (object->coreType()) | |
| { | |
| case DrawRulesBase::typeKey: | |
| { | |
| DrawRules* rules = static_cast<DrawRules*>(object); | |
| Core* component = resolve(rules->parentId()); | |
| if (component != nullptr) | |
| { | |
| componentDrawRules[component] = rules; | |
| } | |
| else | |
| { | |
| fprintf(stderr, | |
| "Artboard::initialize - Draw rule targets missing " | |
| "component width id %u\n", | |
| static_cast<uint32_t>(rules->parentId())); | |
| } | |
| break; | |
| } | |
| case NestedArtboardBase::typeKey: | |
| case NestedArtboardLeafBase::typeKey: | |
| case NestedArtboardLayoutBase::typeKey: | |
| { | |
| m_NestedArtboards.push_back(object->as<NestedArtboard>()); | |
| m_ArtboardHosts.push_back(object->as<NestedArtboard>()); | |
| break; | |
| } | |
| case ArtboardComponentListBase::typeKey: | |
| m_ComponentLists.push_back(object->as<ArtboardComponentList>()); | |
| m_ArtboardHosts.push_back(object->as<ArtboardComponentList>()); | |
| break; | |
| case JoystickBase::typeKey: | |
| { | |
| Joystick* joystick = object->as<Joystick>(); | |
| if (!joystick->canApplyBeforeUpdate()) | |
| { | |
| m_JoysticksApplyBeforeUpdate = false; | |
| } | |
| joystick->addDependents(this); | |
| m_Joysticks.push_back(joystick); | |
| break; | |
| } | |
| } | |
| auto advancingComponent = AdvancingComponent::from(object); | |
| if (advancingComponent) | |
| { | |
| m_advancingComponents.push_back(advancingComponent); | |
| } | |
| } | |
| if (!isInstance()) | |
| { | |
| for (auto object : m_Animations) | |
| { | |
| if (!canContinue(code = object->onAddedClean(this))) | |
| { | |
| return code; | |
| } | |
| } | |
| for (auto object : m_StateMachines) | |
| { | |
| if (!canContinue(code = object->onAddedClean(this))) | |
| { | |
| return code; | |
| } | |
| } | |
| } | |
| // Multi-level references have been built up, now we can | |
| // actually mark what's dependent on what. | |
| for (auto object : m_Objects) | |
| { | |
| if (object == nullptr) | |
| { | |
| continue; | |
| } | |
| if (object->is<Component>()) | |
| { | |
| object->as<Component>()->buildDependencies(); | |
| } | |
| if (object->is<Drawable>() && object != this) | |
| { | |
| Drawable* drawable = object->as<Drawable>(); | |
| m_Drawables.push_back(drawable); | |
| // Move the foreground drawable before its parent. We traverse the | |
| // added list of drawables and swap their positions with the | |
| // foreground drawable until we find the parent | |
| if (drawable->is<ForegroundLayoutDrawable>()) | |
| { | |
| auto parent = drawable->parent(); | |
| auto index = m_Drawables.size() - 1; | |
| while (index >= 1) | |
| { | |
| auto swappingDrawable = m_Drawables[index - 1]; | |
| std::swap(m_Drawables[index - 1], m_Drawables[index]); | |
| if (swappingDrawable == parent) | |
| { | |
| break; | |
| } | |
| index--; | |
| } | |
| } | |
| for (ContainerComponent* parent = drawable; parent != nullptr; | |
| parent = parent->parent()) | |
| { | |
| auto itr = componentDrawRules.find(parent); | |
| if (itr != componentDrawRules.end()) | |
| { | |
| drawable->flattenedDrawRules = itr->second; | |
| break; | |
| } | |
| } | |
| } | |
| else if (object->is<ClippingShape>()) | |
| { | |
| m_clippingShapes.push_back(object->as<ClippingShape>()); | |
| } | |
| } | |
| // A layout only needs a DrawableProxy if it paints, clips, can gain a clip | |
| // at runtime, or is an interaction/listener hit target. Those deferred | |
| // reasons are stamped as ForceDrawableProxy when their references resolve | |
| // (KeyedObject::onAddedDirty, DataBind::target, ScrollConstraint, and | |
| // StateMachineListener::onAddedClean; source-only ones are carried to | |
| // instances by LayoutComponent::clone) -- guaranteeing the proxy exists | |
| // before this first, one-time injection. See needsDrawableProxy. | |
| // Iterate over the drawables in order to inject proxies for layouts | |
| std::vector<LayoutComponent*> layouts; | |
| for (int i = 0; i < m_Drawables.size(); i++) | |
| { | |
| auto drawable = m_Drawables[i]; | |
| LayoutComponent* currentLayout = nullptr; | |
| bool isInCurrentLayout = true; | |
| if (!layouts.empty()) | |
| { | |
| currentLayout = layouts.back(); | |
| isInCurrentLayout = drawable->isChildOfLayout(currentLayout); | |
| } | |
| // We inject a DrawableProxy after all of the children of a | |
| // LayoutComponent so that we can draw a stroke above and background | |
| // below the children This also allows us to clip the children. Layouts | |
| // that will never paint or clip skip the proxy entirely (it would draw | |
| // nothing) -- pure containers are the common case. | |
| if (currentLayout != nullptr && !isInCurrentLayout) | |
| { | |
| // This is the first item in the list of drawables that isn't a | |
| // child of the layout, so we insert a proxy before it | |
| do | |
| { | |
| if (currentLayout->needsDrawableProxy()) | |
| { | |
| m_Drawables.insert(m_Drawables.begin() + i, | |
| currentLayout->proxy()); | |
| i += 1; | |
| } | |
| layouts.pop_back(); | |
| if (!layouts.empty()) | |
| { | |
| currentLayout = layouts.back(); | |
| } | |
| } while (!layouts.empty() && | |
| !drawable->isChildOfLayout(currentLayout)); | |
| } | |
| if (drawable->is<LayoutComponent>()) | |
| { | |
| layouts.push_back(drawable->as<LayoutComponent>()); | |
| } | |
| } | |
| while (!layouts.empty()) | |
| { | |
| auto layout = layouts.back(); | |
| if (layout->needsDrawableProxy()) | |
| { | |
| m_Drawables.push_back(layout->proxy()); | |
| } | |
| layouts.pop_back(); | |
| } | |
| sortDependencies(); | |
| std::vector<DrawRules*> rulesList; | |
| // Build the rules in the right order. We use the map componentDrawRules | |
| // to make sure we traverse the objects in the right order from parent | |
| // to child, and add the rules accordingly. | |
| for (auto object : m_Objects) | |
| { | |
| if (object == nullptr) | |
| { | |
| continue; | |
| } | |
| auto itr = componentDrawRules.find(object); | |
| if (itr != componentDrawRules.end()) | |
| { | |
| rulesList.emplace_back(componentDrawRules[object]); | |
| } | |
| } | |
| DrawTarget root; | |
| // Build up the draw order. Look for draw targets and build | |
| // their dependencies. | |
| for (auto rules : rulesList) | |
| { | |
| for (auto child : rules->children()) | |
| { | |
| auto target = child->as<DrawTarget>(); | |
| root.addDependent(target); | |
| auto dependentRules = target->drawable()->flattenedDrawRules; | |
| if (dependentRules != nullptr) | |
| { | |
| // Because we don't store targets on rules, we need | |
| // to find the targets that belong to this rule | |
| // here. | |
| for (auto object : m_Objects) | |
| { | |
| if (object != nullptr && object->is<DrawTarget>()) | |
| { | |
| DrawTarget* dependentTarget = object->as<DrawTarget>(); | |
| if (dependentTarget->parent() == dependentRules) | |
| { | |
| dependentTarget->addDependent(target); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| DependencySorter sorter; | |
| std::vector<Component*> drawTargetOrder; | |
| sorter.sort(&root, drawTargetOrder); | |
| if (drawTargetOrder.size() > 0) | |
| { | |
| auto itr = drawTargetOrder.begin(); | |
| itr++; | |
| while (itr != drawTargetOrder.end()) | |
| { | |
| m_DrawTargets.push_back(static_cast<DrawTarget*>(*itr++)); | |
| } | |
| } | |
| initScriptedObjects(); | |
| return StatusCode::Ok; | |
| } | |
| void Artboard::sortDrawOrder() | |
| { | |
| m_drawOrderChangeCounter = | |
| m_drawOrderChangeCounter == std::numeric_limits<uint8_t>::max() | |
| ? 0 | |
| : m_drawOrderChangeCounter + 1; | |
| for (auto target : m_DrawTargets) | |
| { | |
| target->first = target->last = nullptr; | |
| } | |
| m_FirstDrawable = nullptr; | |
| Drawable* lastDrawable = nullptr; | |
| for (auto drawable : m_Drawables) | |
| { | |
| auto rules = drawable->flattenedDrawRules; | |
| if (rules != nullptr && rules->activeTarget() != nullptr) | |
| { | |
| auto target = rules->activeTarget(); | |
| if (target->first == nullptr) | |
| { | |
| target->first = target->last = drawable; | |
| drawable->prev = drawable->next = nullptr; | |
| } | |
| else | |
| { | |
| target->last->next = drawable; | |
| drawable->prev = target->last; | |
| target->last = drawable; | |
| drawable->next = nullptr; | |
| } | |
| } | |
| else | |
| { | |
| drawable->prev = lastDrawable; | |
| drawable->next = nullptr; | |
| if (lastDrawable == nullptr) | |
| { | |
| lastDrawable = m_FirstDrawable = drawable; | |
| } | |
| else | |
| { | |
| lastDrawable->next = drawable; | |
| lastDrawable = drawable; | |
| } | |
| } | |
| } | |
| for (auto rule : m_DrawTargets) | |
| { | |
| if (rule->first == nullptr) | |
| { | |
| continue; | |
| } | |
| auto targetDrawable = rule->drawable(); | |
| switch (rule->placement()) | |
| { | |
| case DrawTargetPlacement::before: | |
| { | |
| if (targetDrawable->prev != nullptr) | |
| { | |
| targetDrawable->prev->next = rule->first; | |
| rule->first->prev = targetDrawable->prev; | |
| } | |
| if (targetDrawable == m_FirstDrawable) | |
| { | |
| m_FirstDrawable = rule->first; | |
| } | |
| targetDrawable->prev = rule->last; | |
| rule->last->next = targetDrawable; | |
| break; | |
| } | |
| case DrawTargetPlacement::after: | |
| { | |
| if (targetDrawable->next != nullptr) | |
| { | |
| targetDrawable->next->prev = rule->last; | |
| rule->last->next = targetDrawable->next; | |
| } | |
| if (targetDrawable == lastDrawable) | |
| { | |
| lastDrawable = rule->last; | |
| } | |
| targetDrawable->next = rule->first; | |
| rule->first->prev = targetDrawable; | |
| break; | |
| } | |
| } | |
| } | |
| m_FirstDrawable = lastDrawable; | |
| // Interleave clipping operations between drawables that share the same | |
| // common clippings. Each clipping operation has a start and an end. | |
| for (auto& clippingShape : m_clippingShapes) | |
| { | |
| clippingShape->resetDrawables(); | |
| } | |
| Drawable* currentDrawable = m_FirstDrawable; | |
| Drawable* nextDrawable = nullptr; | |
| std::vector<ClippingShape*> _clippingStack; | |
| while (currentDrawable) | |
| { | |
| currentDrawable->needsSaveOperation(true); | |
| auto drawableClippingShapes = currentDrawable->clippingShapes(); | |
| // Remove all clippings that are not part of the current drawable. Since | |
| // they are applied as a stack, if one clipping is removed, all | |
| // subsequent clippings from the stack need to be removed as well | |
| size_t removingIndex = _clippingStack.size(); | |
| for (size_t i = 0; i < _clippingStack.size(); ++i) | |
| { | |
| auto& cl = _clippingStack[i]; | |
| // Check if this clipping should stay (is in both stack and | |
| // drawable's clippings) | |
| bool shouldStay = std::find(drawableClippingShapes.begin(), | |
| drawableClippingShapes.end(), | |
| cl) != drawableClippingShapes.end(); | |
| if (!shouldStay) | |
| { | |
| removingIndex = i; | |
| break; | |
| } | |
| } | |
| // Remove all clippings from stack in the reverse order they were added | |
| if (_clippingStack.size() > 0 && removingIndex < _clippingStack.size()) | |
| { | |
| size_t i = _clippingStack.size() - 1; | |
| while (i >= removingIndex) | |
| { | |
| auto& clippingShape = _clippingStack[i]; | |
| // Insert in the drawing list a clipEnd for each clipping that | |
| // is removed | |
| auto proxyDrawable = | |
| clippingShape->createProxyDrawable(&clippingShape->clipEnd); | |
| if (nextDrawable) | |
| { | |
| proxyDrawable->next = nextDrawable; | |
| nextDrawable->prev = proxyDrawable; | |
| } | |
| else | |
| { | |
| fprintf(stderr, | |
| "Error - adding clip end as first operation\n"); | |
| } | |
| proxyDrawable->prev = currentDrawable; | |
| currentDrawable->next = proxyDrawable; | |
| nextDrawable = proxyDrawable; | |
| if (i == 0) | |
| { | |
| break; | |
| } | |
| i--; | |
| } | |
| _clippingStack.erase(_clippingStack.begin() + removingIndex, | |
| _clippingStack.end()); | |
| } | |
| // Find clippings that are applied to the drawable but are not on the | |
| // stack | |
| for (auto& clippingShape : drawableClippingShapes) | |
| { | |
| auto itr = std::find(_clippingStack.begin(), | |
| _clippingStack.end(), | |
| clippingShape); | |
| if (itr == _clippingStack.end()) | |
| { | |
| auto proxyDrawable = clippingShape->createProxyDrawable( | |
| &clippingShape->clipStart); | |
| if (nextDrawable) | |
| { | |
| proxyDrawable->next = nextDrawable; | |
| nextDrawable->prev = proxyDrawable; | |
| } | |
| else | |
| { | |
| m_FirstDrawable = proxyDrawable; | |
| } | |
| proxyDrawable->prev = currentDrawable; | |
| currentDrawable->next = proxyDrawable; | |
| nextDrawable = proxyDrawable; | |
| _clippingStack.push_back(clippingShape); | |
| } | |
| } | |
| nextDrawable = currentDrawable; | |
| currentDrawable = currentDrawable->prev; | |
| } | |
| // Add closing calls to remaining clippings in the stack | |
| if (_clippingStack.size() > 0) | |
| { | |
| for (int i = (int)(_clippingStack.size() - 1); i >= 0; i--) | |
| { | |
| auto& clippingShape = _clippingStack[i]; | |
| auto proxyDrawable = | |
| clippingShape->createProxyDrawable(&clippingShape->clipEnd); | |
| if (nextDrawable) | |
| { | |
| nextDrawable->prev = proxyDrawable; | |
| proxyDrawable->next = nextDrawable; | |
| } | |
| proxyDrawable->prev = nullptr; // End of list | |
| nextDrawable = proxyDrawable; | |
| } | |
| } | |
| clearRedundantOperations(); | |
| } | |
| // Look for drawables that are preceeding and succeeding drawables that call | |
| // save and restore. If found, the drawable does not need to call save and | |
| // restore itself. | |
| void Artboard::clearRedundantOperations() | |
| { | |
| Drawable* currentDrawable = m_FirstDrawable; | |
| bool prevAppliedSave = false; | |
| // Keep a stack of clipStart operation results to apply the same operation | |
| // to its clipEnd | |
| std::vector<bool> appliedClippingSaveOperations; | |
| while (currentDrawable) | |
| { | |
| currentDrawable->needsSaveOperation(true); | |
| // If previous operation applied a save operation | |
| if (prevAppliedSave) | |
| { | |
| // With consecutive clippings, we can skip the save and restore | |
| // operation since the previous one has applied it | |
| if (currentDrawable->isClipStart()) | |
| { | |
| appliedClippingSaveOperations.push_back(false); | |
| currentDrawable->needsSaveOperation(false); | |
| } | |
| else if (currentDrawable->isClipEnd()) | |
| { | |
| // Apply or skip the clipEnd Restore operation matching its clip | |
| // start counterpart | |
| auto operationApplied = appliedClippingSaveOperations.back(); | |
| appliedClippingSaveOperations.pop_back(); | |
| currentDrawable->needsSaveOperation(operationApplied); | |
| } | |
| else | |
| { | |
| // Check if next is clip end, if it is, we can skip the drawable | |
| // save/restore because it is tightly wrapped in a clipping | |
| // operation | |
| auto nextDrawable = currentDrawable->prev; | |
| if (nextDrawable->isClipEnd()) | |
| { | |
| currentDrawable->needsSaveOperation(false); | |
| } | |
| } | |
| } | |
| else if (currentDrawable->isClipStart()) | |
| { | |
| appliedClippingSaveOperations.push_back(true); | |
| } | |
| else if (currentDrawable->isClipEnd()) | |
| { | |
| // Apply or skip the clipEnd Restore operation matching its clip | |
| // start counterpart | |
| auto operationApplied = appliedClippingSaveOperations.back(); | |
| currentDrawable->needsSaveOperation(operationApplied); | |
| appliedClippingSaveOperations.pop_back(); | |
| } | |
| prevAppliedSave = currentDrawable->isClipStart() && | |
| (currentDrawable->willClip() || prevAppliedSave); | |
| currentDrawable = currentDrawable->prev; | |
| } | |
| assert(appliedClippingSaveOperations.size() == 0); | |
| } | |
| #ifdef WITH_RIVE_EDITOR | |
| void Artboard::initLayoutForEditor() | |
| { | |
| m_layout = Layout(0.0f, 0.0f, width(), height()); | |
| // NOTE: intentionally skipping `markLayoutDirty(this)` — the full | |
| // layout pipeline isn't wired for coop-loaded artboards yet (no | |
| // style / Yoga node setup), and running syncStyleChanges on a | |
| // partial state has been observed to crash. Phase 2.1 adds the | |
| // layout pipeline back behind the `#ifdef WITH_RIVE_LAYOUT` once | |
| // the missing setup is in place. | |
| } | |
| #endif | |
| void Artboard::sortDependencies() | |
| { | |
| DependencySorter sorter; | |
| sorter.sort(this, m_DependencyOrder); | |
| unsigned int graphOrder = 0; | |
| for (auto component : m_DependencyOrder) | |
| { | |
| component->m_GraphOrder = graphOrder++; | |
| } | |
| m_Dirt |= ComponentDirt::Components; | |
| } | |
| void Artboard::addObject(Core* object) { m_Objects.push_back(object); } | |
| void Artboard::addAnimation(LinearAnimation* object) | |
| { | |
| m_Animations.push_back(object); | |
| } | |
| void Artboard::addStateMachine(StateMachine* object) | |
| { | |
| m_StateMachines.push_back(object); | |
| } | |
| void Artboard::addScriptedObject(ScriptedObject* object) | |
| { | |
| m_ScriptedObjects.push_back(object); | |
| } | |
| void Artboard::initScriptedObjects() | |
| { | |
| if (isInstance()) | |
| { | |
| for (auto obj : m_ScriptedObjects) | |
| { | |
| if (obj->scriptAsset() != nullptr) | |
| { | |
| if (!obj->userLuaInitDone()) | |
| { | |
| obj->scriptAsset()->initScriptedObject(obj); | |
| } | |
| obj->hydrateScriptInputs(); | |
| } | |
| } | |
| } | |
| } | |
| void Artboard::pollAsyncWork() { rive_pollAsyncWork(); } | |
| void Artboard::advanceScriptedViewModels() | |
| { | |
| #ifdef WITH_RIVE_SCRIPTING_LUAU | |
| if (m_scriptingVM != nullptr) | |
| { | |
| if (auto* context = m_scriptingVM->context()) | |
| { | |
| context->advanceDetachedViewModels(); | |
| } | |
| } | |
| #endif | |
| #ifdef WITH_RIVE_SCRIPTING_WASM | |
| if (auto f = artboardFile()) | |
| { | |
| for (auto& vm : f->wasmVMs()) | |
| { | |
| vm->advanceDetachedViewModels(); | |
| } | |
| } | |
| #endif | |
| } | |
| Core* Artboard::resolve(Id id) const | |
| { | |
| #ifdef WITH_RIVE_EDITOR | |
| // Editor `Id` is `{client, object}` and objects come from two | |
| // sources that share the same namespace: | |
| // • `.riv`-loaded objects — `CoreIdType::runtimeDeserialize` | |
| // synthesizes `Id{0, runtime_index}` which indexes straight | |
| // into `m_Objects` (same behavior as a runtime build). | |
| // • Coop-delivered objects — stamped by the server with | |
| // `client == 0` too, looked up via `m_editorResolver`'s | |
| // CoopId map. Non-zero client always means a coop peer ID. | |
| // `m_Objects` first (runtime path) then fall through to the | |
| // resolver, so both sources resolve correctly without the caller | |
| // needing to know which. `kEmptyId == {0, 0}` is short-circuited | |
| // — `m_Objects[0]` is a legitimate object (the Artboard itself) | |
| // and would be a wrong answer for "no id set". | |
| if (id.empty()) | |
| return nullptr; | |
| if (id.client == 0 && id.object < m_Objects.size()) | |
| { | |
| if (auto* hit = m_Objects[id.object]) | |
| return hit; | |
| } | |
| return m_editorResolver ? m_editorResolver->resolve(id) : nullptr; | |
| #else | |
| // Runtime: single-source `Id` (a raw uint index) into `m_Objects`. | |
| return id < static_cast<int>(m_Objects.size()) ? m_Objects[id] : nullptr; | |
| #endif | |
| } | |
| uint32_t Artboard::idOf(Core* object) const | |
| { | |
| auto it = std::find(m_Objects.begin(), m_Objects.end(), object); | |
| if (it != m_Objects.end()) | |
| { | |
| return castTo<uint32_t>(it - m_Objects.begin()); | |
| } | |
| else | |
| { | |
| return 0; | |
| } | |
| } | |
| void Artboard::onComponentDirty(Component* component) | |
| { | |
| m_didChange = true; | |
| m_Dirt |= ComponentDirt::Components; | |
| /// If the order of the component is less than the current dirt | |
| /// depth, update the dirt depth so that the update loop can break | |
| /// out early and re-run (something up the tree is dirty). | |
| if (component->graphOrder() < m_DirtDepth) | |
| { | |
| m_DirtDepth = component->graphOrder(); | |
| } | |
| } | |
| void Artboard::onDirty(ComponentDirt dirt) | |
| { | |
| m_Dirt |= ComponentDirt::Components; | |
| } | |
| #ifdef WITH_RIVE_LAYOUT | |
| void Artboard::propagateSize() | |
| { | |
| addDirt(ComponentDirt::Path); | |
| if (sharesLayoutWithHost()) | |
| { | |
| m_host->markHostTransformDirty(); | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| if (m_layoutChangedCallback != nullptr) | |
| { | |
| m_layoutChangedCallback(callbackUserData); | |
| } | |
| #endif | |
| } | |
| #endif | |
| bool Artboard::sharesLayoutWithHost() const | |
| { | |
| return m_host != nullptr && m_host->isLayoutProvider(); | |
| } | |
| void Artboard::cloneObjectDataBinds(const Core* object, | |
| Core* clone, | |
| Artboard* artboard) const | |
| { | |
| for (auto dataBind : dataBinds()) | |
| { | |
| if (dataBind->target() == object) | |
| { | |
| auto dataBindClone = static_cast<DataBind*>(dataBind->clone()); | |
| dataBindClone->target(clone); | |
| dataBindClone->file(dataBind->file()); | |
| dataBindClone->initialize(); | |
| if (dataBind->converter() != nullptr) | |
| { | |
| dataBindClone->converter( | |
| dataBind->converter()->clone()->as<DataConverter>()); | |
| } | |
| artboard->addDataBind(dataBindClone); | |
| } | |
| } | |
| } | |
| void Artboard::buildKeyFrameSourceBindsIndex() const | |
| { | |
| m_keyFrameSourceBindsBuilt = true; | |
| for (auto dataBind : dataBinds()) | |
| { | |
| auto target = dataBind->target(); | |
| if (target == nullptr || !target->is<KeyFrame>()) | |
| { | |
| continue; | |
| } | |
| // A keyframe holds a single value, so keep the first bind per target | |
| // (emplace does not overwrite an existing key). | |
| m_keyFrameSourceBinds.emplace(target->as<KeyFrame>(), dataBind); | |
| } | |
| } | |
| bool Artboard::hasKeyFrameSourceBinds() const | |
| { | |
| if (!m_keyFrameSourceBindsBuilt) | |
| { | |
| buildKeyFrameSourceBindsIndex(); | |
| } | |
| return !m_keyFrameSourceBinds.empty(); | |
| } | |
| DataBind* Artboard::keyFrameSourceBind(const KeyFrame* keyframe) const | |
| { | |
| if (!m_keyFrameSourceBindsBuilt) | |
| { | |
| buildKeyFrameSourceBindsIndex(); | |
| } | |
| auto it = m_keyFrameSourceBinds.find(keyframe); | |
| return it != m_keyFrameSourceBinds.end() ? it->second : nullptr; | |
| } | |
| void Artboard::host(ArtboardHost* artboardHost) | |
| { | |
| addedToHost(); | |
| m_host = artboardHost; | |
| #ifdef WITH_RIVE_LAYOUT | |
| if (!sharesLayoutWithHost()) | |
| { | |
| return; | |
| } | |
| Artboard* parent = parentArtboard(); | |
| if (parent != nullptr) | |
| { | |
| parent->markLayoutDirty(this); | |
| parent->syncLayoutChildren(); | |
| } | |
| #endif | |
| } | |
| ArtboardHost* Artboard::host() const { return m_host; } | |
| StatusCode Artboard::onAddedClean(CoreContext* context) | |
| { | |
| auto code = Super::onAddedClean(context); | |
| if (code != StatusCode::Ok) | |
| { | |
| return code; | |
| } | |
| NodeBase::x(0); | |
| NodeBase::y(0); | |
| return StatusCode::Ok; | |
| } | |
| Artboard* Artboard::parentArtboard() const | |
| { | |
| if (m_host == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| return m_host->parentArtboard(); | |
| } | |
| float Artboard::layoutWidth() const | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| return m_layout.width(); | |
| #else | |
| return width(); | |
| #endif | |
| } | |
| float Artboard::layoutHeight() const | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| return m_layout.height(); | |
| #else | |
| return height(); | |
| #endif | |
| } | |
| float Artboard::layoutX() const | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| return m_layout.left(); | |
| #else | |
| return 0.0f; | |
| #endif | |
| } | |
| float Artboard::layoutY() const | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| return m_layout.top(); | |
| #else | |
| return 0.0f; | |
| #endif | |
| } | |
| void Artboard::updateRenderPath() | |
| { | |
| AABB bg = AABB::fromLTWH(-layoutWidth() * originX(), | |
| -layoutHeight() * originY(), | |
| layoutWidth(), | |
| layoutHeight()); | |
| AABB clip; | |
| if (m_FrameOrigin) | |
| { | |
| clip = {0.0f, 0.0f, layoutWidth(), layoutHeight()}; | |
| } | |
| else | |
| { | |
| clip = bg; | |
| } | |
| auto& renderPaths = mutableRenderPaths(); | |
| renderPaths.local.rewind(); | |
| renderPaths.local.addRect(bg); | |
| renderPaths.world.rewind(); | |
| renderPaths.world.addRect(clip); | |
| } | |
| void Artboard::update(ComponentDirt value) | |
| { | |
| Super::update(value); | |
| if (hasDirt(value, ComponentDirt::DrawOrder)) | |
| { | |
| sortDrawOrder(); | |
| } | |
| if (hasDirt(value, ComponentDirt::Clipping)) | |
| { | |
| clearRedundantOperations(); | |
| } | |
| #ifdef WITH_RIVE_LAYOUT | |
| if (hasDirt(value, ComponentDirt::LayoutStyle)) | |
| { | |
| bool cascadeChanged = cascadeLayoutStyle(interpolation(), | |
| interpolator(), | |
| interpolationTime(), | |
| actualDirection()); | |
| // TODO: Explore whether we can remove the syncStyleChanges call in | |
| // updatePass. Since updatePass calls updateComponents, where the first | |
| // component is the artboard itself, hence calling update, we end up | |
| // calling this twice. Although it is safe, because syncStyleChanges | |
| // checks for the list of dirty layouts that would be empty at this | |
| // point, it seems redundant. | |
| syncStyleChangesWithUpdate(cascadeChanged); | |
| } | |
| #endif | |
| m_hostTransformMarkedDirty = false; | |
| } | |
| void Artboard::addDirtyDataBind(DataBind* dataBind) | |
| { | |
| // Most artboard data binds target Components and need the component graph | |
| // marked dirty. Keyframe value binds instead target transient | |
| // BindableProperty holders (see | |
| // LinearAnimationInstance::keyFrameValueHolder) that aren't in the | |
| // component graph — they're read directly during animation apply — so only | |
| // propagate component dirt for Component targets. | |
| auto target = dataBind->target(); | |
| if (target != nullptr && target->is<Component>()) | |
| { | |
| onComponentDirty(target->as<Component>()); | |
| } | |
| DataBindContainer::addDirtyDataBind(dataBind); | |
| } | |
| void Artboard::updateDataBinds(bool applyTargetToSource) | |
| { | |
| for (auto artboardHost : m_ArtboardHosts) | |
| { | |
| artboardHost->updateDataBinds(); | |
| } | |
| DataBindContainer::updateDataBinds(applyTargetToSource); | |
| } | |
| bool Artboard::updateComponents() | |
| { | |
| if (!hasDirt(ComponentDirt::Components)) | |
| { | |
| return false; | |
| } | |
| const int maxSteps = 100; | |
| int step = 0; | |
| auto count = m_DependencyOrder.size(); | |
| while (hasDirt(ComponentDirt::Components) && step < maxSteps) | |
| { | |
| m_Dirt = m_Dirt & ~ComponentDirt::Components; | |
| // Track dirt depth here so that if something else marks | |
| // dirty, we restart. | |
| for (unsigned int i = 0; i < count; i++) | |
| { | |
| auto component = m_DependencyOrder[i]; | |
| m_DirtDepth = i; | |
| auto d = component->m_Dirt; | |
| if (d == ComponentDirt::None || | |
| (d & ComponentDirt::Collapsed) == ComponentDirt::Collapsed) | |
| { | |
| continue; | |
| } | |
| component->m_Dirt = ComponentDirt::None; | |
| component->update(d); | |
| // If the update changed the dirt depth by adding dirt | |
| // to something before us (in the DAG), early out and | |
| // re-run the update. | |
| if (m_DirtDepth < i) | |
| { | |
| break; | |
| } | |
| } | |
| step++; | |
| } | |
| return true; | |
| } | |
| LayoutData* Artboard::takeLayoutData() | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| m_updatesOwnLayout = false; | |
| return m_layoutData; | |
| #else | |
| return nullptr; | |
| #endif | |
| } | |
| void Artboard::cleanLayout(LayoutComponent* layoutComponent) | |
| { | |
| assert(!m_isCleaningDirtyLayouts); | |
| if (m_isCleaningDirtyLayouts) | |
| { | |
| fprintf(stderr, | |
| "Artboard::cleanLayout - trying to remove a dirty layout " | |
| "during clean pass!\n"); | |
| return; | |
| } | |
| if (!m_dirtyLayout.empty()) | |
| { | |
| auto itr = m_dirtyLayout.find(layoutComponent); | |
| if (itr != m_dirtyLayout.end()) | |
| { | |
| m_dirtyLayout.erase(itr); | |
| } | |
| } | |
| // If we called cleanLayout on ourselves, make sure to also | |
| // call it on our parent artboard in case we were dirtied | |
| if (layoutComponent == this) | |
| { | |
| Artboard* parent = parentArtboard(); | |
| if (parent != nullptr) | |
| { | |
| parent->cleanLayout(layoutComponent); | |
| } | |
| } | |
| } | |
| void Artboard::markLayoutDirty(LayoutComponent* layoutComponent) | |
| { | |
| assert(!m_isCleaningDirtyLayouts); | |
| if (m_isCleaningDirtyLayouts) | |
| { | |
| fprintf(stderr, | |
| "Artboard::markLayoutDirty - trying to mark a layout dirty " | |
| "during clean pass!\n"); | |
| return; | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| if (m_dirtyLayout.empty() && m_layoutDirtyCallback != nullptr) | |
| { | |
| m_layoutDirtyCallback(callbackUserData); | |
| } | |
| #endif | |
| m_dirtyLayout.insert(layoutComponent); | |
| if (isInstance()) | |
| { | |
| if (sharesLayoutWithHost()) | |
| { | |
| m_host->markHostingLayoutDirty(this->as<ArtboardInstance>()); | |
| } | |
| else | |
| { | |
| markHostTransformDirty(); | |
| } | |
| } | |
| addDirt(ComponentDirt::Components); | |
| } | |
| void Artboard::markHostTransformDirty() | |
| { | |
| #ifdef WITH_RIVE_TOOLS | |
| if (!m_hostTransformMarkedDirty && m_transformDirtyCallback != nullptr) | |
| { | |
| m_transformDirtyCallback(callbackUserData); | |
| } | |
| #endif | |
| m_hostTransformMarkedDirty = true; | |
| if (host()) | |
| { | |
| host()->markHostTransformDirty(); | |
| } | |
| } | |
| void Artboard::syncStyleChangesWithUpdate(bool forceUpdate) | |
| { | |
| #ifdef WITH_RIVE_LAYOUT | |
| if (syncStyleChanges() && (m_updatesOwnLayout || forceUpdate)) | |
| { | |
| calculateLayout(); | |
| updateLayoutBounds(/*animation*/ true); // maybe use a static to allow | |
| // the editor to set this. | |
| } | |
| #endif | |
| } | |
| bool Artboard::syncStyleChanges() | |
| { | |
| bool updated = false; | |
| m_isCleaningDirtyLayouts = true; | |
| #ifdef WITH_RIVE_LAYOUT | |
| if (!m_dirtyLayout.empty()) | |
| { | |
| for (auto layout : m_dirtyLayout) | |
| { | |
| if (layout == nullptr) | |
| { | |
| continue; | |
| } | |
| switch (layout->coreType()) | |
| { | |
| case ArtboardBase::typeKey: | |
| { | |
| auto artboard = layout->as<Artboard>(); | |
| if (artboard == this) | |
| { | |
| artboard->syncStyle(); | |
| } | |
| else | |
| { | |
| // This is a nested artboard, sync its changes too. | |
| if (!artboard->updatesOwnLayout()) | |
| { | |
| artboard->syncStyleChanges(); | |
| } | |
| } | |
| break; | |
| } | |
| default: | |
| layout->syncStyle(); | |
| break; | |
| } | |
| } | |
| m_dirtyLayout.clear(); | |
| updated = true; | |
| } | |
| #endif | |
| m_isCleaningDirtyLayouts = false; | |
| return updated; | |
| } | |
| void Artboard::calculateLayout() | |
| { | |
| // Always pass NAN and let calculateLayoutInternal decide whether to use | |
| // the intrinsic (hug) size or fall back to width()/height(). | |
| // | |
| // This covers all cases: | |
| // - Runtime: | |
| // - Top level artboards: intrinsically sized artboards get NAN so Yoga | |
| // computes from children; fixed-size artboards fall back to | |
| // width()/height() inside calculateLayoutInternal. | |
| // - Nested node/leaf artboards (m_updatesOwnLayout == true): same as | |
| // top-level, calculateLayoutInternal handles the distinction. | |
| // - Nested layout-mode artboards (NestedArtboardLayout): their layout | |
| // node is owned by the parent via takeLayoutData(), which sets | |
| // m_updatesOwnLayout = false. syncStyleChangesWithUpdate() gates on | |
| // that flag, so calculateLayout() is not reached for these. | |
| // - Editor: | |
| // - Top level artboards: are handled on the Dart side so don't take | |
| // this code path | |
| // - Nested node/leaf artboards (m_updatesOwnLayout == true): | |
| // same as runtime, calculateLayoutInternal handles the distinction. | |
| // - Nested layout-mode artboards (NestedArtboardLayout): same | |
| // as runtime, their layout node is owned by the Dart parent | |
| // which sets m_updatesOwnLayout = false | |
| calculateLayoutInternal(NAN, NAN); | |
| } | |
| bool Artboard::updatePass(bool isRoot) | |
| { | |
| RIVE_PROF_SCOPE() | |
| updateDataBinds(); | |
| bool didUpdate = false; | |
| syncStyleChangesWithUpdate(); | |
| m_hostTransformMarkedDirty = false; | |
| if (m_JoysticksApplyBeforeUpdate) | |
| { | |
| for (auto joystick : m_Joysticks) | |
| { | |
| joystick->apply(this); | |
| } | |
| } | |
| if (updateComponents()) | |
| { | |
| didUpdate = true; | |
| } | |
| if (!m_JoysticksApplyBeforeUpdate) | |
| { | |
| for (auto joystick : m_Joysticks) | |
| { | |
| if (!joystick->canApplyBeforeUpdate()) | |
| { | |
| updateDataBinds(); | |
| if (updateComponents()) | |
| { | |
| didUpdate = true; | |
| } | |
| } | |
| joystick->apply(this); | |
| } | |
| updateDataBinds(); | |
| if (updateComponents()) | |
| { | |
| didUpdate = true; | |
| } | |
| } | |
| if (didUpdate) | |
| { | |
| updateDataBinds(); | |
| } | |
| return didUpdate; | |
| } | |
| bool Artboard::advanceInternal(float elapsedSeconds, AdvanceFlags flags) | |
| { | |
| RIVE_PROF_SCOPE() | |
| bool didUpdate = false; | |
| for (auto adv : m_advancingComponents) | |
| { | |
| if (adv->advanceComponent(elapsedSeconds, flags)) | |
| { | |
| didUpdate = true; | |
| } | |
| } | |
| if (advanceDataBinds(elapsedSeconds)) | |
| { | |
| didUpdate = true; | |
| } | |
| return didUpdate; | |
| } | |
| void Artboard::reset() | |
| { | |
| if (m_Resettables.size() == 0) | |
| { | |
| return; | |
| } | |
| for (auto obj : m_Resettables) | |
| { | |
| obj->reset(); | |
| } | |
| } | |
| bool Artboard::advance(float elapsedSeconds, AdvanceFlags flags) | |
| { | |
| // Poll async work (image decodes, etc.) so promises resolve before | |
| // script advance() callbacks run. | |
| pollAsyncWork(); | |
| AdvanceFlags advancingFlags = flags; | |
| advancingFlags |= AdvanceFlags::IsRoot; | |
| bool didUpdate = advanceInternal(elapsedSeconds, advancingFlags); | |
| if (updatePass(true)) | |
| { | |
| didUpdate = true; | |
| } | |
| return didUpdate || hasDirt(ComponentDirt::Components); | |
| } | |
| Core* Artboard::hitTest(HitInfo* hinfo, const Mat2D& xform) | |
| { | |
| if (clip()) | |
| { | |
| // TODO: can we get the rawpath for the clip? | |
| } | |
| auto mx = xform; | |
| if (m_FrameOrigin) | |
| { | |
| mx *= Mat2D::fromTranslate(layoutWidth() * originX(), | |
| layoutHeight() * originY()); | |
| } | |
| // Mirror drawInternal's own rotation/scale so hit-testing matches what is | |
| // drawn. This single spot also covers nested instances, since | |
| // NestedArtboard::hitTest re-enters Artboard::hitTest. | |
| if (hasSelfTransform()) | |
| { | |
| mx *= selfTransform(); | |
| } | |
| Drawable* last = m_FirstDrawable; | |
| if (last) | |
| { | |
| // walk to the end, so we can visit in reverse-order | |
| while (last->prev) | |
| { | |
| last = last->prev; | |
| } | |
| } | |
| for (auto drawable = last; drawable; drawable = drawable->next) | |
| { | |
| if (drawable->isHidden()) | |
| { | |
| continue; | |
| } | |
| if (auto c = drawable->hitTest(hinfo, mx)) | |
| { | |
| return c; | |
| } | |
| } | |
| // TODO: should we hit-test the background? | |
| return nullptr; | |
| } | |
| Vec2D Artboard::rootTransform(const Vec2D& point) | |
| { | |
| // When this artboard is nested, its own rotation/scale (applied about the | |
| // origin at draw time) is part of how its contents land in the parent, so | |
| // fold it in before mapping through the host. Top-level artboards are the | |
| // root coordinate space, so their own transform is not applied here. | |
| if (host()) | |
| { | |
| auto local = hasSelfTransform() ? selfTransform() * point : point; | |
| return host()->hostTransformPoint(local, this->as<ArtboardInstance>()); | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| // Editor artboards don't have a host, so we expose a function that calls | |
| // the host in dart. The callback is only wired up for mounted (nested) | |
| // instances, so applying the self transform here matches the host() path. | |
| if (m_rootTransformCallback != nullptr) | |
| { | |
| auto local = hasSelfTransform() ? selfTransform() * point : point; | |
| auto x = | |
| m_rootTransformCallback(callbackUserData, local.x, local.y, true); | |
| auto y = | |
| m_rootTransformCallback(callbackUserData, local.x, local.y, false); | |
| return Vec2D(x, y); | |
| } | |
| #endif | |
| return point; | |
| } | |
| bool Artboard::hitTestPoint(const Vec2D& position, | |
| bool skipOnUnclipped, | |
| bool isPrimaryHit) | |
| { | |
| if (host() != nullptr && isInstance()) | |
| { | |
| if (!host()->hitTestHost(position, | |
| skipOnUnclipped, | |
| this->as<ArtboardInstance>())) | |
| { | |
| return false; | |
| } | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| // Editor artboards don't have a host, so we expose a function that calls | |
| // the host in dart. | |
| if (m_testBoundsCallback != nullptr) | |
| { | |
| // Dart can't return booleans to cpp, so we use a uint_8 instead | |
| auto didHit = m_testBoundsCallback(callbackUserData, | |
| position.x, | |
| position.y, | |
| skipOnUnclipped); | |
| if (didHit == 0) | |
| { | |
| return false; | |
| } | |
| } | |
| #endif | |
| return LayoutComponent::hitTestPoint(position, | |
| skipOnUnclipped, | |
| isPrimaryHit); | |
| } | |
| void Artboard::draw(Renderer* renderer) | |
| { | |
| sm_frameId++; | |
| drawInternal(renderer); | |
| } | |
| void Artboard::drawInternal(Renderer* renderer) | |
| { | |
| RIVE_PROF_SCOPE_L(1) | |
| m_didChange = false; | |
| if (childOpacity() == 0) | |
| { | |
| return; | |
| } | |
| bool hasSelf = hasSelfTransform(); | |
| bool save = clip() || m_FrameOrigin || hasSelf; | |
| if (save) | |
| { | |
| renderer->save(); | |
| } | |
| if (m_FrameOrigin) | |
| { | |
| Mat2D artboardTransform; | |
| artboardTransform[4] = layoutWidth() * originX(); | |
| artboardTransform[5] = layoutHeight() * originY(); | |
| renderer->transform(artboardTransform); | |
| } | |
| // Apply the artboard's own rotation/scale, pivoted around its origin. | |
| // Content-local (0,0) is the origin anchor, so this is simply rotation * | |
| // scale with no extra pivot translation. Applied after the frame-origin | |
| // translation so the anchor stays put, and it covers both top-level and | |
| // nested (mounted) rendering since both funnel through drawInternal. | |
| // Hit-testing mirrors this via selfTransform() so interaction matches. | |
| if (hasSelf) | |
| { | |
| renderer->transform(selfTransform()); | |
| } | |
| // Clip after the frame-origin and self transforms so the clip region tracks | |
| // the (possibly rotated/scaled) artboard. The local path is the | |
| // content-local bounds, so it is transformed along with the content. | |
| if (clip()) | |
| { | |
| renderer->clipPath(mutableRenderPaths().local.renderPath(this)); | |
| } | |
| for (auto shapePaint : m_ShapePaints) | |
| { | |
| if (!shapePaint->shouldDraw()) | |
| { | |
| continue; | |
| } | |
| auto shapePaintPath = shapePaint->pickPath(this); | |
| if (shapePaintPath == nullptr) | |
| { | |
| continue; | |
| } | |
| shapePaint->draw(renderer, shapePaintPath, worldTransform()); | |
| } | |
| // Empty clips is a counter for clipping shapes that are empty, for | |
| // example because they are hidden in a solo. If emptyClips > 0, the | |
| // drawables should not be drawn. | |
| int emptyClips = 0; | |
| // We stack clip operations to avoid calling a save + clip + restore on | |
| // clipping that don't have any drawables in between. this is a common | |
| // case with drawables in solos where the drawables are not drawn. | |
| std::vector<Drawable*> pendingClipOperations; | |
| for (auto drawable = m_FirstDrawable; drawable != nullptr; | |
| drawable = drawable->prev) | |
| { | |
| auto prevClips = emptyClips; | |
| emptyClips += drawable->emptyClipCount(); | |
| if (!drawable->willDraw() || emptyClips != prevClips || emptyClips > 0) | |
| { | |
| continue; | |
| } | |
| if (drawable->isClipStart()) | |
| { | |
| pendingClipOperations.push_back(drawable); | |
| continue; | |
| } | |
| else if (pendingClipOperations.size() > 0) | |
| { | |
| // If there are clip operations pending and the next drawable is | |
| // a clip end, the clipping operation does not clip anything and | |
| // both can be skipped. | |
| if (drawable->isClipEnd()) | |
| { | |
| pendingClipOperations.pop_back(); | |
| continue; | |
| } | |
| else | |
| { | |
| for (auto& pendingClip : pendingClipOperations) | |
| { | |
| pendingClip->draw(renderer); | |
| } | |
| pendingClipOperations.clear(); | |
| } | |
| } | |
| drawable->draw(renderer); | |
| } | |
| if (save) | |
| { | |
| renderer->restore(); | |
| } | |
| } | |
| void Artboard::addToRenderPath(RenderPath* path, const Mat2D& transform) | |
| { | |
| for (auto drawable = m_FirstDrawable; drawable != nullptr; | |
| drawable = drawable->prev) | |
| { | |
| if (drawable->isHidden() || !drawable->is<Shape>()) | |
| { | |
| continue; | |
| } | |
| Shape* shape = drawable->as<Shape>(); | |
| shape->addToRenderPath(path, transform); | |
| } | |
| } | |
| void Artboard::addToRawPath(RawPath& path, const Mat2D* transform) | |
| { | |
| for (auto drawable = m_FirstDrawable; drawable != nullptr; | |
| drawable = drawable->prev) | |
| { | |
| if (drawable->isHidden() || !drawable->is<Shape>()) | |
| { | |
| continue; | |
| } | |
| Shape* shape = drawable->as<Shape>(); | |
| shape->addToRawPath(path, transform); | |
| } | |
| } | |
| Vec2D Artboard::origin() const | |
| { | |
| return m_FrameOrigin | |
| ? Vec2D(0.0f, 0.0f) | |
| : Vec2D(-layoutWidth() * originX(), -layoutHeight() * originY()); | |
| } | |
| void Artboard::xChanged() | |
| { | |
| Super::xChanged(); | |
| markHostTransformDirty(); | |
| } | |
| void Artboard::yChanged() | |
| { | |
| Super::yChanged(); | |
| markHostTransformDirty(); | |
| } | |
| // Origin has no dedicated animation/change plumbing, so a live change (e.g. a | |
| // nested artboard origin override, or a keyframed artboard origin) would leave | |
| // the cached render path and layout stale. Invalidate the background/clip path | |
| // (updateRenderPath reads origin) and force the update pass to re-run so the | |
| // content is repositioned. Components dirt is what a hosting NestedArtboard | |
| // keys off (see NestedArtboard::advanceComponent) to re-run our updatePass; | |
| // markHostTransformDirty notifies the host that our transform moved. | |
| void Artboard::originXChanged() | |
| { | |
| // Base originXChanged is a no-op; go straight to invalidation. | |
| addDirt(ComponentDirt::Path | ComponentDirt::Components); | |
| markHostTransformDirty(); | |
| } | |
| void Artboard::originYChanged() | |
| { | |
| addDirt(ComponentDirt::Path | ComponentDirt::Components); | |
| markHostTransformDirty(); | |
| } | |
| AABB Artboard::bounds() const | |
| { | |
| return m_FrameOrigin ? AABB(0.0f, 0.0f, layoutWidth(), layoutHeight()) | |
| : AABB::fromLTWH(-layoutWidth() * originX(), | |
| -layoutHeight() * originY(), | |
| layoutWidth(), | |
| layoutHeight()); | |
| } | |
| AABB Artboard::worldBounds() const | |
| { | |
| return AABB::fromLTWH(NodeBase::x(), | |
| NodeBase::y(), | |
| m_layout.width(), | |
| m_layout.height()); | |
| } | |
| bool Artboard::isTranslucent() const | |
| { | |
| for (const auto sp : m_ShapePaints) | |
| { | |
| if (!sp->isTranslucent()) | |
| { | |
| return false; // one opaque fill is sufficient to be opaque | |
| } | |
| } | |
| return true; | |
| } | |
| bool Artboard::hasAudio() const | |
| { | |
| for (auto object : m_Objects) | |
| { | |
| if (object != nullptr && object->coreType() == AudioEventBase::typeKey) | |
| { | |
| return true; | |
| } | |
| } | |
| for (auto artboardHost : m_ArtboardHosts) | |
| { | |
| for (int i = 0; i < artboardHost->artboardCount(); i++) | |
| { | |
| auto artboard = artboardHost->artboardInstance(i); | |
| if (artboard != nullptr && artboard->hasAudio()) | |
| { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| bool Artboard::isTranslucent(const LinearAnimation* anim) const | |
| { | |
| // For now we're conservative/lazy -- if we see that any of our paints are | |
| // animated we assume that might make it non-opaque, so we early out | |
| for (const auto& obj : anim->m_KeyedObjects) | |
| { | |
| const auto ptr = this->resolve(obj->objectId()); | |
| for (const auto sp : m_ShapePaints) | |
| { | |
| if (ptr == sp) | |
| { | |
| return true; | |
| } | |
| } | |
| } | |
| // If we get here, we have no animations, so just check our paints for | |
| // opacity | |
| return this->isTranslucent(); | |
| } | |
| bool Artboard::isTranslucent(const LinearAnimationInstance* inst) const | |
| { | |
| return this->isTranslucent(inst->animation()); | |
| } | |
| std::string Artboard::animationNameAt(size_t index) const | |
| { | |
| auto la = this->animation(index); | |
| return la ? la->name() : ""; | |
| } | |
| // Helper: check if a FocusData has a parent FocusData within the artboard | |
| // by walking up the component hierarchy | |
| static bool hasParentFocusData(const FocusData* focusData) | |
| { | |
| // FocusData's parent is a ContainerComponent (likely a Node) | |
| // Walk up to find if any ancestor Node has a FocusData child | |
| auto* current = focusData->parent(); | |
| while (current != nullptr) | |
| { | |
| if (current->is<Node>()) | |
| { | |
| auto* node = current->as<Node>(); | |
| for (auto child : node->children()) | |
| { | |
| if (child->is<FocusData>() && child != focusData) | |
| { | |
| return true; | |
| } | |
| } | |
| } | |
| current = current->parent(); | |
| } | |
| return false; | |
| } | |
| size_t Artboard::rootFocusDataCount() const | |
| { | |
| size_t count = 0; | |
| for (auto* object : m_Objects) | |
| { | |
| if (object != nullptr && object->is<FocusData>()) | |
| { | |
| if (!hasParentFocusData(object->as<FocusData>())) | |
| { | |
| count++; | |
| } | |
| } | |
| } | |
| return count; | |
| } | |
| FocusData* Artboard::rootFocusDataAt(size_t index) const | |
| { | |
| size_t count = 0; | |
| for (auto* object : m_Objects) | |
| { | |
| if (object != nullptr && object->is<FocusData>()) | |
| { | |
| if (!hasParentFocusData(object->as<FocusData>())) | |
| { | |
| if (count == index) | |
| { | |
| return object->as<FocusData>(); | |
| } | |
| count++; | |
| } | |
| } | |
| } | |
| return nullptr; | |
| } | |
| namespace | |
| { | |
| // Depth-first in scene (Container child) order so FocusManager child order | |
| // (tab) matches the hierarchy, not the flat m_Objects / grouped passes. | |
| // At most one FocusData is allowed as a direct child of a container; it is | |
| // registered in a first pass, then child traversal skips that FocusData. | |
| void buildFocusTreeVisit(FocusManager* focusManager, | |
| Component* component, | |
| rcp<FocusNode> focusNode) | |
| { | |
| if (component == nullptr) | |
| { | |
| return; | |
| } | |
| if (component->is<NestedArtboard>()) | |
| { | |
| auto* nestedHost = component->as<NestedArtboard>(); | |
| // Wire the nested state machines to this manager before placing the | |
| // scope: setExternalFocusManager rebuilds the nested focus tree at the | |
| // manager root, so it must run first. Track whether any was actually | |
| // re-wired — a re-wire leaves the tree at the root and requires | |
| // re-homing under the scope (forceRebuild); an already-wired tree is | |
| // left in place. | |
| bool rewired = false; | |
| for (auto* animation : nestedHost->nestedAnimations()) | |
| { | |
| if (animation->is<NestedStateMachine>()) | |
| { | |
| auto* nsm = animation->as<NestedStateMachine>(); | |
| auto* smi = nsm->stateMachineInstance(); | |
| if (smi != nullptr && smi->focusManager() != focusManager) | |
| { | |
| smi->setExternalFocusManager(focusManager); | |
| rewired = true; | |
| } | |
| } | |
| } | |
| // Scope placement must be the final write so it overrides the root | |
| // rebuild above. For data-bound hosts this parents the nested focus | |
| // tree under the host's persistent structural scope; static hosts | |
| // build directly under focusNode. placeScope=true: the build pass is | |
| // the ordering authority — the scope is re-appended at the walk's | |
| // position. | |
| nestedHost->syncNestedFocusTree(focusNode, | |
| /*placeScope=*/true, | |
| /*forceRebuild=*/rewired); | |
| } | |
| else if (component->is<ArtboardComponentList>()) | |
| { | |
| auto* list = component->as<ArtboardComponentList>(); | |
| list->ensureListScopeFocusNode(focusManager, focusNode); | |
| } | |
| if (component->is<ContainerComponent>()) | |
| { | |
| auto* cc = component->as<ContainerComponent>(); | |
| FocusData* directFd = nullptr; | |
| for (Component* ch : cc->children()) | |
| { | |
| if (ch != nullptr && ch->is<FocusData>()) | |
| { | |
| directFd = ch->as<FocusData>(); | |
| break; | |
| } | |
| } | |
| rcp<FocusNode> recurseWith; | |
| if (directFd != nullptr) | |
| { | |
| focusManager->addChild(focusNode, directFd->focusNode()); | |
| recurseWith = directFd->focusNode(); | |
| } | |
| else | |
| { | |
| recurseWith = focusNode; | |
| } | |
| for (Component* ch : cc->children()) | |
| { | |
| if (ch == nullptr || ch->is<FocusData>()) | |
| { | |
| continue; | |
| } | |
| buildFocusTreeVisit(focusManager, ch, recurseWith); | |
| } | |
| } | |
| } | |
| } // namespace | |
| FocusManager* Artboard::ensureFocusManager() | |
| { | |
| if (m_activeFocusManager != nullptr) | |
| { | |
| return m_activeFocusManager; | |
| } | |
| if (m_ownedFocusManager == nullptr) | |
| { | |
| m_ownedFocusManager = std::make_unique<FocusManager>(); | |
| } | |
| m_activeFocusManager = m_ownedFocusManager.get(); | |
| return m_activeFocusManager; | |
| } | |
| void Artboard::adoptFocusManager(FocusManager* manager) | |
| { | |
| // A null manager means "stop borrowing", which falls back to our own if we | |
| // have one — the same shape as clearing the old external-manager override. | |
| if (manager == nullptr) | |
| { | |
| manager = m_ownedFocusManager.get(); | |
| } | |
| if (m_activeFocusManager == manager) | |
| { | |
| return; | |
| } | |
| if (m_activeFocusManager != nullptr) | |
| { | |
| cleanupFocusTree(); | |
| } | |
| m_activeFocusManager = manager; | |
| if (manager != nullptr) | |
| { | |
| buildFocusTree(manager, nullptr); | |
| } | |
| } | |
| void Artboard::buildFocusTree(FocusManager* focusManager, | |
| rcp<FocusNode> parentFocusNode) | |
| { | |
| if (focusManager == nullptr) | |
| { | |
| return; | |
| } | |
| // Store reference to the active focus manager | |
| setActiveFocusManager(focusManager); | |
| #ifdef WITH_RIVE_TOOLS | |
| // Store the parent focus node if provided (for later retrieval by tools) | |
| if (parentFocusNode != nullptr) | |
| { | |
| m_externalParentFocusNode = parentFocusNode; | |
| } | |
| // Use explicit parent if provided, otherwise fall back to external parent | |
| rcp<FocusNode> effectiveParent = parentFocusNode != nullptr | |
| ? parentFocusNode | |
| : m_externalParentFocusNode; | |
| #else | |
| rcp<FocusNode> effectiveParent = parentFocusNode; | |
| #endif | |
| if (as<ContainerComponent>() != nullptr) | |
| { | |
| buildFocusTreeVisit(focusManager, this, std::move(effectiveParent)); | |
| } | |
| } | |
| void Artboard::buildFocusTree(rcp<FocusNode> parentFocusNode) | |
| { | |
| if (parentFocusNode == nullptr) | |
| { | |
| return; | |
| } | |
| auto* manager = parentFocusNode->manager(); | |
| if (manager == nullptr) | |
| { | |
| return; | |
| } | |
| buildFocusTree(manager, parentFocusNode); | |
| } | |
| void Artboard::cleanupFocusTree() | |
| { | |
| if (m_activeFocusManager == nullptr) | |
| { | |
| return; | |
| } | |
| // Remove all FocusData's FocusNodes from the FocusManager | |
| for (auto* obj : m_Objects) | |
| { | |
| if (obj != nullptr && obj->is<FocusData>()) | |
| { | |
| auto* fd = obj->as<FocusData>(); | |
| auto node = fd->focusNode(); | |
| if (node == nullptr) | |
| { | |
| // FocusNode never lazily created — nothing to remove. | |
| continue; | |
| } | |
| // Remove the node when it belongs to this manager. Nodes owned by | |
| // a DIFFERENT live manager are left untouched. | |
| if (node->manager() == m_activeFocusManager) | |
| { | |
| m_activeFocusManager->removeChild(node); | |
| } | |
| else if (node->manager() == nullptr && node->parent() != nullptr) | |
| { | |
| // The node's manager was nulled while it is still attached — | |
| // either by an ancestor removal (removeChild clears m_manager | |
| // across a removed subtree) or by ~FocusManager, which nulls | |
| // m_manager tree-wide as it dies. Detach node-side only: in the | |
| // second case m_activeFocusManager is already dangling (it is | |
| // an unowned back-pointer, see setActiveFocusManager) and | |
| // touching it is a use-after-free. Nothing is lost by skipping | |
| // markFocusableContentDirty here — if the manager is alive, the | |
| // ancestor removal that nulled m_manager already marked it. | |
| node->removeFromParent(); | |
| } | |
| } | |
| } | |
| // Propagate cleanup to nested artboards that share our FocusManager | |
| for (auto* nestedArtboardHost : m_NestedArtboards) | |
| { | |
| auto* nestedArtboard = nestedArtboardHost->artboardInstance(0); | |
| if (nestedArtboard != nullptr && | |
| nestedArtboard->focusManager() == m_activeFocusManager) | |
| { | |
| nestedArtboard->cleanupFocusTree(); | |
| } | |
| } | |
| // Propagate cleanup to ArtboardComponentList items | |
| for (auto* componentList : m_ComponentLists) | |
| { | |
| for (size_t i = 0; i < componentList->artboardCount(); i++) | |
| { | |
| auto* nestedArtboard = | |
| componentList->artboardInstance(static_cast<int>(i)); | |
| if (nestedArtboard != nullptr && | |
| nestedArtboard->focusManager() == m_activeFocusManager) | |
| { | |
| nestedArtboard->cleanupFocusTree(); | |
| } | |
| } | |
| } | |
| for (auto* componentList : m_ComponentLists) | |
| { | |
| componentList->removeListScopeFocusNode(); | |
| } | |
| // Fall back to our own manager rather than to "no manager" | |
| m_activeFocusManager = m_ownedFocusManager.get(); | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| void Artboard::setExternalParentFocusNode(rcp<FocusNode> node) | |
| { | |
| m_externalParentFocusNode = std::move(node); | |
| } | |
| rcp<FocusNode> Artboard::externalParentFocusNode() const | |
| { | |
| return m_externalParentFocusNode; | |
| } | |
| void Artboard::collapseSingle(bool value) { Component::collapse(value); } | |
| #endif | |
| // Builds the semantic tree for this artboard. Iterates all objects, | |
| // registers each SemanticData's SemanticNode with the manager, then | |
| // propagates to nested artboards using findClosestSemanticNode() for | |
| // parent resolution across artboard boundaries. | |
| // | |
| // For nested artboards (those with a host), a boundary SemanticNode is | |
| // created and inserted as the parent of all semantic nodes within this | |
| // artboard. Boundary nodes are structural-only — skipped during | |
| // flattening — but enable better subtree collapse/uncollapse and | |
| // targeted bounds dirtying when the host transform changes. | |
| void Artboard::buildSemanticTree(SemanticManager* semanticManager, | |
| rcp<SemanticNode> parentSemanticNode) | |
| { | |
| if (semanticManager == nullptr) | |
| { | |
| return; | |
| } | |
| // Store reference to the active semantic manager | |
| setActiveSemanticManager(semanticManager); | |
| // For nested artboards, create a boundary node that acts as the | |
| // structural root of this artboard's semantic subtree. | |
| rcp<SemanticNode> effectiveParent = parentSemanticNode; | |
| if (host() != nullptr) | |
| { | |
| if (m_semanticBoundaryNode == nullptr) | |
| { | |
| // Id is assigned by the SemanticManager on addChild() below. | |
| m_semanticBoundaryNode = rcp<SemanticNode>(new SemanticNode()); | |
| m_semanticBoundaryNode->isBoundaryNode(true); | |
| m_semanticBoundaryNode->boundaryArtboard(this); | |
| } | |
| semanticManager->addChild(parentSemanticNode, m_semanticBoundaryNode); | |
| // Seed the boundary as dirty so the first drainDiff() resolves its | |
| // bounds from the artboard rect even when semantics are enabled on | |
| // an already-settled scene (no subsequent WorldTransform dirt to | |
| // trigger this via the normal update cycle). | |
| markSemanticBoundaryTransformDirty(); | |
| effectiveParent = m_semanticBoundaryNode; | |
| } | |
| // Register all SemanticData in this artboard | |
| for (auto* obj : m_Objects) | |
| { | |
| if (obj != nullptr && obj->is<SemanticData>()) | |
| { | |
| auto* sd = obj->as<SemanticData>(); | |
| auto* localParent = sd->findParentSemanticData(); | |
| rcp<SemanticNode> parentNode = localParent != nullptr | |
| ? localParent->semanticNode() | |
| : effectiveParent; | |
| semanticManager->addChild(parentNode, sd->semanticNode()); | |
| sd->syncSemanticTreeVisibility(); | |
| } | |
| } | |
| // Propagate semantic registration to nested artboards that might have been | |
| // created before this artboard's semanticManager was available. | |
| // | |
| // We check if the nested artboard's semanticManager is DIFFERENT from ours. | |
| // If it is, that means it created its own internal semanticManager when it | |
| // should be sharing the parent's. We rebuild its semantic tree with the | |
| // correct shared semanticManager. | |
| // Handle NestedArtboard instances | |
| for (auto* nestedArtboardHost : m_NestedArtboards) | |
| { | |
| // Find closest semantic node (handles artboard boundaries) | |
| auto hostParentNode = | |
| SemanticData::findClosestSemanticNode(nestedArtboardHost); | |
| if (hostParentNode == nullptr) | |
| { | |
| hostParentNode = effectiveParent; | |
| } | |
| auto* nestedArtboard = nestedArtboardHost->artboardInstance(0); | |
| if (nestedArtboard != nullptr && | |
| nestedArtboard->semanticManager() != semanticManager) | |
| { | |
| // Clean up old semantic tree if it exists (with wrong | |
| // semanticManager) | |
| nestedArtboard->cleanupSemanticTree(); | |
| nestedArtboard->buildSemanticTree(semanticManager, hostParentNode); | |
| } | |
| } | |
| // Handle ArtboardComponentList instances | |
| for (auto* componentList : m_ComponentLists) | |
| { | |
| // Find closest semantic node (handles artboard boundaries) | |
| auto hostParentNode = | |
| SemanticData::findClosestSemanticNode(componentList); | |
| if (hostParentNode == nullptr) | |
| { | |
| hostParentNode = effectiveParent; | |
| } | |
| for (size_t i = 0; i < componentList->artboardCount(); i++) | |
| { | |
| auto* nestedArtboard = | |
| componentList->artboardInstance(static_cast<int>(i)); | |
| if (nestedArtboard != nullptr && | |
| nestedArtboard->semanticManager() != semanticManager) | |
| { | |
| // Clean up old semantic tree if it exists (with wrong | |
| // semanticManager) | |
| nestedArtboard->cleanupSemanticTree(); | |
| nestedArtboard->buildSemanticTree(semanticManager, | |
| hostParentNode); | |
| } | |
| } | |
| } | |
| } | |
| void Artboard::cleanupSemanticTree() | |
| { | |
| if (m_activeSemanticManager == nullptr) | |
| { | |
| return; | |
| } | |
| // Propagate cleanup to nested artboards that share our SemanticManager. | |
| // Done FIRST so their boundary nodes are removed before we remove ours. | |
| for (auto* nestedArtboardHost : m_NestedArtboards) | |
| { | |
| auto* nestedArtboard = nestedArtboardHost->artboardInstance(0); | |
| if (nestedArtboard != nullptr && | |
| nestedArtboard->semanticManager() == m_activeSemanticManager) | |
| { | |
| nestedArtboard->cleanupSemanticTree(); | |
| } | |
| } | |
| // Propagate cleanup to ArtboardComponentList items | |
| for (auto* componentList : m_ComponentLists) | |
| { | |
| for (size_t i = 0; i < componentList->artboardCount(); i++) | |
| { | |
| auto* nestedArtboard = | |
| componentList->artboardInstance(static_cast<int>(i)); | |
| if (nestedArtboard != nullptr && | |
| nestedArtboard->semanticManager() == m_activeSemanticManager) | |
| { | |
| nestedArtboard->cleanupSemanticTree(); | |
| } | |
| } | |
| } | |
| // Remove all SemanticData's SemanticNodes from the SemanticManager. | |
| // Use hasSemanticNode() to avoid lazy-creating a node during cleanup. | |
| for (auto* obj : m_Objects) | |
| { | |
| if (obj != nullptr && obj->is<SemanticData>()) | |
| { | |
| auto* sd = obj->as<SemanticData>(); | |
| if (!sd->hasSemanticNode()) | |
| { | |
| continue; | |
| } | |
| auto node = sd->semanticNode(); | |
| if (node != nullptr && node->manager() == m_activeSemanticManager) | |
| { | |
| m_activeSemanticManager->removeChild(node); | |
| } | |
| } | |
| } | |
| // Remove the boundary node for this artboard (if any). | |
| if (m_semanticBoundaryNode != nullptr && | |
| m_semanticBoundaryNode->manager() == m_activeSemanticManager) | |
| { | |
| m_activeSemanticManager->removeChild(m_semanticBoundaryNode); | |
| } | |
| m_semanticBoundaryNode = nullptr; | |
| // Clear the active semantic manager reference | |
| m_activeSemanticManager = nullptr; | |
| } | |
| // Walk the semantic subtree under a boundary node and collapse each | |
| // SemanticData directly via the back-pointer. O(K) where K = semantic | |
| // nodes under this boundary. | |
| static void collapseBoundarySubtree(SemanticNode* node, bool value) | |
| { | |
| // Copy children before iterating — collapse(true) calls | |
| // removeChild which mutates the original vector. | |
| std::vector<rcp<SemanticNode>> children(node->children()); | |
| for (const auto& child : children) | |
| { | |
| auto* sd = child->semanticData(); | |
| if (sd != nullptr && sd->isCollapsed() != value) | |
| { | |
| sd->collapse(value); | |
| } | |
| collapseBoundarySubtree(child.get(), value); | |
| } | |
| } | |
| // Semantic-only collapse for this artboard's semantic nodes. | |
| // When collapsing, walks the boundary's semantic subtree (O(K) semantic | |
| // nodes). When uncollapsing, falls back to m_Objects because | |
| // SemanticData::collapse(false) re-parents nodes outside the boundary. | |
| void Artboard::collapseSemanticBoundary(bool value) | |
| { | |
| if (m_activeSemanticManager == nullptr) | |
| { | |
| return; | |
| } | |
| if (value && m_semanticBoundaryNode != nullptr) | |
| { | |
| // Fast path: walk the boundary tree. | |
| collapseBoundarySubtree(m_semanticBoundaryNode.get(), true); | |
| } | |
| else | |
| { | |
| // Uncollapse or no boundary: scan artboard objects. | |
| for (auto* obj : m_Objects) | |
| { | |
| if (obj != nullptr && obj->is<SemanticData>()) | |
| { | |
| auto* sd = obj->as<SemanticData>(); | |
| if (sd->isCollapsed() != value) | |
| { | |
| sd->collapse(value); | |
| } | |
| } | |
| } | |
| } | |
| if (!value) | |
| { | |
| markSemanticBoundaryTransformDirty(); | |
| } | |
| } | |
| // Recursively mark all semantic nodes under the boundary as bounds-dirty. | |
| // Called when the host transform changes so bounds are recalculated in | |
| // root artboard space. | |
| void Artboard::markSemanticBoundaryTransformDirty() | |
| { | |
| if (m_semanticBoundaryNode == nullptr || m_activeSemanticManager == nullptr) | |
| { | |
| return; | |
| } | |
| m_activeSemanticManager->markBoundaryDirty(m_semanticBoundaryNode->id()); | |
| } | |
| std::string Artboard::stateMachineNameAt(size_t index) const | |
| { | |
| auto sm = this->stateMachine(index); | |
| return sm ? sm->name() : ""; | |
| } | |
| LinearAnimation* Artboard::animation(const std::string& name) const | |
| { | |
| for (auto animation : m_Animations) | |
| { | |
| if (animation->name() == name) | |
| { | |
| return animation; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| LinearAnimation* Artboard::animation(size_t index) const | |
| { | |
| if (index >= m_Animations.size()) | |
| { | |
| return nullptr; | |
| } | |
| return m_Animations[index]; | |
| } | |
| StateMachine* Artboard::stateMachine(const std::string& name) const | |
| { | |
| for (auto machine : m_StateMachines) | |
| { | |
| if (machine->name() == name) | |
| { | |
| return machine; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| StateMachine* Artboard::stateMachine(size_t index) const | |
| { | |
| if (index >= m_StateMachines.size()) | |
| { | |
| return nullptr; | |
| } | |
| return m_StateMachines[index]; | |
| } | |
| int Artboard::defaultStateMachineIndex() const | |
| { | |
| int index = defaultStateMachineId(); | |
| if ((size_t)index >= m_StateMachines.size()) | |
| { | |
| index = -1; | |
| } | |
| return index; | |
| } | |
| NestedArtboard* Artboard::nestedArtboard(const std::string& name) const | |
| { | |
| for (auto nested : m_NestedArtboards) | |
| { | |
| if (nested->name() == name) | |
| { | |
| return nested; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| NestedArtboard* Artboard::nestedArtboardAtPath(const std::string& path) const | |
| { | |
| // name parameter can be a name or a path to recursively find a nested | |
| // artboard | |
| std::string delimiter = "/"; | |
| size_t firstDelim = path.find(delimiter); | |
| std::string artboardName = | |
| firstDelim == std::string::npos ? path : path.substr(0, firstDelim); | |
| std::string restOfPath = firstDelim == std::string::npos | |
| ? "" | |
| : path.substr(firstDelim + 1, path.size()); | |
| // Find the nested artboard at this level | |
| if (!artboardName.empty()) | |
| { | |
| auto nested = nestedArtboard(artboardName); | |
| if (nested != nullptr) | |
| { | |
| if (restOfPath.empty()) | |
| { | |
| return nested; | |
| } | |
| else | |
| { | |
| auto artboard = nested->artboardInstance(); | |
| return artboard->nestedArtboardAtPath(restOfPath); | |
| } | |
| } | |
| } | |
| return nullptr; | |
| } | |
| // std::unique_ptr<ArtboardInstance> Artboard::instance() const | |
| // { | |
| // std::unique_ptr<ArtboardInstance> artboardClone(new ArtboardInstance); | |
| // artboardClone->copy(*this); | |
| // artboardClone->m_Factory = m_Factory; | |
| // artboardClone->m_FrameOrigin = m_FrameOrigin; | |
| // artboardClone->m_IsInstance = true; | |
| // std::vector<Core*>& cloneObjects = artboardClone->m_Objects; | |
| // cloneObjects.push_back(artboardClone.get()); | |
| // if (!m_Objects.empty()) | |
| // { | |
| // // Skip first object (artboard). | |
| // auto itr = m_Objects.begin(); | |
| // while (++itr != m_Objects.end()) | |
| // { | |
| // auto object = *itr; | |
| // cloneObjects.push_back(object == nullptr ? nullptr : | |
| // object->clone()); | |
| // } | |
| // } | |
| // for (auto animation : m_Animations) | |
| // { | |
| // artboardClone->m_Animations.push_back(animation); | |
| // } | |
| // for (auto stateMachine : m_StateMachines) | |
| // { | |
| // artboardClone->m_StateMachines.push_back(stateMachine); | |
| // } | |
| // if (artboardClone->initialize() != StatusCode::Ok) | |
| // { | |
| // artboardClone = nullptr; | |
| // } | |
| // assert(artboardClone->isInstance()); | |
| // return artboardClone; | |
| // } | |
| void Artboard::frameOrigin(bool value) | |
| { | |
| if (value == m_FrameOrigin) | |
| { | |
| return; | |
| } | |
| m_FrameOrigin = value; | |
| addDirt(ComponentDirt::Path); | |
| } | |
| StatusCode Artboard::import(ImportStack& importStack) | |
| { | |
| auto backboardImporter = | |
| importStack.latest<BackboardImporter>(Backboard::typeKey); | |
| if (backboardImporter == nullptr) | |
| { | |
| return StatusCode::MissingObject; | |
| } | |
| StatusCode result = Super::import(importStack); | |
| if (result == StatusCode::Ok) | |
| { | |
| backboardImporter->addArtboard(this); | |
| } | |
| else | |
| { | |
| backboardImporter->addMissingArtboard(); | |
| } | |
| return result; | |
| } | |
| void Artboard::buildDataContext(rcp<DataContext> value) {} | |
| void Artboard::internalDataContext(rcp<DataContext> value) | |
| { | |
| m_DataContext = value; | |
| for (auto artboardHost : m_ArtboardHosts) | |
| { | |
| auto value = | |
| m_DataContext->getViewModelInstance(artboardHost->dataBindPath()); | |
| if (value != nullptr && value->is<ViewModelInstance>()) | |
| { | |
| artboardHost->bindViewModelInstance(value, m_DataContext); | |
| } | |
| else | |
| { | |
| artboardHost->internalDataContext(m_DataContext); | |
| } | |
| } | |
| bindDataBindsFromContext(m_DataContext.get()); | |
| sortDataBinds(); | |
| for (auto* scriptedObject : m_ScriptedObjects) | |
| { | |
| scriptedObject->dataContext(value); | |
| } | |
| initScriptedObjects(); | |
| } | |
| void Artboard::rebind() { internalDataContext(m_DataContext); } | |
| void Artboard::relinkDataContext() | |
| { | |
| if (m_DataContext == nullptr) | |
| { | |
| return; | |
| } | |
| for (auto artboardHost : m_ArtboardHosts) | |
| { | |
| rcp<ViewModelInstance> value = | |
| m_DataContext->getViewModelInstance(artboardHost->dataBindPath()); | |
| if (value == nullptr) | |
| { | |
| value = m_DataContext->mainViewModelInstance(); | |
| } | |
| artboardHost->relinkDataContext(value); | |
| } | |
| } | |
| void Artboard::rebuildDataBind(DataBind* dataBind) | |
| { | |
| if (dataBind->is<DataBindContext>()) | |
| { | |
| dataBind->as<DataBindContext>()->bindFromContext(m_DataContext.get()); | |
| } | |
| }; | |
| void Artboard::unbind() | |
| { | |
| clearDataContext(); | |
| unbindDataBinds(); | |
| for (auto artboardHost : m_ArtboardHosts) | |
| { | |
| artboardHost->unbind(); | |
| } | |
| } | |
| void Artboard::clearDataContext() | |
| { | |
| if (m_DataContext) | |
| { | |
| m_DataContext->removeDependentContainer(this); | |
| m_DataContext = nullptr; | |
| } | |
| for (auto artboardHost : m_ArtboardHosts) | |
| { | |
| artboardHost->clearDataContext(); | |
| } | |
| for (auto* scriptedObject : m_ScriptedObjects) | |
| { | |
| scriptedObject->resetLuaInit(); | |
| } | |
| } | |
| float Artboard::volume() const { return m_volume; } | |
| void Artboard::volume(float value) | |
| { | |
| m_volume = value; | |
| for (auto artboardHost : m_ArtboardHosts) | |
| { | |
| for (int i = 0; i < artboardHost->artboardCount(); i++) | |
| { | |
| auto artboard = artboardHost->artboardInstance(i); | |
| if (artboard != nullptr) | |
| { | |
| artboard->volume(value); | |
| } | |
| } | |
| } | |
| } | |
| void Artboard::hostOpacity(float value) | |
| { | |
| if (m_hostOpacity == value) | |
| { | |
| return; | |
| } | |
| m_hostOpacity = value; | |
| // Re-propagate opacity to our contents. The property itself is untouched, | |
| // so we dirty render opacity directly rather than through opacity(...). | |
| addDirt(ComponentDirt::RenderOpacity, true); | |
| } | |
| void Artboard::dataContext(rcp<DataContext> value) | |
| { | |
| internalDataContext(value); | |
| } | |
| rcp<const File> Artboard::artboardFile() const { return nullptr; } | |
| void Artboard::bindViewModelInstance(rcp<ViewModelInstance> viewModelInstance) | |
| { | |
| bindViewModelInstance(viewModelInstance, nullptr); | |
| } | |
| void Artboard::bindViewModelInstance(rcp<ViewModelInstance> viewModelInstance, | |
| rcp<DataContext> parent) | |
| { | |
| if (viewModelInstance == nullptr) | |
| { | |
| unbind(); | |
| return; | |
| } | |
| setViewModelInstance(std::move(viewModelInstance)); | |
| if (parent != nullptr && m_DataContext != nullptr) | |
| { | |
| m_DataContext->parent(parent); | |
| } | |
| bind(); | |
| } | |
| void Artboard::setViewModelInstance(rcp<ViewModelInstance> viewModelInstance) | |
| { | |
| if (viewModelInstance == nullptr) | |
| { | |
| return; | |
| } | |
| if (m_DataContext == nullptr) | |
| { | |
| m_DataContext = make_rcp<DataContext>(viewModelInstance); | |
| m_DataContext->addDependentContainer(this); | |
| return; | |
| } | |
| // The data context re-points every attached container (this artboard and | |
| // any state machines sharing the context) off the old main and onto the new | |
| // one. | |
| m_DataContext->setMainViewModelInstance(viewModelInstance); | |
| } | |
| void Artboard::bindViewModelInstances( | |
| std::vector<rcp<ViewModelInstance>> viewModelInstances, | |
| rcp<DataContext> parent) | |
| { | |
| if (viewModelInstances.empty()) | |
| { | |
| unbind(); | |
| return; | |
| } | |
| clearDataContext(); | |
| auto dataContext = make_rcp<DataContext>(std::move(viewModelInstances)); | |
| dataContext->addDependentContainer(this); | |
| dataContext->parent(parent); | |
| internalDataContext(dataContext); | |
| } | |
| void Artboard::bind() | |
| { | |
| if (m_DataContext != nullptr) | |
| { | |
| internalDataContext(m_DataContext); | |
| } | |
| } | |
| rcp<ViewModelInstance> Artboard::globalViewModelInstance( | |
| const std::string& name) | |
| { | |
| // Pure read: returns the instance in the named slot only if one has been | |
| // set/bound; never creates. | |
| if (m_DataContext == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| auto f = artboardFile(); | |
| if (f == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| return m_DataContext->instanceForSlot(f->viewModelId(name)); | |
| } | |
| bool Artboard::setGlobalViewModelInstance( | |
| const std::string& name, | |
| rcp<ViewModelInstance> viewModelInstance) | |
| { | |
| // A null instance is allowed: it empties the named slot below. | |
| auto f = artboardFile(); | |
| if (f == nullptr) | |
| { | |
| return false; | |
| } | |
| // The slot is addressed by the named view model (its file index), not by | |
| // the instance's own view model — so an override instance of a different | |
| // view model can be placed on the slot. | |
| uint32_t slotKey = f->viewModelId(name); | |
| if (slotKey >= f->viewModelCount()) | |
| { | |
| return false; | |
| } | |
| // Only global view models get a slot; a non-global name is not a valid | |
| // global slot and must not be slotted. | |
| auto slotViewModel = f->viewModel(slotKey); | |
| if (slotViewModel == nullptr || | |
| static_cast<ViewModelType>(slotViewModel->viewModelType()) != | |
| ViewModelType::global) | |
| { | |
| return false; | |
| } | |
| if (m_DataContext == nullptr) | |
| { | |
| // Nothing to clear when there is no context yet; only create one when | |
| // actually placing an instance. | |
| if (viewModelInstance == nullptr) | |
| { | |
| return true; | |
| } | |
| m_DataContext = make_rcp<DataContext>(rcp<ViewModelInstance>(nullptr)); | |
| m_DataContext->addDependentContainer(this); | |
| } | |
| // The data context re-points every attached container off any previous | |
| // instance occupying this slot and onto the new one (or empties the slot | |
| // when the instance is null). | |
| m_DataContext->setViewModelInstanceForSlot(slotKey, viewModelInstance); | |
| return true; | |
| } | |
| bool Artboard::isAncestor(const Artboard* artboard) | |
| { | |
| if (artboard != nullptr && m_artboardSource == artboard->artboardSource()) | |
| { | |
| return true; | |
| } | |
| if (parentArtboard() != nullptr) | |
| { | |
| return parentArtboard()->isAncestor(artboard); | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| // Editor artboards don't have a host, so we expose a function that calls | |
| // the host in dart. | |
| if (m_isAncestorCallback != nullptr) | |
| { | |
| // Dart can't return booleans to cpp, so we use a uint_8 instead | |
| auto isAncestor = | |
| m_isAncestorCallback(callbackUserData, artboard->artboardId()); | |
| if (isAncestor == 1) | |
| { | |
| return true; | |
| } | |
| } | |
| #endif | |
| return false; | |
| } | |
| void Artboard::changed() | |
| { | |
| if (!m_didChange) | |
| { | |
| m_didChange = true; | |
| if (parentArtboard()) | |
| { | |
| parentArtboard()->changed(); | |
| } | |
| } | |
| } | |
| ////////// ArtboardInstance | |
| #include "rive/animation/linear_animation_instance.hpp" | |
| #include "rive/animation/state_machine_instance.hpp" | |
| ArtboardInstance::ArtboardInstance() {} | |
| ArtboardInstance::~ArtboardInstance() {} | |
| void ArtboardInstance::file(rcp<const File> file) { m_file = std::move(file); } | |
| rcp<const File> ArtboardInstance::file() const { return m_file; } | |
| rcp<const File> ArtboardInstance::artboardFile() const { return m_file; } | |
| std::unique_ptr<LinearAnimationInstance> ArtboardInstance::animationAt( | |
| size_t index) | |
| { | |
| auto la = this->animation(index); | |
| return la ? std::make_unique<LinearAnimationInstance>(la, this) : nullptr; | |
| } | |
| std::unique_ptr<LinearAnimationInstance> ArtboardInstance::animationNamed( | |
| const std::string& name) | |
| { | |
| auto la = this->animation(name); | |
| return la ? std::make_unique<LinearAnimationInstance>(la, this) : nullptr; | |
| } | |
| std::unique_ptr<StateMachineInstance> ArtboardInstance::stateMachineAt( | |
| size_t index) | |
| { | |
| auto sm = this->stateMachine(index); | |
| if (sm == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| auto smInstance = std::make_unique<StateMachineInstance>(sm, this); | |
| if (auto dc = dataContext()) | |
| { | |
| smInstance->inheritDataContext(dc); | |
| } | |
| return smInstance; | |
| } | |
| std::unique_ptr<StateMachineInstance> ArtboardInstance::stateMachineNamed( | |
| const std::string& name) | |
| { | |
| auto sm = this->stateMachine(name); | |
| if (sm == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| auto smInstance = std::make_unique<StateMachineInstance>(sm, this); | |
| if (auto dc = dataContext()) | |
| { | |
| smInstance->inheritDataContext(dc); | |
| } | |
| return smInstance; | |
| } | |
| std::unique_ptr<StateMachineInstance> ArtboardInstance::defaultStateMachine() | |
| { | |
| const int index = this->defaultStateMachineIndex(); | |
| return index >= 0 ? this->stateMachineAt(index) : nullptr; | |
| } | |
| std::unique_ptr<Scene> ArtboardInstance::defaultScene() | |
| { | |
| std::unique_ptr<Scene> scene = this->defaultStateMachine(); | |
| if (!scene) | |
| { | |
| scene = this->stateMachineAt(0); | |
| } | |
| if (!scene) | |
| { | |
| scene = this->animationAt(0); | |
| } | |
| return scene; | |
| } | |
| SMIInput* ArtboardInstance::input(const std::string& name, | |
| const std::string& path) | |
| { | |
| return getNamedInput<SMIInput>(name, path); | |
| } | |
| template <typename InstType> | |
| InstType* ArtboardInstance::getNamedInput(const std::string& name, | |
| const std::string& path) | |
| { | |
| if (!path.empty()) | |
| { | |
| auto nestedArtboard = nestedArtboardAtPath(path); | |
| if (nestedArtboard != nullptr) | |
| { | |
| auto input = nestedArtboard->input(name); | |
| if (input != nullptr && input->input() != nullptr) | |
| { | |
| return static_cast<InstType*>(input->input()); | |
| } | |
| } | |
| } | |
| return nullptr; | |
| } | |
| SMIBool* ArtboardInstance::getBool(const std::string& name, | |
| const std::string& path) | |
| { | |
| return getNamedInput<SMIBool>(name, path); | |
| } | |
| SMINumber* ArtboardInstance::getNumber(const std::string& name, | |
| const std::string& path) | |
| { | |
| return getNamedInput<SMINumber>(name, path); | |
| } | |
| SMITrigger* ArtboardInstance::getTrigger(const std::string& name, | |
| const std::string& path) | |
| { | |
| return getNamedInput<SMITrigger>(name, path); | |
| } | |
| TextValueRun* ArtboardInstance::getTextRun(const std::string& name, | |
| const std::string& path) | |
| { | |
| if (path.empty()) | |
| { | |
| return nullptr; | |
| } | |
| auto nestedArtboard = nestedArtboardAtPath(path); | |
| if (nestedArtboard == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| auto artboardInstance = nestedArtboard->artboardInstance(); | |
| if (artboardInstance == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| return artboardInstance->find<TextValueRun>(name); | |
| } | |
| #ifdef EXTERNAL_RIVE_AUDIO_ENGINE | |
| rcp<AudioEngine> Artboard::audioEngine() const { return m_audioEngine; } | |
| void Artboard::audioEngine(rcp<AudioEngine> audioEngine) | |
| { | |
| m_audioEngine = audioEngine; | |
| for (auto artboardHost : m_ArtboardHosts) | |
| { | |
| for (int i = 0; i < artboardHost->artboardCount(); i++) | |
| { | |
| auto artboard = artboardHost->artboardInstance(i); | |
| if (artboard != nullptr) | |
| { | |
| artboard->audioEngine(audioEngine); | |
| } | |
| } | |
| } | |
| } | |
| #endif |