| #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_color.hpp" | |
| #include "rive/custom_property_number.hpp" | |
| #include "rive/custom_property_trigger.hpp" | |
| #include "rive/math/math_types.hpp" | |
| #include "rive/shapes/paint/color.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/watermark.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/path_composer.hpp" | |
| #include "rive/text/text_style.hpp" | |
| #include "rive/text/text_variation_helper.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 "rive/bitmap_cache.hpp" | |
| #include "rive/layer_mask.hpp" | |
| #ifdef RIVE_CANVAS | |
| #include "rive/offscreen_raster.hpp" | |
| #include "rive/renderer/render_context.hpp" | |
| #include "rive/renderer/render_canvas.hpp" | |
| #include "rive/renderer/cmd/deferred_canvas_host.hpp" | |
| #include "rive/shapes/paint/image_sampler.hpp" | |
| #include <algorithm> | |
| #include <array> | |
| #include <cmath> | |
| #endif | |
| #include <set> | |
| #include <unordered_map> | |
| using namespace rive; | |
| uint64_t Artboard::sm_frameId = 0; | |
| #ifdef TESTING | |
| uint64_t Artboard::sm_layoutPassCount = 0; | |
| uint64_t Artboard::sm_dirtNotifications = 0; | |
| #endif | |
| Artboard::Artboard() | |
| { | |
| // Artboards need to override default clip value to true. | |
| m_Clip = true; | |
| #ifdef WITH_RIVE_TOOLS | |
| callbackUserData = this; | |
| #endif | |
| } | |
| #ifdef TESTING | |
| Artboard::Artboard(Factory* factory) : m_Factory(factory) { m_Clip = true; } | |
| #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 != nullptr | |
| ? m_audioEngine | |
| : AudioEngine::RuntimeEngine(false); | |
| #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; | |
| } | |
| case BitmapCacheBase::typeKey: | |
| m_BitmapCache = object->as<BitmapCache>(); | |
| 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>()); | |
| } | |
| else if (object->is<LayerMask>()) | |
| { | |
| m_layerMasks.push_back(object->as<LayerMask>()); | |
| } | |
| } | |
| // 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); | |
| const 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; | |
| } | |
| } | |
| interleaveLayerMasks(); | |
| clearRedundantOperations(); | |
| } | |
| // True when `d` belongs inside a bracket whose members are `members`. Pure | |
| // bracket operations (clip and mask markers) are always allowed: they are | |
| // nested structure, not content. A DrawableProxy is allowed when the component | |
| // it stands in for is a member -- a LayoutComponent's proxy is injected by the | |
| // artboard and so never appears in a subtree walk, but its save()+clipPath() | |
| // has to sit inside the same bracket as the restore() in its draw(). | |
| static bool spansMember(Drawable* d, | |
| const std::unordered_set<Drawable*>& members) | |
| { | |
| if (members.count(d) != 0) | |
| { | |
| return true; | |
| } | |
| if (!d->isProxy()) | |
| { | |
| return false; | |
| } | |
| if (d->isClipStart() || d->isClipEnd() || d->isMaskStart() || | |
| d->isMaskEnd()) | |
| { | |
| return true; | |
| } | |
| Drawable* stands = d->hittableComponent(); | |
| return stands != nullptr && members.count(stands) != 0; | |
| } | |
| bool Artboard::spliceLayerMaskBracket(LayerMask* mask, | |
| const std::vector<Drawable*>& members, | |
| LayerMaskOp startOp, | |
| LayerMaskOp endOp, | |
| LayerMaskProxyDrawable** startOut) | |
| { | |
| if (startOut != nullptr) | |
| { | |
| *startOut = nullptr; | |
| } | |
| if (members.empty()) | |
| { | |
| return false; | |
| } | |
| std::unordered_set<Drawable*> memberSet(members.begin(), members.end()); | |
| // `next` is earlier in draw order, `prev` later: the walk from | |
| // m_FirstDrawable follows prev, so first/last here are in draw order. | |
| Drawable* first = nullptr; | |
| Drawable* last = nullptr; | |
| for (auto d = m_FirstDrawable; d != nullptr; d = d->prev) | |
| { | |
| if (memberSet.count(d) != 0) | |
| { | |
| if (first == nullptr) | |
| { | |
| first = d; | |
| } | |
| last = d; | |
| } | |
| } | |
| if (first == nullptr) | |
| { | |
| // The members are not in this artboard's draw list at all (all hidden | |
| // behind a collapsed parent, say). | |
| return false; | |
| } | |
| // Our markers have to nest with every bracket already in the list, never | |
| // interleave: the draw walk is handed a [start, end) range and would run | |
| // off the list if an inner end marker escaped our outer one. The member | |
| // extremes are not necessarily balanced -- a mask processed before us may | |
| // have bracketed our first or last member -- so grow the range outward | |
| // until it is. Bracket markers are always allowed inside a span, so growing | |
| // can only make the contiguity check below easier. | |
| bool grew = true; | |
| while (grew) | |
| { | |
| grew = false; | |
| // Starts whose end is inside the range, and ends whose start is before | |
| // it. | |
| int clipDepth = 0, maskDepth = 0; | |
| int clipDeficit = 0, maskDeficit = 0; | |
| for (auto d = first;; d = d->prev) | |
| { | |
| if (d->isClipStart()) | |
| { | |
| ++clipDepth; | |
| } | |
| else if (d->isClipEnd()) | |
| { | |
| clipDepth > 0 ? --clipDepth : ++clipDeficit; | |
| } | |
| else if (d->isMaskStart()) | |
| { | |
| ++maskDepth; | |
| } | |
| else if (d->isMaskEnd()) | |
| { | |
| maskDepth > 0 ? --maskDepth : ++maskDeficit; | |
| } | |
| if (d == last) | |
| { | |
| break; | |
| } | |
| } | |
| // Pull the start back over the openers whose closers we contain. | |
| while (clipDeficit > 0 || maskDeficit > 0) | |
| { | |
| Drawable* before = first->next; | |
| if (before == nullptr) | |
| { | |
| break; | |
| } | |
| if (maskDeficit > 0 && before->isMaskStart()) | |
| { | |
| --maskDeficit; | |
| } | |
| else if (clipDeficit > 0 && before->isClipStart()) | |
| { | |
| --clipDeficit; | |
| } | |
| else | |
| { | |
| break; | |
| } | |
| first = before; | |
| grew = true; | |
| } | |
| // Push the end forward over the closers whose openers we contain. | |
| while (clipDepth > 0 || maskDepth > 0) | |
| { | |
| Drawable* after = last->prev; | |
| if (after == nullptr) | |
| { | |
| break; | |
| } | |
| if (maskDepth > 0 && after->isMaskEnd()) | |
| { | |
| --maskDepth; | |
| } | |
| else if (clipDepth > 0 && after->isClipEnd()) | |
| { | |
| --clipDepth; | |
| } | |
| else | |
| { | |
| break; | |
| } | |
| last = after; | |
| grew = true; | |
| } | |
| if (!grew && (clipDeficit > 0 || maskDeficit > 0 || clipDepth > 0 || | |
| maskDepth > 0)) | |
| { | |
| // Could not balance: some bracket genuinely half-overlaps this | |
| // range, and no placement of our markers avoids crossing it. | |
| return false; | |
| } | |
| } | |
| // Contiguity. A draw rule can lift an unrelated drawable into the middle of | |
| // a subtree's span; masking it too would be silently wrong, and splitting | |
| // into two brackets would composite the mask twice. Refuse instead, and let | |
| // the caller draw the content unmasked. | |
| for (auto d = first;; d = d->prev) | |
| { | |
| if (!spansMember(d, memberSet)) | |
| { | |
| return false; | |
| } | |
| if (d == last) | |
| { | |
| break; | |
| } | |
| } | |
| auto* startMarker = mask->createProxyDrawable(startOp); | |
| auto* endMarker = mask->createProxyDrawable(endOp); | |
| // Insert startMarker immediately before `first` in draw order. | |
| Drawable* before = first->next; | |
| startMarker->next = before; | |
| startMarker->prev = first; | |
| if (before != nullptr) | |
| { | |
| before->prev = startMarker; | |
| } | |
| else | |
| { | |
| m_FirstDrawable = startMarker; | |
| } | |
| first->next = startMarker; | |
| // Insert endMarker immediately after `last` in draw order. | |
| Drawable* after = last->prev; | |
| endMarker->prev = after; | |
| endMarker->next = last; | |
| if (after != nullptr) | |
| { | |
| after->next = endMarker; | |
| } | |
| last->prev = endMarker; | |
| startMarker->pairedEnd(endMarker); | |
| if (startOut != nullptr) | |
| { | |
| *startOut = startMarker; | |
| } | |
| return true; | |
| } | |
| void Artboard::interleaveLayerMasks() | |
| { | |
| if (m_layerMasks.empty()) | |
| { | |
| return; | |
| } | |
| for (auto& mask : m_layerMasks) | |
| { | |
| mask->resetDrawables(); | |
| } | |
| for (auto& mask : m_layerMasks) | |
| { | |
| // A self-referential mask is inert: its source sits inside the range it | |
| // would rasterize, so bracketing it would recurse forever. | |
| if (mask->isSelfReferential()) | |
| { | |
| continue; | |
| } | |
| // Collect the drawables this mask applies to. Done per mask rather than | |
| // from one shared index because each splice shifts the list. | |
| std::vector<Drawable*> masked; | |
| for (auto d = m_FirstDrawable; d != nullptr; d = d->prev) | |
| { | |
| const auto& masks = d->layerMasks(); | |
| if (std::find(masks.begin(), masks.end(), mask) != masks.end()) | |
| { | |
| masked.push_back(d); | |
| } | |
| } | |
| LayerMaskProxyDrawable* start = nullptr; | |
| if (!spliceLayerMaskBracket(mask, | |
| masked, | |
| LayerMaskOp::maskStart, | |
| LayerMaskOp::maskEnd, | |
| &start)) | |
| { | |
| continue; | |
| } | |
| // Only bracket the source once the masked range is genuinely bracketed: | |
| // suppressing the source while the content draws unmasked would just | |
| // make the mask art vanish for nothing. | |
| LayerMaskProxyDrawable* sourceStart = nullptr; | |
| if (spliceLayerMaskBracket(mask, | |
| mask->sourceDrawables(), | |
| LayerMaskOp::sourceStart, | |
| LayerMaskOp::sourceEnd, | |
| &sourceStart)) | |
| { | |
| mask->sourceBracket(sourceStart, sourceStart->pairedEnd()); | |
| } | |
| } | |
| } | |
| // 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 | |
| Component* Artboard::componentForOrderEntry( | |
| const DependencyOrderEntry& entry) const | |
| { | |
| if (entry.objectIndex >= m_Objects.size()) | |
| { | |
| return nullptr; | |
| } | |
| Core* object = m_Objects[entry.objectIndex]; | |
| if (object == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| if (entry.helperSlot == 0) | |
| { | |
| return object->is<Component>() ? object->as<Component>() : nullptr; | |
| } | |
| // Slot 1 is the single owned helper for the types that have one. Add | |
| // further slots here if another Component gains an owned graph node. | |
| if (entry.helperSlot == 1) | |
| { | |
| if (object->is<Shape>()) | |
| { | |
| return object->as<Shape>()->pathComposer(); | |
| } | |
| if (object->is<TextStyle>()) | |
| { | |
| return object->as<TextStyle>()->variationHelper(); | |
| } | |
| } | |
| return nullptr; | |
| } | |
| const std::vector<Artboard::DependencyOrderEntry>* Artboard:: | |
| dependencyOrderRecipe() const | |
| { | |
| switch (m_RecipeState) | |
| { | |
| case RecipeState::valid: | |
| return &m_DependencyOrderRecipe; | |
| case RecipeState::unusable: | |
| return nullptr; | |
| case RecipeState::unbuilt: | |
| break; | |
| } | |
| // No reverse Component* -> index map is needed: sortDependencies() has | |
| // already stamped every component in m_DependencyOrder with its position | |
| // via m_GraphOrder, so one walk of m_Objects can scatter each entry | |
| // straight into its slot. That keeps recipe construction allocation-free | |
| // beyond the result vector, which matters because the source artboard is | |
| // re-decoded (and so re-builds this) on every editor regeneration. | |
| const size_t count = m_DependencyOrder.size(); | |
| constexpr uint32_t unset = ~(uint32_t)0; | |
| m_DependencyOrderRecipe.assign(count, {unset, 0}); | |
| size_t filled = 0; | |
| auto stamp = [&](const Component* component, uint32_t index, uint8_t slot) { | |
| if (component == nullptr) | |
| { | |
| return; | |
| } | |
| const unsigned int position = component->graphOrder(); | |
| // A component the sort never placed keeps Component's sentinel, which | |
| // no order can hand out. One that a *previous, longer* order placed | |
| // keeps that order's position, which this one may well have handed to | |
| // somebody else -- so the stamp alone is not proof. Confirm the order | |
| // really holds this component where it claims to be, which is an exact | |
| // membership test no stale stamp can pass. | |
| if (position >= count || m_DependencyOrder[position] != component) | |
| { | |
| return; | |
| } | |
| DependencyOrderEntry& entry = m_DependencyOrderRecipe[position]; | |
| if (entry.objectIndex != unset) | |
| { | |
| // One component reachable through two slots. Leave the first. | |
| return; | |
| } | |
| entry = {index, slot}; | |
| filled++; | |
| }; | |
| for (size_t i = 0; i < m_Objects.size(); i++) | |
| { | |
| Core* object = m_Objects[i]; | |
| if (object == nullptr || !object->is<Component>()) | |
| { | |
| continue; | |
| } | |
| const uint32_t index = (uint32_t)i; | |
| stamp(object->as<Component>(), index, 0); | |
| if (object->is<Shape>()) | |
| { | |
| stamp(object->as<Shape>()->pathComposer(), index, 1); | |
| } | |
| else if (object->is<TextStyle>()) | |
| { | |
| stamp(object->as<TextStyle>()->variationHelper(), index, 1); | |
| } | |
| } | |
| if (filled != count) | |
| { | |
| // Something in the order isn't addressable as index/slot (or the | |
| // graph-order stamps disagree). Don't emit a partial order. | |
| m_DependencyOrderRecipe.clear(); | |
| m_RecipeState = RecipeState::unusable; | |
| return nullptr; | |
| } | |
| m_RecipeState = RecipeState::valid; | |
| return &m_DependencyOrderRecipe; | |
| } | |
| bool Artboard::replaySourceDependencyOrder(const Artboard* source) | |
| { | |
| if (source == nullptr || source == this || | |
| source->m_Objects.size() != m_Objects.size()) | |
| { | |
| return false; | |
| } | |
| const std::vector<DependencyOrderEntry>* recipe = | |
| source->dependencyOrderRecipe(); | |
| if (recipe == nullptr) | |
| { | |
| return false; | |
| } | |
| std::vector<Component*> order; | |
| order.reserve(recipe->size()); | |
| for (const DependencyOrderEntry& entry : *recipe) | |
| { | |
| Component* component = componentForOrderEntry(entry); | |
| if (component == nullptr) | |
| { | |
| // A helper the source had but this instance doesn't (they are | |
| // lazily created). Fall back rather than emit a short order. | |
| return false; | |
| } | |
| order.push_back(component); | |
| } | |
| m_DependencyOrder = std::move(order); | |
| return true; | |
| } | |
| void Artboard::sortDependencies() | |
| { | |
| // An instance replays its source's order instead of re-walking the graph; | |
| // see DependencyOrderEntry. Falls back to sorting whenever anything about | |
| // the instance doesn't line up. | |
| bool replayed = replaySourceDependencyOrder(m_artboardSource); | |
| if (!replayed) | |
| { | |
| DependencySorter sorter; | |
| sorter.sort(this, m_DependencyOrder); | |
| } | |
| unsigned int graphOrder = 0; | |
| for (auto component : m_DependencyOrder) | |
| { | |
| component->m_GraphOrder = graphOrder++; | |
| } | |
| m_DependencyOrderRecipe.clear(); | |
| m_RecipeState = RecipeState::unbuilt; | |
| 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) | |
| { | |
| #ifdef TESTING | |
| sm_dirtNotifications++; | |
| #endif | |
| wakeIfQuietRow(); | |
| 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) | |
| { | |
| #ifdef TESTING | |
| sm_dirtNotifications++; | |
| #endif | |
| wakeIfQuietRow(); | |
| 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) | |
| { | |
| artboard->addDataBind(dataBind->cloneWithTarget(clone)); | |
| } | |
| } | |
| } | |
| void Artboard::syncInstanceValueBinds() | |
| { | |
| // Mid update the removes and adds would only queue, and a second change | |
| // in the same pass could not see them; coalesce until the pass drains. | |
| if (isProcessingDataBinds()) | |
| { | |
| m_instanceValueBindsPending = true; | |
| return; | |
| } | |
| auto instance = dataBindContext() != nullptr | |
| ? dataBindContext()->mainViewModelInstance() | |
| : nullptr; | |
| if (instance == m_instanceValueBindsSource) | |
| { | |
| return; | |
| } | |
| // Snapshot: removing mutates the list we are walking. | |
| auto previous = dataBinds(); | |
| for (auto dataBind : previous) | |
| { | |
| if (dataBind->isInstanceValueBind()) | |
| { | |
| removeAndDeleteDataBind(dataBind); | |
| } | |
| } | |
| m_instanceValueBindsSource = instance; | |
| if (instance == nullptr) | |
| { | |
| return; | |
| } | |
| for (auto dataBind : instance->valueDataBinds()) | |
| { | |
| auto dataBindClone = dataBind->cloneWithTarget(dataBind->target()); | |
| dataBindClone->markInstanceValueBind(); | |
| addDataBind(dataBindClone); | |
| } | |
| } | |
| void Artboard::mainViewModelInstanceChanged() | |
| { | |
| wakeIfQuietRow(); | |
| syncInstanceValueBinds(); | |
| } | |
| void Artboard::dataContextChanged() { wakeIfQuietRow(); } | |
| void Artboard::wakeQuietRow() | |
| { | |
| auto row = m_quietHostRow; | |
| m_quietHostRow = kNoQuietRow; | |
| if (m_host != nullptr) | |
| { | |
| m_host->hostedRowWoke(this, row); | |
| } | |
| } | |
| AdvancingComponent::QuietState Artboard::rowQuietState() | |
| { | |
| using QuietState = AdvancingComponent::QuietState; | |
| // Work this check can't see into: hosted artboards and lists, joysticks, | |
| // resettables and scripts. | |
| if (!m_ArtboardHosts.empty() || !m_Joysticks.empty() || | |
| !m_Resettables.empty() || !m_ScriptedObjects.empty()) | |
| { | |
| return QuietState::never; | |
| } | |
| // Cheapest first: a busy row is checked again every frame. | |
| if (hasDirt(ComponentDirt::Components) || !m_dirtyLayout.empty() || | |
| m_hostTransformMarkedDirty || m_instanceValueBindsPending || | |
| hasDataBindWork()) | |
| { | |
| return QuietState::busy; | |
| } | |
| auto state = QuietState::quiet; | |
| for (auto advancing : m_advancingComponents) | |
| { | |
| switch (advancing->quietState()) | |
| { | |
| case QuietState::never: | |
| return QuietState::never; | |
| case QuietState::busy: | |
| state = QuietState::busy; | |
| break; | |
| case QuietState::quiet: | |
| break; | |
| } | |
| } | |
| if (state == QuietState::quiet && mayAdvanceDataBinds()) | |
| { | |
| // Converters that advance can't say whether they would. | |
| return QuietState::never; | |
| } | |
| return state; | |
| } | |
| void Artboard::dataBindsProcessed() | |
| { | |
| if (m_instanceValueBindsPending) | |
| { | |
| m_instanceValueBindsPending = false; | |
| syncInstanceValueBinds(); | |
| } | |
| } | |
| 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) | |
| { | |
| wakeIfQuietRow(); | |
| // 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) | |
| { | |
| wakeIfQuietRow(); | |
| 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() | |
| { | |
| wakeIfQuietRow(); | |
| #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 | |
| #ifdef TESTING | |
| sm_layoutPassCount++; | |
| #endif | |
| 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::watermark(std::unique_ptr<Watermark> watermark) | |
| { | |
| m_watermark = std::move(watermark); | |
| } | |
| bool Artboard::advanceWatermark(float elapsedSeconds) | |
| { | |
| if (m_watermark == nullptr) | |
| { | |
| return false; | |
| } | |
| if (!m_watermark->advance(elapsedSeconds)) | |
| { | |
| m_watermark = nullptr; | |
| return false; | |
| } | |
| return true; | |
| } | |
| void Artboard::draw(Renderer* renderer) | |
| { | |
| sm_frameId++; | |
| // Nested artboards and component lists draw through drawInternal, so only a | |
| // top level draw can be diverted to the watermark. isPlaying() keeps an | |
| // artboard that is never advanced through a state machine (and so never | |
| // starts its watermark) drawing itself rather than freezing on a pre-roll | |
| // that would never end. | |
| if (m_watermark != nullptr && m_watermark->isPlaying()) | |
| { | |
| m_watermark->draw(renderer, bounds()); | |
| return; | |
| } | |
| // A standalone/root artboard is never cached as a bitmap: it is already the | |
| // top-level render target, and a host can skip drawing entirely via | |
| // didChange(). Only nested/instanced draws (which reach drawInternal | |
| // directly) participate in cache-as-bitmap. | |
| drawContent(renderer); | |
| } | |
| struct ModulatedDraw | |
| { | |
| uint32_t propertyKey; | |
| // Alpha, red, green, blue of the tags above, multiplied in float so | |
| // depth does not round. | |
| std::array<float, 4> level = {1.0f, 1.0f, 1.0f, 1.0f}; | |
| }; | |
| static void modulatedDrawVisitor(void* context, | |
| Drawable* drawable, | |
| Renderer* renderer) | |
| { | |
| auto modulated = static_cast<ModulatedDraw*>(context); | |
| auto property = drawable->customProperty(modulated->propertyKey); | |
| std::array<float, 4> own; | |
| if (property != nullptr && property->is<CustomPropertyNumber>()) | |
| { | |
| float value = | |
| math::clamp(property->as<CustomPropertyNumber>()->propertyValue(), | |
| 0.0f, | |
| 1.0f); | |
| own = {1.0f, value, value, value}; | |
| } | |
| else if (property != nullptr && property->is<CustomPropertyColor>()) | |
| { | |
| ColorInt color = property->as<CustomPropertyColor>()->propertyValue(); | |
| own = {colorOpacity(color), | |
| colorRed(color) / 255.0f, | |
| colorGreen(color) / 255.0f, | |
| colorBlue(color) / 255.0f}; | |
| } | |
| else | |
| { | |
| // Tagged, but not with this key. | |
| drawable->draw(renderer); | |
| return; | |
| } | |
| std::array<float, 4> outer = modulated->level; | |
| unsigned int channels[4]; | |
| for (size_t i = 0; i < own.size(); i++) | |
| { | |
| modulated->level[i] = outer[i] * own[i]; | |
| channels[i] = (unsigned int)std::lround(modulated->level[i] * 255.0f); | |
| } | |
| renderer->modulateColor( | |
| colorARGB(channels[0], channels[1], channels[2], channels[3]), | |
| true); | |
| drawable->draw(renderer); | |
| modulated->level = outer; | |
| } | |
| void Artboard::drawModulated(Renderer* renderer, | |
| uint32_t propertyKey, | |
| const File* keysFile) | |
| { | |
| ModulatedDraw modulated = {propertyKey}; | |
| drawInternal(renderer, modulatedDrawVisitor, &modulated, keysFile); | |
| } | |
| const File* Artboard::drawVisitorFile() const | |
| { | |
| return m_drawVisitorFile != nullptr ? m_drawVisitorFile | |
| : artboardFile().get(); | |
| } | |
| void Artboard::drawHosted(Artboard* hosted, Renderer* renderer) | |
| { | |
| DrawVisitor visitor = m_drawVisitor; | |
| if (visitor != nullptr) | |
| { | |
| // Name ids are per file, so a key means nothing in an artboard bound | |
| // in from another one. | |
| const File* file = drawVisitorFile(); | |
| const File* hostedFile = hosted->artboardFile().get(); | |
| if (hostedFile != nullptr && hostedFile != file) | |
| { | |
| visitor = nullptr; | |
| } | |
| hosted->m_drawVisitorFile = file; | |
| } | |
| hosted->drawInternal(renderer, visitor, m_drawVisitorContext); | |
| } | |
| void Artboard::drawInternal(Renderer* renderer, | |
| DrawVisitor visitor, | |
| void* visitorContext, | |
| const File* keysFile) | |
| { | |
| if (keysFile != nullptr) | |
| { | |
| m_drawVisitorFile = keysFile; | |
| } | |
| #ifdef RIVE_CANVAS | |
| // A cached bitmap has no drawables left to visit. | |
| if (visitor == nullptr && m_BitmapCache != nullptr && | |
| drawCachedAsBitmap(renderer)) | |
| { | |
| return; | |
| } | |
| #endif | |
| drawContent(renderer, visitor, visitorContext); | |
| } | |
| void Artboard::drawContent(Renderer* renderer, | |
| DrawVisitor visitor, | |
| void* visitorContext) | |
| { | |
| RIVE_PROF_SCOPE_L(1) | |
| m_didChange = false; | |
| if (childOpacity() == 0) | |
| { | |
| return; | |
| } | |
| // Hosted artboards read these while this draw runs. A draw started from | |
| // inside a visit hands them back, and none outlives its context. | |
| struct VisitorScope | |
| { | |
| Artboard* artboard; | |
| DrawVisitor visitor; | |
| void* context; | |
| ~VisitorScope() | |
| { | |
| artboard->m_drawVisitor = visitor; | |
| artboard->m_drawVisitorContext = context; | |
| } | |
| } visitorScope{this, m_drawVisitor, m_drawVisitorContext}; | |
| m_drawVisitor = visitor; | |
| m_drawVisitorContext = visitorContext; | |
| 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()); | |
| } | |
| drawDrawableRange(renderer, m_FirstDrawable, nullptr); | |
| if (save) | |
| { | |
| renderer->restore(); | |
| } | |
| } | |
| void Artboard::drawDrawableRange(Renderer* renderer, | |
| Drawable* first, | |
| Drawable* stop) | |
| { | |
| // 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. A layer mask's source bracket rides the | |
| // same counter to suppress the source in the normal pass. | |
| 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. | |
| // Deliberately a local: this function recurses through drawMasked. | |
| std::vector<Drawable*> pendingClipOperations; | |
| for (auto drawable = first; drawable != stop; 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(); | |
| } | |
| } | |
| if (drawable->isMaskStart()) | |
| { | |
| // Pending clips have already been flushed above, so the ancestor | |
| // clips are live on `renderer` and will constrain the composite. | |
| auto* startMarker = static_cast<LayerMaskProxyDrawable*>(drawable); | |
| Drawable* end = startMarker->pairedEnd(); | |
| if (drawMasked(renderer, startMarker, end)) | |
| { | |
| // Consumed: skip to the closing marker. `continue` runs the | |
| // for-increment, so the next iteration starts at end->prev. | |
| drawable = end; | |
| } | |
| // Otherwise fall through and let the range draw inline; the marker | |
| // itself draws nothing either way. | |
| continue; | |
| } | |
| if (drawable->isMaskEnd()) | |
| { | |
| continue; | |
| } | |
| if (m_drawVisitor != nullptr && drawable->hasCustomProperties()) | |
| { | |
| // Whatever the visitor sets on the renderer ends with the visit. | |
| renderer->save(); | |
| m_drawVisitor(m_drawVisitorContext, drawable, renderer); | |
| renderer->restore(); | |
| } | |
| else | |
| { | |
| drawable->draw(renderer); | |
| } | |
| } | |
| } | |
| #ifdef RIVE_CANVAS | |
| namespace | |
| { | |
| // How much to pad a drawable that reported `approximate` bounds. Generous on | |
| // purpose: the mask box is intersected with the artboard's own bounds, so the | |
| // worst case of an over-wide margin is a raster no bigger than the one this | |
| // whole change replaces -- while the worst case of an under-wide one is a hard | |
| // crop through the middle of someone's artwork. | |
| constexpr float kApproximateSlopDevicePx = 8.0f; | |
| constexpr float kApproximateSlopFraction = 0.25f; | |
| AABB padApproximate(const AABB& box, float rasterScale) | |
| { | |
| const float relative = | |
| kApproximateSlopFraction * std::max(box.width(), box.height()); | |
| const float absolute = rasterScale > 0.0f | |
| ? kApproximateSlopDevicePx / rasterScale | |
| : kApproximateSlopDevicePx; | |
| const float slop = std::max(relative, absolute); | |
| if (!std::isfinite(slop) || slop <= 0.0f) | |
| { | |
| return box; | |
| } | |
| return box.outset(slop, slop); | |
| } | |
| // Float AABB has no intersect helper, and AABB::overlaps is wrong (it compares | |
| // maxX against b.minY), so this is written out. False for an empty result. | |
| bool intersectBoxes(const AABB& a, const AABB& b, AABB* out) | |
| { | |
| const AABB r(std::max(a.left(), b.left()), | |
| std::max(a.top(), b.top()), | |
| std::min(a.right(), b.right()), | |
| std::min(a.bottom(), b.bottom())); | |
| if (r.isEmptyOrNaN()) | |
| { | |
| return false; | |
| } | |
| *out = r; | |
| return true; | |
| } | |
| } // namespace | |
| BoundsFidelity Artboard::rangeDrawBounds(Drawable* first, | |
| Drawable* stop, | |
| float rasterScale, | |
| AABB* out, | |
| bool* anyDrawn) const | |
| { | |
| // The skip rules mirror drawDrawableRange deliberately, which is what buys | |
| // the awkward cases for free: a drawable inside a nested empty clip is | |
| // excluded, and so is a nested mask's source range -- its markers push the | |
| // same empty-clip counter -- while a nested mask's own members are | |
| // included, which is correct because a mask only ever attenuates what is | |
| // already there. | |
| int emptyClips = 0; | |
| AABB accumulated = AABB::forExpansion(); | |
| BoundsFidelity worst = BoundsFidelity::exact; | |
| bool drew = false; | |
| for (auto* d = first; d != stop; d = d->prevDrawable()) | |
| { | |
| const int prevClips = emptyClips; | |
| emptyClips += d->emptyClipCount(); | |
| if (!d->willDraw() || emptyClips != prevClips || emptyClips > 0) | |
| { | |
| continue; | |
| } | |
| // Bracket markers paint nothing of their own: a clip only constrains | |
| // what follows it, and a mask marker is consumed by drawMasked. | |
| if (d->isClipStart() || d->isClipEnd() || d->isMaskStart() || | |
| d->isMaskEnd()) | |
| { | |
| continue; | |
| } | |
| drew = true; | |
| AABB painted; | |
| const BoundsFidelity fidelity = d->paintedWorldBounds(&painted); | |
| if (fidelity == BoundsFidelity::none) | |
| { | |
| // Not "pad it harder": there is no measurement to pad. Give up on a | |
| // tight box for this range rather than inventing one. | |
| *anyDrawn = true; | |
| return BoundsFidelity::none; | |
| } | |
| if (fidelity == BoundsFidelity::approximate) | |
| { | |
| worst = BoundsFidelity::approximate; | |
| painted = padApproximate(painted, rasterScale); | |
| } | |
| // A drawable can legitimately paint nothing -- a shape whose paths all | |
| // collapsed reports an empty box. Filtering here also keeps | |
| // AABB::forExpansion()'s +/-FLT_MAX sentinel out of the union. | |
| if (!painted.isEmptyOrNaN()) | |
| { | |
| accumulated.expand(painted); | |
| } | |
| } | |
| *anyDrawn = drew; | |
| if (!drew) | |
| { | |
| return BoundsFidelity::exact; | |
| } | |
| if (accumulated.isEmptyOrNaN()) | |
| { | |
| // Something drew but nothing measurable came of it. Rare, and not worth | |
| // reasoning about further: fall back. | |
| return BoundsFidelity::none; | |
| } | |
| *out = accumulated; | |
| return worst; | |
| } | |
| bool Artboard::drawMasked(Renderer* renderer, | |
| Drawable* startMarker, | |
| Drawable* endMarker) | |
| { | |
| LayerMask& mask = | |
| *static_cast<LayerMaskProxyDrawable*>(startMarker)->mask(); | |
| if (!mask.isVisible()) | |
| { | |
| return false; | |
| } | |
| // Re-entry guard, and defence in depth rather than a fix for anything | |
| // reachable today. | |
| // | |
| // onAddedClean rejects a mask whose source sits under its own parent, which | |
| // only catches a mask pointing at itself. Two masks naming each other's | |
| // subtrees pass that test, and the coverage pass draws a RANGE that | |
| // drawDrawableRange dispatches from, so in principle A could rasterize B | |
| // which rasterizes A without bound. | |
| // | |
| // In practice spliceLayerMaskBracket grows every range outward until it is | |
| // balanced with the brackets already in the list, so brackets nest and | |
| // never interleave: if B's members sit inside A's source bracket, B's range | |
| // grows to enclose the whole of A, which puts A's source range INSIDE B | |
| // rather than the other way round, and walking it never reaches B's start | |
| // marker. A mutual pair therefore terminates today -- there is a | |
| // [layer-mask] test pinning exactly that -- and this costs a bool and a | |
| // branch against the day the nesting invariant is relaxed, or a longer | |
| // cycle is reached some other way. | |
| // | |
| // Per mask, so legitimate nesting -- a different LayerMask inside a masked | |
| // subtree -- is unaffected. | |
| if (mask.m_isDrawing) | |
| { | |
| return false; | |
| } | |
| // Cleared however this returns, and there are many exits below. | |
| struct DrawingGuard | |
| { | |
| LayerMask& mask; | |
| DrawingGuard(LayerMask& m) : mask(m) { mask.m_isDrawing = true; } | |
| ~DrawingGuard() { mask.m_isDrawing = false; } | |
| } drawingGuard(mask); | |
| // canvasContentHost, not deferredCanvasHost: this only needs somewhere to | |
| // rasterize into. No host means nobody can give us an offscreen frame (a | |
| // plain non-GPU or test factory), so the content draws unmasked -- the | |
| // permanent fallback, and the reason a mask degrades rather than vanishing. | |
| Factory* f = factory(); | |
| auto* host = f ? f->canvasContentHost() : nullptr; | |
| if (host == nullptr) | |
| { | |
| return false; | |
| } | |
| // The same fallback, one step earlier, for a host that can rasterize but | |
| // cannot apply the mask. Renderer::applyLayerMask already no-ops there, but | |
| // finding out that late is too late: the rasters below are sized to the | |
| // content-and-coverage intersection, so the layer would come back unmasked | |
| // and *cropped* to a box that only made sense if the mask had been applied. | |
| // Bailing here draws the range inline -- unmasked for real, and two | |
| // canvases and a composite cheaper. | |
| if (!host->supportsLayerMask()) | |
| { | |
| return false; | |
| } | |
| // The raster scale before anything is measured: it depends only on the | |
| // renderer's transform, and it is the unit the slop margin for an | |
| // approximately-bounded drawable is expressed against. | |
| offscreen::RasterPlan plan; | |
| if (!offscreen::planRasterScale(renderer, mask.resolution(), &plan)) | |
| { | |
| return false; | |
| } | |
| // mask.m_dirty alone. LayerMask sits on the dependency graph as a dependent | |
| // of everything in its own range and its own source, so its update() raises | |
| // this when -- and only when -- something it actually rasterizes changed. | |
| // | |
| // This used to also read the artboard-wide "something moved" flag, which | |
| // meant every mask re-rasterized both of its canvases whenever anything | |
| // anywhere in the artboard moved, however unrelated. That flag was captured | |
| // into a separate field just for this read, and both are now gone. | |
| const bool contentChanged = mask.m_contentCanvas == nullptr || | |
| mask.m_maskCanvas == nullptr || mask.m_dirty; | |
| AABB tight; | |
| if (contentChanged) | |
| { | |
| // Measuring both ranges also answers "would anything draw at all", | |
| // which is what the two cheap probes that used to live here did on | |
| // their own. | |
| bool contentDraws = false; | |
| AABB contentBox; | |
| const BoundsFidelity contentFidelity = | |
| rangeDrawBounds(startMarker->prevDrawable(), | |
| endMarker, | |
| plan.rasterScale, | |
| &contentBox, | |
| &contentDraws); | |
| if (!contentDraws) | |
| { | |
| // Nothing in the range would have drawn, so consume the bracket | |
| // rather than rasterizing two empty textures. | |
| return true; | |
| } | |
| bool coverageDraws = false; | |
| AABB sourceBox; | |
| BoundsFidelity sourceFidelity = BoundsFidelity::none; | |
| if (mask.sourceStart() == nullptr) | |
| { | |
| // No source bracket. Two very different situations arrive here and | |
| // the mode-switch below is only right for one of them. | |
| // | |
| // interleaveLayerMasks splices the masked range first and the | |
| // source second, and the second can fail on its own -- a draw rule | |
| // can scatter the source subtree so it is not contiguous. The | |
| // masked bracket is already in the list by then, so this mask is | |
| // live with no source bracket, and the source subtree is NOT | |
| // suppressed: it draws in its natural position. Treating coverage | |
| // as absent would erase the whole masked range in alpha/luminance | |
| // while the mask art itself stayed on screen -- the content gone | |
| // and the mask visible, which is the worst of both. | |
| // | |
| // So only take the empty-coverage path when the source genuinely | |
| // draws nothing. If anything in it would draw, the bracket is the | |
| // thing that is missing, and the mask goes inert: return false and | |
| // the range draws inline, unmasked. | |
| for (Drawable* d : mask.sourceDrawables()) | |
| { | |
| if (d->willDraw()) | |
| { | |
| return false; | |
| } | |
| } | |
| } | |
| else | |
| { | |
| // Over the source bracket rather than sourceDrawables(), matching | |
| // what the coverage pass below actually draws -- so a clipping | |
| // shape nested inside the source is accounted for the same way. | |
| sourceFidelity = rangeDrawBounds(mask.sourceStart()->prevDrawable(), | |
| mask.sourceEnd(), | |
| plan.rasterScale, | |
| &sourceBox, | |
| &coverageDraws); | |
| } | |
| if (!coverageDraws) | |
| { | |
| // Coverage is zero everywhere -- the mask source is hidden, or sits | |
| // in an inactive solo. Which way that falls depends on the mode, | |
| // and both answers avoid the GPU entirely. | |
| switch (mask.maskMode()) | |
| { | |
| case MaskMode::alpha: | |
| case MaskMode::luminance: | |
| return true; // content fully masked out | |
| case MaskMode::invertedAlpha: | |
| case MaskMode::invertedLuminance: | |
| return false; // content fully visible; draw it inline | |
| } | |
| } | |
| // One box and one scale for both canvases, so a content texel and a | |
| // coverage texel at the same coordinate correspond with no resampling | |
| // and no second transform. | |
| bool authoredBox = false; | |
| const AABB authored = mask.customBounds(); | |
| if (mask.useCustomBounds() && !authored.isEmptyOrNaN()) | |
| { | |
| // The author drew this rectangle, in artboard space. Taken | |
| // verbatim: no slop margin, and deliberately no intersect with the | |
| // artboard box either, because cropping a mask to a region that | |
| // reaches outside the artboard is intent rather than an error. | |
| // kMaxDim still caps it. | |
| // | |
| // The ranges above were still walked, which is what keeps the | |
| // "nothing draws" and "no coverage" shortcuts working; only the box | |
| // comes from here. The mode-aware intersect is skipped on purpose | |
| // -- the author has already said which region matters. | |
| // | |
| // A degenerate rectangle -- zero or negative extent, which is what | |
| // a half-drawn one looks like -- falls through to measuring rather | |
| // than collapsing the raster and dropping the layer. | |
| tight = authored; | |
| authoredBox = true; | |
| } | |
| else if (contentFidelity == BoundsFidelity::none) | |
| { | |
| // Something in the range could not say where it paints. Fall back | |
| // to the box that is always big enough. | |
| tight = bounds(); | |
| } | |
| else | |
| { | |
| tight = contentBox; | |
| // In the un-inverted modes the coverage texel is zero outside the | |
| // source, so content out there is erased anyway: intersecting costs | |
| // nothing and it is where the win lives, because a small mask over | |
| // a full-stage layer now rasterizes only the small mask. The | |
| // inverted modes are the opposite -- absent coverage means a factor | |
| // of 1, so the content box has to stand on its own. | |
| const bool intersectsSource = | |
| mask.maskMode() == MaskMode::alpha || | |
| mask.maskMode() == MaskMode::luminance; | |
| if (sourceFidelity != BoundsFidelity::none) | |
| { | |
| // sourceBox is a superset of where coverage actually lands, so | |
| // "these do not overlap" is a claim that holds: no coverage | |
| // reaches the content at all. | |
| AABB overlap; | |
| const bool overlaps = | |
| intersectBoxes(tight, sourceBox, &overlap); | |
| if (intersectsSource) | |
| { | |
| if (!overlaps) | |
| { | |
| // Every content texel would be multiplied by zero | |
| // coverage. Consume the bracket and draw nothing at | |
| // all. | |
| return true; | |
| } | |
| tight = overlap; | |
| } | |
| else if (!overlaps) | |
| { | |
| // Inverted, so absent coverage means a factor of one: the | |
| // layer is fully visible. Decline the bracket and let it | |
| // draw inline, exactly as a source that draws nothing at | |
| // all does. | |
| return false; | |
| } | |
| } | |
| } | |
| // Clamped to the artboard only when the artboard actually clips. | |
| // | |
| // When it does, content reaching past the edge is cropped at composite | |
| // time anyway, so rasterizing it would be wasted texels -- and the | |
| // clamp keeps the raster no larger than the artboard-sized one this | |
| // replaced, which is what makes the common case monotone: kMaxDim's | |
| // coarsening can only bite less than before, never more. | |
| // | |
| // When it does not clip, that content is genuinely visible, and | |
| // clamping would crop it inside the mask while it draws fine everywhere | |
| // else. The artboard-sized rasters had that bug too; this is where it | |
| // is fixed. | |
| // | |
| // The cost is that the monotone guarantee does not hold for an | |
| // unclipped artboard whose content sprawls far past it: a bigger box | |
| // can hit kMaxDim and coarsen. Blurrier beats cropped. | |
| // | |
| // Skipped entirely for an authored box, where reaching outside the | |
| // artboard is a decision rather than an accident. | |
| if (!authoredBox && clip() && !intersectBoxes(tight, bounds(), &tight)) | |
| { | |
| return false; | |
| } | |
| } | |
| else | |
| { | |
| // Nothing changed, so re-measuring would land in the same place. Re-fit | |
| // the box that was measured, never the box that was fitted -- feeding a | |
| // fitted box back through the guard band would grow the raster a few | |
| // texels on every frame. | |
| tight = mask.m_tightBox; | |
| } | |
| // A guard band of transparent texels on every side. Without it the content | |
| // reaches the raster edge, and the composite's LinearClamp then replicates | |
| // that edge texel outward under rotation: a one-texel smear along the seam. | |
| // It is added after the artboard clamp above, so the band can sit a couple | |
| // of texels outside the artboard -- where the composite is itself clipped | |
| // when the artboard clips, and where showing those texels is the more | |
| // correct answer when it does not. | |
| constexpr uint32_t kGuardTexels = 2; | |
| if (!offscreen::fitRasterToBox(tight, | |
| kGuardTexels, | |
| offscreen::RasterFit::stableGrid, | |
| &plan)) | |
| { | |
| return false; | |
| } | |
| // Growth is immediate, shrinking waits. Anything rotating or scaling | |
| // changes the size of its own bounding box every frame, and re-allocating | |
| // two GPU textures that often costs far more than carrying one bucket of | |
| // slack. | |
| const offscreen::RasterResize resize = | |
| offscreen::decideRasterResize(plan.widthPx, | |
| plan.heightPx, | |
| mask.m_widthPx, | |
| mask.m_heightPx, | |
| &mask.m_shrinkStreak); | |
| // Hold whatever allocation was settled on, growing the box right and bottom | |
| // to span it so the composite's inverse still maps exactly onto the pixels. | |
| offscreen::holdRasterAllocation(resize.widthPx, resize.heightPx, &plan); | |
| // New textures hold nothing, and the cap inside fitRasterToBox can lower | |
| // the scale out from under a cached raster, so either forces a rebuild even | |
| // on a frame that had decided it could reuse what it had. | |
| const bool mustRaster = contentChanged || resize.reallocate || | |
| plan.rasterScale != mask.m_rasterScale; | |
| if (mustRaster) | |
| { | |
| if (resize.reallocate || mask.m_contentCanvas == nullptr || | |
| mask.m_maskCanvas == nullptr) | |
| { | |
| mask.m_contentCanvas = | |
| host->makeContentCanvas(plan.widthPx, plan.heightPx); | |
| mask.m_maskCanvas = | |
| host->makeContentCanvas(plan.widthPx, plan.heightPx); | |
| #ifdef TESTING | |
| mask.testAllocations++; | |
| #endif | |
| } | |
| if (mask.m_contentCanvas == nullptr || mask.m_maskCanvas == nullptr) | |
| { | |
| mask.releaseCanvases(); | |
| return false; | |
| } | |
| // Sequentially, never nested: DeferredSession::endCanvasContent routes | |
| // back to the screen rather than to an enclosing canvas, so an inner | |
| // bracket closing inside an outer one would misroute the rest of the | |
| // outer content. Finishing the coverage canvas before opening the | |
| // content one keeps the recorded stream two independent segments. | |
| bool opened = true; | |
| { | |
| offscreen::CanvasContentScope scope(host, | |
| mask.m_maskCanvas.get(), | |
| plan, | |
| 0); | |
| if (Renderer* r = scope.renderer()) | |
| { | |
| // Over the source bracket, not sourceDrawables() directly, so a | |
| // clipping shape nested inside the source still brackets its | |
| // own drawables. Starting past the opening marker keeps its | |
| // own +1 suppression from hiding the very range we want. | |
| drawDrawableRange(r, | |
| mask.sourceStart()->prevDrawable(), | |
| mask.sourceEnd()); | |
| } | |
| else | |
| { | |
| opened = false; | |
| } | |
| } | |
| if (opened) | |
| { | |
| offscreen::CanvasContentScope scope(host, | |
| mask.m_contentCanvas.get(), | |
| plan, | |
| 0); | |
| if (Renderer* r = scope.renderer()) | |
| { | |
| drawDrawableRange(r, startMarker->prevDrawable(), endMarker); | |
| // The mask apply has to be the last draw into this canvas: it | |
| // multiplies everything already there, so anything drawn after | |
| // it would land unmasked. Issued in the canvas's own pixel | |
| // space (the scope's transform is still in effect, so undo it) | |
| // because the coverage texture is the same size as the target | |
| // and has to line up texel for texel. | |
| if (rcp<RenderImage> coverage = | |
| host->contentCanvasImage(mask.m_maskCanvas.get())) | |
| { | |
| r->save(); | |
| const float inv = 1.0f / plan.rasterScale; | |
| r->translate(plan.box.left(), plan.box.top()); | |
| r->transform(Mat2D::fromScale(inv, inv)); | |
| r->applyLayerMask( | |
| coverage.get(), | |
| ImageSampler::LinearClamp(), | |
| static_cast<LayerMaskMode>(mask.maskMode())); | |
| r->restore(); | |
| } | |
| } | |
| else | |
| { | |
| opened = false; | |
| } | |
| } | |
| if (!opened) | |
| { | |
| // The host could not open an offscreen frame. Nothing was drawn, so | |
| // drop the canvases and stay dirty: a later frame retries, and in | |
| // the meantime the content draws unmasked rather than as a hole. | |
| mask.releaseCanvases(); | |
| mask.m_dirty = true; | |
| return false; | |
| } | |
| // Commit what the pixels now are. m_tightBox is what a later frame | |
| // re-fits from; m_box is where these pixels live, which is what the | |
| // composite has to place them at even after the plan moves on. | |
| mask.m_tightBox = tight; | |
| mask.m_box = plan.box; | |
| mask.m_widthPx = plan.widthPx; | |
| mask.m_heightPx = plan.heightPx; | |
| mask.m_rasterScale = plan.rasterScale; | |
| mask.m_dirty = false; | |
| } | |
| rcp<RenderImage> contentImage = | |
| host->contentCanvasImage(mask.m_contentCanvas.get()); | |
| rcp<RenderImage> coverageImage = | |
| host->contentCanvasImage(mask.m_maskCanvas.get()); | |
| if (contentImage == nullptr || coverageImage == nullptr) | |
| { | |
| return false; | |
| } | |
| // mask.m_box, not plan.box: on a frame that reused the cached rasters the | |
| // plan was re-fitted from scratch, and the pixels being composited belong | |
| // to whatever box they were rasterized for. | |
| auto placement = offscreen::beginComposite(renderer, | |
| host, | |
| plan, | |
| mask.m_rasterScale, | |
| mask.m_box); | |
| placement.renderer->drawImage(contentImage.get(), | |
| ImageSampler::LinearClamp(), | |
| BlendMode::srcOver, | |
| placement.opacity); | |
| offscreen::endComposite(placement); | |
| return true; | |
| } | |
| #else | |
| bool Artboard::drawMasked(Renderer*, Drawable*, Drawable*) { return false; } | |
| #endif | |
| #ifdef RIVE_CANVAS | |
| bool Artboard::drawCachedAsBitmap(Renderer* renderer) | |
| { | |
| // Only reached when m_BitmapCache != nullptr (guarded in drawInternal). | |
| BitmapCache& cache = *m_BitmapCache; | |
| if (!cache.cacheEnabled()) | |
| { | |
| // Authored off (or animated/bound off): behave as though the artboard | |
| // had no BitmapCache at all. The texture is released by | |
| // BitmapCache::cacheFlagsChanged when the bit clears. | |
| return false; | |
| } | |
| if (childOpacity() == 0.0f) | |
| { | |
| // Fully transparent: nothing to draw, and no need to (re)rasterize. | |
| // The pending change still has to be consumed the way drawContent() | |
| // would have on the vector path -- a host that gates frame submission | |
| // on didChange() (Unity does) otherwise resubmits this invisible | |
| // artboard every frame, forever. Fold it into the cache rather than | |
| // dropping it, so content that moved while it was transparent still | |
| // re-rasterizes when it becomes visible again instead of compositing | |
| // the raster it had before. | |
| cache.m_dirty = cache.m_dirty || didChange(); | |
| m_didChange = false; | |
| return true; | |
| } | |
| Factory* f = factory(); | |
| // canvasContentHost, not deferredCanvasHost: this only needs somewhere to | |
| // rasterize into, and an immediate renderer can provide that without | |
| // claiming its content is being recorded. The host also allocates the | |
| // canvas, so nothing here has to go looking for a device. | |
| auto* deferredHost = f ? f->canvasContentHost() : nullptr; | |
| // No host means nobody can give us an offscreen frame (a plain non-GPU or | |
| // test factory), so fall back to a normal vector draw. | |
| if (deferredHost == nullptr) | |
| { | |
| return false; | |
| } | |
| // The artboard's own box, which is not [0,0,width,height] in general: with | |
| // frameOrigin off, bounds() is offset by the origin -- and NestedArtboard | |
| // turns frameOrigin off on everything it hosts, which is the only way to | |
| // reach this path. Rasterizing [0,0,w,h] there would capture the wrong | |
| // region and composite it in the wrong place. bounds() also tracks the | |
| // resolved layout size rather than the authored width/height. | |
| const AABB box = bounds(); | |
| const float w = box.width(); | |
| const float h = box.height(); | |
| if (w <= 0.0f || h <= 0.0f) | |
| { | |
| return false; | |
| } | |
| // drawContent applies the artboard's own rotation/scale, so a self | |
| // transform ends up baked into the raster -- while the target and the | |
| // composite below are both sized and placed from the untransformed box. | |
| // A scaled artboard would be cropped to its original extent, and a rotated | |
| // one flattened into an axis-aligned texture whose footprint no longer | |
| // matches what the vector path draws. Caching that correctly needs the | |
| // target sized from the transformed bounds and the composite placed by the | |
| // same transform; until then these artboards draw as vectors. | |
| if (hasSelfTransform()) | |
| { | |
| return false; | |
| } | |
| // Chooses the raster size from the scale the artboard is actually viewed | |
| // at, and captures the CTM and modulated opacity the composite below needs. | |
| // See offscreen_raster.hpp for why each of those has to be read here, | |
| // while recording, rather than at replay. | |
| offscreen::RasterPlan plan; | |
| if (!offscreen::planRaster(renderer, box, cache.resolution(), &plan)) | |
| { | |
| return false; | |
| } | |
| // Rebuild when there is no cache, it was explicitly invalidated (resolution | |
| // changed), the target size or raster scale changed (artboard resized, or | |
| // the artboard is being viewed at a different zoom), or the content changed | |
| // this frame. | |
| bool geomChanged = plan.widthPx != cache.m_widthPx || | |
| plan.heightPx != cache.m_heightPx || | |
| plan.rasterScale != cache.m_rasterScale; | |
| if (cache.m_canvas == nullptr || cache.m_dirty || geomChanged || | |
| didChange()) | |
| { | |
| renderIntoCanvas(deferredHost, plan); | |
| } | |
| if (cache.m_canvas == nullptr) | |
| { | |
| return false; // allocation failed; fall back to vector draw. | |
| } | |
| // Composite the cached texture in place of the vector content. The | |
| // renderer's CTM already includes the mount transform; drawImage() spans | |
| // (widthPx, heightPx), so undoing the raster's fit -- scale back to the | |
| // box, then move it to the box's corner -- maps a local point (lx,ly) to | |
| // CTM * (lx,ly), pixel-exact with the vector path. | |
| // The artboard's own opacity is already baked into the raster (host/render | |
| // opacity propagate to children and invalidate the cache via didChange()), | |
| // so the only opacity left to apply is an enclosing modulateOpacity() | |
| // scope -- and only on the fresh-renderer path, which does not inherit it. | |
| // Not necessarily the canvas's own image: some backends cannot sample | |
| // their canvas textures directly and stand in a companion for it. | |
| rcp<RenderImage> image = | |
| deferredHost->contentCanvasImage(cache.m_canvas.get()); | |
| if (image == nullptr) | |
| { | |
| return false; | |
| } | |
| // Picks the renderer to composite through, saves, pixel snaps, and maps the | |
| // raster's pixel span back onto the box. m_rasterScale, not | |
| // plan.rasterScale: the raster being composited may have been built on an | |
| // earlier frame, and the mapping has to invert the scale it was actually | |
| // drawn at. | |
| auto placement = offscreen::beginComposite(renderer, | |
| deferredHost, | |
| plan, | |
| cache.m_rasterScale); | |
| placement.renderer->drawImage(image.get(), | |
| ImageSampler::LinearClamp(), | |
| BlendMode::srcOver, | |
| placement.opacity); | |
| offscreen::endComposite(placement); | |
| return true; | |
| } | |
| void Artboard::renderIntoCanvas(cmd::DeferredCanvasHost* deferredHost, | |
| const offscreen::RasterPlan& plan) | |
| { | |
| BitmapCache& cache = *m_BitmapCache; | |
| // Reuse the texture whenever it is already the right size. This runs on | |
| // every frame the artboard changes, and on an immediate host each of these | |
| // is a real GPU allocation -- re-minting one per frame both defeats the | |
| // point of a cache and thrashes the driver. | |
| if (cache.m_canvas == nullptr || cache.m_widthPx != plan.widthPx || | |
| cache.m_heightPx != plan.heightPx) | |
| { | |
| // The host decides whether this needs real pixels now or can defer | |
| // them to whoever replays. | |
| cache.m_canvas = | |
| deferredHost->makeContentCanvas(plan.widthPx, plan.heightPx); | |
| } | |
| if (cache.m_canvas == nullptr) | |
| { | |
| return; | |
| } | |
| // The scope opens the content bracket and applies the raster transform; | |
| // drawContent (not drawInternal) so we never re-enter the cache hook. | |
| // clearColor is transparent black. | |
| bool opened = false; | |
| { | |
| offscreen::CanvasContentScope scope(deferredHost, | |
| cache.m_canvas.get(), | |
| plan, | |
| 0); | |
| if (Renderer* r = scope.renderer()) | |
| { | |
| opened = true; | |
| drawContent(r); | |
| } | |
| } | |
| if (!opened) | |
| { | |
| // The host could not open an offscreen frame (an unbacked canvas, or a | |
| // host that cannot nest one). Nothing was drawn, so the canvas holds | |
| // whatever it held before -- uninitialized on the first attempt. Drop | |
| // it rather than end a bracket that never began: the caller's null | |
| // check then takes the vector path, and leaving the cache dirty means | |
| // a later frame retries instead of compositing this hole forever. | |
| cache.m_canvas.reset(); | |
| cache.m_widthPx = 0; | |
| cache.m_heightPx = 0; | |
| cache.m_dirty = true; | |
| return; | |
| } | |
| cache.m_widthPx = plan.widthPx; | |
| cache.m_heightPx = plan.heightPx; | |
| cache.m_rasterScale = plan.rasterScale; | |
| cache.m_dirty = false; | |
| } | |
| #endif | |
| 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) | |
| { | |
| // Set the context before recursing into the artboard hosts; they read it | |
| // back off this artboard while they bind. The binds are walked after. | |
| dataBindContext(value); | |
| syncInstanceValueBinds(); | |
| for (auto artboardHost : m_ArtboardHosts) | |
| { | |
| auto hostValue = | |
| value->getViewModelInstance(artboardHost->dataBindPath()); | |
| if (hostValue != nullptr && hostValue->is<ViewModelInstance>()) | |
| { | |
| artboardHost->bindViewModelInstance(hostValue, value); | |
| } | |
| else | |
| { | |
| artboardHost->internalDataContext(value); | |
| } | |
| } | |
| bindDataBindsFromContext(); | |
| sortDataBinds(); | |
| for (auto* scriptedObject : m_ScriptedObjects) | |
| { | |
| scriptedObject->dataContext(value); | |
| } | |
| initScriptedObjects(); | |
| } | |
| void Artboard::rebind() { internalDataContext(dataBindContext()); } | |
| void Artboard::relinkDataContext() | |
| { | |
| wakeIfQuietRow(); | |
| if (dataBindContext() == nullptr) | |
| { | |
| return; | |
| } | |
| for (auto artboardHost : m_ArtboardHosts) | |
| { | |
| rcp<ViewModelInstance> value = dataBindContext()->getViewModelInstance( | |
| artboardHost->dataBindPath()); | |
| if (value == nullptr) | |
| { | |
| value = dataBindContext()->mainViewModelInstance(); | |
| } | |
| artboardHost->relinkDataContext(value); | |
| } | |
| } | |
| void Artboard::rebuildDataBind(DataBind* dataBind) | |
| { | |
| if (dataBind->is<DataBindContext>()) | |
| { | |
| dataBind->as<DataBindContext>()->bindFromContext( | |
| dataBindContext().get()); | |
| } | |
| }; | |
| void Artboard::unbind() | |
| { | |
| clearDataContext(); | |
| unbindDataBinds(); | |
| for (auto artboardHost : m_ArtboardHosts) | |
| { | |
| artboardHost->unbind(); | |
| } | |
| } | |
| void Artboard::clearDataContext() | |
| { | |
| if (dataBindContext() != nullptr) | |
| { | |
| dataBindContext()->removeDependentContainer(this); | |
| dataBindContext(nullptr); | |
| syncInstanceValueBinds(); | |
| } | |
| 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 && dataBindContext() != nullptr) | |
| { | |
| dataBindContext()->parent(parent); | |
| } | |
| bind(); | |
| } | |
| void Artboard::setViewModelInstance(rcp<ViewModelInstance> viewModelInstance) | |
| { | |
| if (viewModelInstance == nullptr) | |
| { | |
| return; | |
| } | |
| if (dataBindContext() == nullptr) | |
| { | |
| dataBindContext(make_rcp<DataContext>(viewModelInstance)); | |
| dataBindContext()->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. | |
| dataBindContext()->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 (dataBindContext() != nullptr) | |
| { | |
| internalDataContext(dataBindContext()); | |
| } | |
| } | |
| 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 (dataBindContext() == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| auto f = artboardFile(); | |
| if (f == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| return dataBindContext()->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 (dataBindContext() == nullptr) | |
| { | |
| // Nothing to clear when there is no context yet; only create one when | |
| // actually placing an instance. | |
| if (viewModelInstance == nullptr) | |
| { | |
| return true; | |
| } | |
| dataBindContext(make_rcp<DataContext>(rcp<ViewModelInstance>(nullptr))); | |
| dataBindContext()->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). | |
| dataBindContext()->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 |