| #include <atomic> | |
| #include <cmath> | |
| #include "rive/animation/animation_reset.hpp" | |
| #include "rive/animation/animation_reset_factory.hpp" | |
| #include "rive/animation/animation_state_instance.hpp" | |
| #include "rive/animation/animation_state.hpp" | |
| #include "rive/animation/any_state.hpp" | |
| #include "rive/animation/keyframe_interpolator.hpp" | |
| #include "rive/animation/keyed_object.hpp" | |
| #ifdef TESTING | |
| #include "rive/animation/keyframe_bool.hpp" | |
| #include "rive/animation/keyframe_color.hpp" | |
| #include "rive/animation/keyframe_double.hpp" | |
| #include "rive/animation/keyframe_id.hpp" | |
| #include "rive/animation/keyframe_int.hpp" | |
| #include "rive/animation/keyframe_string.hpp" | |
| #include "rive/animation/keyframe_uint.hpp" | |
| #include "rive/generated/core_registry.hpp" | |
| #endif | |
| #include "rive/animation/keyed_property.hpp" | |
| #include "rive/animation/linear_animation.hpp" | |
| #include "rive/animation/linear_animation_instance.hpp" | |
| #include "rive/data_bind/bindable_property_number.hpp" | |
| #include "rive/data_bind/converters/data_converter.hpp" | |
| #include "rive/animation/entry_state.hpp" | |
| #include "rive/animation/exit_state.hpp" | |
| #include "rive/animation/layer_state_flags.hpp" | |
| #include "rive/animation/nested_linear_animation.hpp" | |
| #include "rive/animation/nested_state_machine.hpp" | |
| #include "rive/animation/scripted_transition_condition.hpp" | |
| #include "rive/animation/state_instance.hpp" | |
| #include "rive/animation/state_machine_bool.hpp" | |
| #include "rive/animation/state_machine_input_instance.hpp" | |
| #include "rive/animation/state_machine_input.hpp" | |
| #include "rive/animation/state_machine_instance.hpp" | |
| #include "rive/animation/state_machine_instance_clusters.hpp" | |
| #include "rive/animation/state_machine_layer.hpp" | |
| #include "rive/animation/listener_invocation.hpp" | |
| #include "rive/animation/state_machine_listener.hpp" | |
| #include "rive/animation/state_machine_listener_single.hpp" | |
| #include "rive/animation/state_machine_number.hpp" | |
| #include "rive/animation/state_machine_trigger.hpp" | |
| #include "rive/animation/state_machine.hpp" | |
| #include "rive/animation/state_transition.hpp" | |
| #include "rive/animation/listener_action.hpp" | |
| #include "rive/animation/listener_types/listener_input_type_viewmodel.hpp" | |
| #include "rive/animation/scripted_listener_action.hpp" | |
| #include "rive/animation/transition_condition.hpp" | |
| #include "rive/animation/transition_comparator.hpp" | |
| #include "rive/animation/transition_property_viewmodel_comparator.hpp" | |
| #include "rive/animation/transition_viewmodel_condition.hpp" | |
| #include "rive/animation/state_machine_fire_event.hpp" | |
| #include "rive/viewmodel/viewmodel_instance_trigger.hpp" | |
| #include "rive/artboard_component_list.hpp" | |
| #include "rive/constraints/draggable_constraint.hpp" | |
| #include "rive/data_bind/data_bind_context.hpp" | |
| #include "rive/data_bind/data_bind.hpp" | |
| #include "rive/data_bind/context/context_value.hpp" | |
| #include "rive/data_bind/data_values/data_value_number.hpp" | |
| #include "rive/data_bind_flags.hpp" | |
| #include "rive/event_report.hpp" | |
| #include "rive/hit_result.hpp" | |
| #include "rive/listener_group.hpp" | |
| #include "rive/math/aabb.hpp" | |
| #include "rive/math/random.hpp" | |
| #include "rive/math/hit_test.hpp" | |
| #include "rive/nested_animation.hpp" | |
| #include "rive/nested_artboard.hpp" | |
| #include "rive/process_event_result.hpp" | |
| #include "rive/scripted/scripted_drawable.hpp" | |
| #include "rive/scripted/scripted_interpolator.hpp" | |
| #include "rive/shapes/image.hpp" | |
| #include "rive/shapes/shape.hpp" | |
| #include "rive/text/text.hpp" | |
| #include "rive/math/math_types.hpp" | |
| #include "rive/audio_event.hpp" | |
| #include "rive/dirtyable.hpp" | |
| #include "rive/profiler/profiler_macros.h" | |
| #include "rive/text/text_input.hpp" | |
| #include "rive/refcnt.hpp" | |
| #include "rive/animation/focus_listener_group.hpp" | |
| #include "rive/animation/text_input_listener_group.hpp" | |
| #include "rive/animation/listener_types/listener_input_type_event.hpp" | |
| #include "rive/focus_data.hpp" | |
| #include "rive/node.hpp" | |
| #include "rive/semantic/semantic_data.hpp" | |
| #include "rive/view_model_type.hpp" | |
| #include "rive/viewmodel/viewmodel.hpp" | |
| #include "rive/file.hpp" | |
| #include "rive/data_bind/data_context.hpp" | |
| #include "rive/viewmodel/write_attribution.hpp" | |
| #include <algorithm> | |
| #include <array> | |
| #include <functional> | |
| #include <limits> | |
| #include <memory> | |
| #include <unordered_map> | |
| #include <vector> | |
| #include <chrono> | |
| using namespace rive; | |
| // ArtboardComponentList builds one StateMachineInstance per row, so a 1000-row | |
| // list pays sizeof(StateMachineInstance) a thousand times over before any | |
| // content exists. The clusters in state_machine_instance_clusters.hpp exist to | |
| // keep it small: 1080 B before that work, 368 B after, which the allocator | |
| // rounds to 384 instead of 1280. | |
| // | |
| // Before adding an inline member, check whether it belongs in one of the | |
| // SMI* sidecar clusters instead — anything that is only populated for a | |
| // specific authored feature (events, bindables, focus/keyboard/gamepad/ | |
| // semantics, scripting) does. Note also that nothing inline here is a | |
| // std::unordered_map any more, which is what makes this type the same size on | |
| // libc++ and libstdc++; an inline hash container would give that up. | |
| #ifdef RIVE_MICROPROFILE | |
| #include "rive/profiler/rive_profile.hpp" | |
| static std::string getStateName(const StateInstance* stateInstance) | |
| { | |
| if (stateInstance == nullptr) | |
| { | |
| return "(null)"; | |
| } | |
| auto state = stateInstance->state(); | |
| if (state->is<AnimationState>()) | |
| { | |
| auto anim = state->as<AnimationState>()->animation(); | |
| return anim != nullptr ? anim->name() : "Animation"; | |
| } | |
| if (state->is<EntryState>()) | |
| { | |
| return "Entry"; | |
| } | |
| if (state->is<ExitState>()) | |
| { | |
| return "Exit"; | |
| } | |
| if (state->is<AnyState>()) | |
| { | |
| return "Any"; | |
| } | |
| return "Blend"; | |
| } | |
| #endif | |
| namespace rive | |
| { | |
| class StateMachineLayerInstance | |
| { | |
| public: | |
| StateMachineLayerInstance() | |
| : m_transitionCompleted(false) | |
| , m_holdAnimationFrom(false) | |
| , m_stateMachineChangedOnAdvance(false) | |
| , m_waitingForExit(false) | |
| , m_settled(false) | |
| {} | |
| ~StateMachineLayerInstance() | |
| { | |
| delete m_anyStateInstance; | |
| delete m_currentState; | |
| delete m_stateFrom; | |
| } | |
| /// The artboard every layer of this instance applies to. This is | |
| /// identical for all layers of a given StateMachineInstance — as was the | |
| /// owning instance pointer — so holding either per layer stored the same | |
| /// value layerCount times over. Both are therefore derived from the `smi` | |
| /// threaded through the methods below rather than stored per layer. | |
| /// | |
| /// The layer *definition* is deliberately NOT derived this way. It is | |
| /// genuinely per-index data, and while `m_machine->layer(this - m_layers)` | |
| /// would recover it, that lookup is only stable in runtime builds. Under | |
| /// WITH_RIVE_EDITOR, StateMachine::layer() reads `m_editorLayers`, which | |
| /// EditorFile::finalizeBatch clears and rebuilds from arena order after | |
| /// every coop batch, while clearStalePlaybackScenes only rebuilds the | |
| /// StateMachineInstance when the StateMachine *pointer* changes. So adding | |
| /// or reparenting a layer can leave slot i resolving to a different | |
| /// definition — or, if a layer was deleted, to nullptr — while m_layers[i] | |
| /// still holds the old layer's runtime state. m_layer is captured once at | |
| /// init and pinned for the instance's lifetime instead. | |
| static ArtboardInstance* artboardOf(const StateMachineInstance* smi) | |
| { | |
| return smi->m_artboardInstance; | |
| } | |
| void init(StateMachineInstance* smi, const StateMachineLayer* layer) | |
| { | |
| assert(m_layer == nullptr); | |
| m_layer = layer; | |
| changeState(smi, m_layer->entryState()); | |
| } | |
| void resetState(StateMachineInstance* smi) | |
| { | |
| // The current state goes away below even when changeState then finds | |
| // nothing to change to (no entry state). | |
| m_settled = false; | |
| if (m_stateFrom != m_anyStateInstance && m_stateFrom != m_currentState) | |
| { | |
| delete m_stateFrom; | |
| } | |
| m_stateFrom = nullptr; | |
| if (m_currentState != m_anyStateInstance) | |
| { | |
| delete m_currentState; | |
| } | |
| m_currentState = nullptr; | |
| changeState(smi, m_layer->entryState()); | |
| } | |
| void updateMix(StateMachineInstance* smi, float seconds) | |
| { | |
| if (m_transition != nullptr && m_stateFrom != nullptr && | |
| resolvedDuration() != 0) | |
| { | |
| auto mixTime = resolvedMixTime(); | |
| if (mixTime == 0.0f) | |
| { | |
| m_mix = 1.0f; | |
| } | |
| else | |
| { | |
| m_mix = std::min(1.0f, std::max(0.0f, (m_mix + seconds / mixTime))); | |
| } | |
| if (m_mix == 1.0f && !m_transitionCompleted) | |
| { | |
| m_transitionCompleted = true; | |
| clearAnimationReset(); | |
| fireEvents( | |
| smi, | |
| StateMachineFireOccurance::atEnd, | |
| m_transition->events()); | |
| performListenerActions( | |
| smi, | |
| StateMachineFireOccurance::atEnd, | |
| m_transition->listenerActions()); | |
| } | |
| } | |
| else | |
| { | |
| m_mix = 1.0f; | |
| } | |
| } | |
| // A settled layer that has finished mixing, in a state that keys nothing, | |
| // has stopped keeping itself going and has no exit time to wait on: its | |
| // advance would only move a clock nothing reads, apply nothing and skip a | |
| // search that can't pass, so it returns what that advance would have. | |
| // The same holds for a state whose animation holds its keyframe values and | |
| // has already applied them (see isPoseInPlace): a pose, or a finished | |
| // one-shot. poseSince is the artboard's change serial when the previous | |
| // advance started. | |
| bool isFrozen(const StateMachineInstance* smi, uint32_t poseSince) const | |
| { | |
| if (!m_settled || m_mix != 1.0f || m_animationReset != nullptr || | |
| m_holdAnimation != nullptr || | |
| (m_transition != nullptr && !m_transitionCompleted) || | |
| m_currentState == nullptr || m_currentState->keepGoing()) | |
| { | |
| return false; | |
| } | |
| auto state = m_currentState->state(); | |
| if (!state->transitionsIgnoreTime()) | |
| { | |
| return false; | |
| } | |
| // Checked on the instance rather than classified with the state: a | |
| // state's animation may not be resolved yet when that runs. | |
| switch (state->coreType()) | |
| { | |
| case EntryStateBase::typeKey: | |
| case ExitStateBase::typeKey: | |
| case AnyStateBase::typeKey: | |
| return true; | |
| case AnimationStateBase::typeKey: | |
| { | |
| // An animation state without an animation plays an empty one. | |
| auto instance = static_cast<const AnimationStateInstance*>( | |
| m_currentState) | |
| ->animationInstance(); | |
| return instance->animation()->numKeyedObjects() == 0 || | |
| isPoseInPlace( | |
| smi, | |
| instance, | |
| state->as<AnimationState>()->speed(), | |
| poseSince); | |
| } | |
| default: | |
| return false; | |
| } | |
| } | |
| // A one-shot that has finished sits clamped to its end (its start, playing | |
| // backwards), crosses no more keyframes and so fires no more callbacks. | |
| // Where that time writes keyframe values as they are (a pose, or a time at | |
| // or outside every property's keyframes), applying it again changes | |
| // nothing once it has been applied at full mix -- until something else | |
| // writes to the artboard: another layer, a bind, a script, the host. Every | |
| // such write bumps the artboard's change serial, so the pose is still in | |
| // place while the serial hasn't moved since the previous advance started | |
| // (this advance's earlier layers included). Keyframe values bound to data | |
| // change without that, so their artboards keep re-applying, and so does a | |
| // transition whose interpolator doesn't end at exactly 1 (its "full" mix | |
| // still blends). Finished is the animation's own word for it: the state's | |
| // keepGoing() also reads false after an advance by zero, before the | |
| // animation has played at all. It is asked in the direction the state | |
| // plays it (stateSpeed): a state playing backwards starts at the | |
| // animation's end, where a forward one-shot would be done. | |
| bool isPoseInPlace( | |
| const StateMachineInstance* smi, | |
| const LinearAnimationInstance* instance, | |
| float stateSpeed, | |
| uint32_t poseSince) const | |
| { | |
| if (instance->keepGoing(stateSpeed) || | |
| !instance->animation()->appliesKeyFrameValuesAt(instance->time())) | |
| { | |
| return false; | |
| } | |
| const ArtboardInstance* artboard = artboardOf(smi); | |
| if (artboard->changeSerial() != poseSince) | |
| { | |
| return false; | |
| } | |
| const Artboard* source = artboard->artboardSource(); | |
| if ((source != nullptr ? source : artboard)->hasKeyFrameSourceBinds()) | |
| { | |
| return false; | |
| } | |
| if (m_transition != nullptr && m_transition->interpolator() != nullptr && | |
| m_transition->interpolator()->transform(1.0f) != 1.0f) | |
| { | |
| return false; | |
| } | |
| return true; | |
| } | |
| #ifdef TESTING | |
| // Every value the skipped pose would have written is already there. | |
| void verifyFrozenPose(const StateMachineInstance* smi) const | |
| { | |
| if (m_currentState == nullptr || | |
| m_currentState->state()->coreType() != AnimationStateBase::typeKey) | |
| { | |
| return; | |
| } | |
| auto instance = static_cast<const AnimationStateInstance*>(m_currentState) | |
| ->animationInstance(); | |
| auto animation = instance->animation(); | |
| if (animation->numKeyedObjects() == 0) | |
| { | |
| return; | |
| } | |
| StateMachineInstance::sm_frozenPoseAdvances++; | |
| float seconds = instance->time(); | |
| if (animation->quantize()) | |
| { | |
| float ffps = (float)animation->fps(); | |
| seconds = std::floor(seconds * ffps) / ffps; | |
| } | |
| ArtboardInstance* artboard = artboardOf(smi); | |
| for (size_t o = 0; o < animation->numKeyedObjects(); o++) | |
| { | |
| const KeyedObject* keyedObject = animation->getObject(o); | |
| Core* object = artboard->resolve(keyedObject->objectId()); | |
| if (object == nullptr) | |
| { | |
| continue; | |
| } | |
| for (size_t p = 0; p < keyedObject->numKeyedProperties(); p++) | |
| { | |
| const KeyedProperty* property = keyedObject->getProperty(p); | |
| int key = property->propertyKey(); | |
| size_t count = property->numKeyFrames(); | |
| if (count == 0 || CoreRegistry::isCallback(key)) | |
| { | |
| continue; | |
| } | |
| // At or before the first keyframe apply writes the first, at | |
| // or after the last it writes the last. | |
| const KeyFrame* keyFrame = count == 1 || | |
| seconds <= property->getKeyFrame(0) | |
| ->seconds() | |
| ? property->getKeyFrame(0) | |
| : property->getKeyFrame(count - 1); | |
| if (count > 1 && keyFrame == property->getKeyFrame(count - 1) && | |
| seconds < keyFrame->seconds()) | |
| { | |
| fprintf( | |
| stderr, | |
| "Frozen animation %s is between keyframes of " | |
| "property %d at %f\n", | |
| animation->name().c_str(), | |
| key, | |
| seconds); | |
| abort(); | |
| } | |
| bool inPlace; | |
| if (keyFrame->is<KeyFrameDouble>()) | |
| { | |
| inPlace = CoreRegistry::getDouble(object, key) == | |
| keyFrame->as<KeyFrameDouble>()->value(); | |
| } | |
| else if (keyFrame->is<KeyFrameColor>()) | |
| { | |
| inPlace = CoreRegistry::getColor(object, key) == | |
| keyFrame->as<KeyFrameColor>()->value(); | |
| } | |
| else if (keyFrame->is<KeyFrameId>()) | |
| { | |
| inPlace = CoreRegistry::getUint(object, key) == | |
| keyFrame->as<KeyFrameId>()->value(); | |
| } | |
| else if (keyFrame->is<KeyFrameUint>()) | |
| { | |
| inPlace = CoreRegistry::getUint(object, key) == | |
| keyFrame->as<KeyFrameUint>()->value(); | |
| } | |
| else if (keyFrame->is<KeyFrameInt>()) | |
| { | |
| inPlace = CoreRegistry::getInt(object, key) == | |
| keyFrame->as<KeyFrameInt>()->value(); | |
| } | |
| else if (keyFrame->is<KeyFrameBool>()) | |
| { | |
| inPlace = CoreRegistry::getBool(object, key) == | |
| keyFrame->as<KeyFrameBool>()->value(); | |
| } | |
| else if (keyFrame->is<KeyFrameString>()) | |
| { | |
| inPlace = CoreRegistry::getString(object, key) == | |
| keyFrame->as<KeyFrameString>()->value(); | |
| } | |
| else | |
| { | |
| fprintf( | |
| stderr, | |
| "Frozen animation %s keys property %d with an " | |
| "unverifiable keyframe type %d\n", | |
| animation->name().c_str(), | |
| key, | |
| (int)keyFrame->coreType()); | |
| abort(); | |
| } | |
| if (!inPlace) | |
| { | |
| fprintf( | |
| stderr, | |
| "Frozen animation %s in layer %s on artboard %s would " | |
| "have changed property %d of object %u\n", | |
| animation->name().c_str(), | |
| m_layer->name().c_str(), | |
| artboard->name().c_str(), | |
| key, | |
| keyedObject->objectId()); | |
| abort(); | |
| } | |
| } | |
| } | |
| } | |
| #endif | |
| bool advance( | |
| StateMachineInstance* smi, | |
| float seconds, | |
| bool newFrame, | |
| uint32_t poseSince) | |
| { | |
| RIVE_WRITE_SOURCE(layer, m_layer, smi); | |
| if (newFrame) | |
| { | |
| m_stateMachineChangedOnAdvance = false; | |
| } | |
| if (isFrozen(smi, poseSince)) | |
| { | |
| // Skipped without a search, but it still has its frame, as a | |
| // settled layer's update does (see updateState). | |
| smi->markFrozenLayerSearched(this); | |
| #ifdef TESTING | |
| StateMachineInstance::sm_frozenLayerAdvances++; | |
| verifyFrozenPose(smi); | |
| #endif | |
| return false; | |
| } | |
| m_currentState->advance(seconds, smi); | |
| updateMix(smi, seconds); | |
| if (m_stateFrom != nullptr && m_mix < 1.0f && !m_holdAnimationFrom) | |
| { | |
| // This didn't advance during our updateState, but it should now | |
| // that we realize we need to mix it in. | |
| m_stateFrom->advance(seconds, smi); | |
| } | |
| apply(smi); | |
| bool changedState = false; | |
| for (int i = 0; updateState(smi); i++) | |
| { | |
| changedState = true; | |
| apply(smi); | |
| if (i == maxIterations) | |
| { | |
| auto stateMachineName = smi->stateMachine() == nullptr | |
| ? "[SM Not found]" | |
| : smi->stateMachine()->name().c_str(); | |
| auto layerName = m_layer == nullptr ? "[LY Not found]" | |
| : m_layer->name().c_str(); | |
| auto artboardName = smi->artboard() == nullptr | |
| ? "[AB Not found]" | |
| : smi->artboard()->name().c_str(); | |
| fprintf( | |
| stderr, | |
| "%s StateMachine exceeded max iterations in layer %s " | |
| "on artboard %s\n", | |
| stateMachineName, | |
| layerName, | |
| artboardName); | |
| return false; | |
| } | |
| } | |
| m_currentState->clearSpilledTime(); | |
| return changedState || m_mix != 1.0f || m_waitingForExit || | |
| (m_currentState != nullptr && m_currentState->keepGoing()); | |
| } | |
| /// Returns the per-instance transition duration, resolving any data | |
| /// binding override. Falls back to the shared definition value when | |
| /// no binding exists. | |
| uint32_t resolvedDuration() const | |
| { | |
| if (m_transitionDurationProperty != nullptr) | |
| { | |
| float val = m_transitionDurationProperty->propertyValue(); | |
| return val < 0 ? 0 : static_cast<uint32_t>(std::round(val)); | |
| } | |
| return m_transition->duration(); | |
| } | |
| /// Computes the mix time using the per-instance resolved duration. | |
| float resolvedMixTime() const | |
| { | |
| auto dur = resolvedDuration(); | |
| if (dur == 0) | |
| { | |
| return 0; | |
| } | |
| if (m_transition->durationIsPercentage()) | |
| { | |
| float animationDuration = 0.0f; | |
| auto state = m_stateFrom->state(); | |
| if (state->is<AnimationState>()) | |
| { | |
| auto animation = state->as<AnimationState>()->animation(); | |
| if (animation != nullptr) | |
| { | |
| animationDuration = animation->durationSeconds(); | |
| } | |
| } | |
| return (float)dur / 100.0f * animationDuration; | |
| } | |
| return (float)dur / 1000.0f; | |
| } | |
| bool isTransitioning() | |
| { | |
| return m_transition != nullptr && m_stateFrom != nullptr && | |
| resolvedDuration() != 0 && m_mix < 1.0f; | |
| } | |
| /// The any state's instance is only ever fed to tryChangeState, so a layer | |
| /// whose any state has no transitions never needs one. Most don't, so this | |
| /// is built on demand instead of at init: it saves a heap allocation per | |
| /// layer in the common case. Lazy rather than a one-shot check at init | |
| /// because LayerState::transitionCount() also reports the editor's | |
| /// live-edit list, which can grow after this instance was built. | |
| void ensureAnyStateInstance(StateMachineInstance* smi) | |
| { | |
| if (m_anyStateInstance != nullptr) | |
| { | |
| return; | |
| } | |
| // A layer without an any state is degenerate but not fatal: every | |
| // other use of m_anyStateInstance is either a delete guard or a | |
| // tryChangeState call, both of which handle null. Keeping this | |
| // tolerant is what lets StateMachineLayer stop requiring the state | |
| // to be present, so exports can eventually omit unused ones. | |
| auto anyState = m_layer == nullptr ? nullptr : m_layer->anyState(); | |
| if (anyState == nullptr || anyState->transitionCount() == 0) | |
| { | |
| return; | |
| } | |
| m_anyStateInstance = anyState->makeInstance(artboardOf(smi)).release(); | |
| } | |
| bool updateState(StateMachineInstance* smi) | |
| { | |
| // Don't allow changing state while a transition is taking place | |
| // (we're mixing one state onto another) if enableEarlyExit is not true. | |
| if (isTransitioning() && !m_transition->enableEarlyExit()) | |
| { | |
| // A view model change pending while this layer can't change state | |
| // still expires with the frame, instead of waiting for a search | |
| // that can act on it. | |
| smi->markViewModelChangesSearched(this); | |
| return false; | |
| } | |
| m_waitingForExit = false; | |
| ensureAnyStateInstance(smi); | |
| if (m_settled) | |
| { | |
| // Marked anyway: a machine bound to another instance would | |
| // otherwise find that instance's past changes pending. | |
| smi->markViewModelChangesSearched(this); | |
| #ifdef TESTING | |
| verifySettled(smi); | |
| StateMachineInstance::sm_settledLayerSkips++; | |
| #endif | |
| return false; | |
| } | |
| smi->markViewModelChangesSearched(this); | |
| if (tryChangeState(smi, m_anyStateInstance)) | |
| { | |
| return true; | |
| } | |
| if (tryChangeState(smi, m_currentState)) | |
| { | |
| return true; | |
| } | |
| #ifndef WITH_RIVE_EDITOR | |
| // Nothing to change to, and that holds until the current state changes | |
| // or a value these transitions read does, provided they read only | |
| // values whose changes reach this instance (transitionsSettleSafe), | |
| // none is waiting on an exit time, and no bind is waiting to be | |
| // applied (a condition may just have read a bindable's old value). | |
| // StateMachineInstance::unsettleLayers undoes it. | |
| if (!m_waitingForExit && !smi->m_bindsPending && | |
| isSettleSafe(m_anyStateInstance) && isSettleSafe(m_currentState)) | |
| { | |
| m_settled = true; | |
| smi->m_anySettled = true; | |
| } | |
| #endif | |
| return false; | |
| } | |
| void unsettle() { m_settled = false; } | |
| static bool isSettleSafe(const StateInstance* stateInstance) | |
| { | |
| return stateInstance == nullptr || | |
| stateInstance->state()->transitionsSettleSafe(); | |
| } | |
| #ifdef TESTING | |
| // A settled layer's search must still come up empty. Repeats it without | |
| // its side effects (random weights, used-layer marks, m_waitingForExit) | |
| // and stops loudly if the settled answer went stale. | |
| void verifySettled(StateMachineInstance* smi) | |
| { | |
| StateInstance* stateFromInstances[] = {m_anyStateInstance, m_currentState}; | |
| for (StateInstance* stateFromInstance : stateFromInstances) | |
| { | |
| if (stateFromInstance == nullptr) | |
| { | |
| continue; | |
| } | |
| auto stateFrom = stateFromInstance->state(); | |
| for (size_t i = 0, count = stateFrom->transitionCount(); i < count; i++) | |
| { | |
| auto transition = stateFrom->transition(i); | |
| if (canChangeState(transition->stateTo()) && | |
| transition->allowed(stateFromInstance, smi, this) != | |
| AllowTransition::no) | |
| { | |
| fprintf( | |
| stderr, | |
| "Settled layer %s on artboard %s would have taken " | |
| "a transition\n", | |
| m_layer->name().c_str(), | |
| smi->artboard()->name().c_str()); | |
| abort(); | |
| } | |
| } | |
| } | |
| } | |
| #endif | |
| void fireEvents( | |
| StateMachineInstance* smi, | |
| StateMachineFireOccurance occurs, | |
| const std::vector<StateMachineFireAction*>& fireEvents) | |
| { | |
| for (auto event : fireEvents) | |
| { | |
| if (event->occurs() == occurs) | |
| { | |
| event->perform(smi); | |
| } | |
| } | |
| } | |
| void performListenerActions( | |
| StateMachineInstance* smi, | |
| StateMachineFireOccurance occurs, | |
| const std::vector<std::unique_ptr<ListenerAction>>& listenerActions) | |
| { | |
| bool performed = false; | |
| for (const auto& action : listenerActions) | |
| { | |
| if (action->matchesScheduledOccurrence(occurs)) | |
| { | |
| action->perform(smi, ListenerInvocation::none()); | |
| performed = true; | |
| } | |
| } | |
| if (performed) | |
| { | |
| smi->updateDataBinds(false); | |
| } | |
| } | |
| bool canChangeState(const LayerState* stateTo) | |
| { | |
| return !( | |
| (m_currentState == nullptr ? nullptr : m_currentState->state()) == | |
| stateTo); | |
| } | |
| double randomValue() { return RandomProvider::generateRandomFloat(); } | |
| void changeState(StateMachineInstance* smi, const LayerState* stateTo) | |
| { | |
| if ((m_currentState == nullptr ? nullptr : m_currentState->state()) == | |
| stateTo) | |
| { | |
| return; | |
| } | |
| // A different state has different transitions to search. | |
| m_settled = false; | |
| // Fire end events for the state we're changing from. | |
| if (m_currentState != nullptr) | |
| { | |
| fireEvents( | |
| smi, | |
| StateMachineFireOccurance::atEnd, | |
| m_currentState->state()->events()); | |
| performListenerActions( | |
| smi, | |
| StateMachineFireOccurance::atEnd, | |
| m_currentState->state()->listenerActions()); | |
| } | |
| m_currentState = stateTo == nullptr | |
| ? nullptr | |
| : stateTo->makeInstance(artboardOf(smi)).release(); | |
| // Fire start events for the state we're changing to. | |
| if (m_currentState != nullptr) | |
| { | |
| fireEvents( | |
| smi, | |
| StateMachineFireOccurance::atStart, | |
| m_currentState->state()->events()); | |
| performListenerActions( | |
| smi, | |
| StateMachineFireOccurance::atStart, | |
| m_currentState->state()->listenerActions()); | |
| } | |
| return; | |
| } | |
| StateTransition* findRandomTransition( | |
| StateMachineInstance* smi, | |
| StateInstance* stateFromInstance) | |
| { | |
| uint32_t totalWeight = 0; | |
| auto stateFrom = stateFromInstance->state(); | |
| for (size_t i = 0, length = stateFrom->transitionCount(); i < length; i++) | |
| { | |
| auto transition = stateFrom->transition(i); | |
| if (canChangeState(transition->stateTo())) | |
| { | |
| auto allowed = transition->allowed(stateFromInstance, smi, this); | |
| if (allowed == AllowTransition::yes) | |
| { | |
| transition->evaluatedRandomWeight(transition->randomWeight()); | |
| totalWeight += transition->randomWeight(); | |
| } | |
| else | |
| { | |
| transition->evaluatedRandomWeight(0); | |
| if (allowed == AllowTransition::waitingForExit) | |
| { | |
| m_waitingForExit = true; | |
| } | |
| } | |
| } | |
| else | |
| { | |
| transition->evaluatedRandomWeight(0); | |
| } | |
| } | |
| if (totalWeight > 0) | |
| { | |
| double randomWeight = randomValue() * totalWeight * 1.0; | |
| double currentWeight = 0; | |
| size_t index = 0; | |
| StateTransition* transition; | |
| while (index < stateFrom->transitionCount()) | |
| { | |
| transition = stateFrom->transition(index); | |
| double transitionWeight = (double)transition->evaluatedRandomWeight(); | |
| if (currentWeight + transitionWeight > randomWeight) | |
| { | |
| transition->useLayerInConditions(smi, this); | |
| return transition; | |
| } | |
| currentWeight += transitionWeight; | |
| index++; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| StateTransition* findAllowedTransition( | |
| StateMachineInstance* smi, | |
| StateInstance* stateFromInstance) | |
| { | |
| auto stateFrom = stateFromInstance->state(); | |
| // If it should randomize | |
| if ((static_cast<LayerStateFlags>(stateFrom->flags()) & | |
| LayerStateFlags::Random) == LayerStateFlags::Random) | |
| { | |
| return findRandomTransition(smi, stateFromInstance); | |
| } | |
| // Else search the first valid transition | |
| for (size_t i = 0, length = stateFrom->transitionCount(); i < length; i++) | |
| { | |
| auto transition = stateFrom->transition(i); | |
| if (canChangeState(transition->stateTo())) | |
| { | |
| auto allowed = transition->allowed(stateFromInstance, smi, this); | |
| if (allowed == AllowTransition::yes) | |
| { | |
| transition->evaluatedRandomWeight(transition->randomWeight()); | |
| transition->useLayerInConditions(smi, this); | |
| return transition; | |
| } | |
| else | |
| { | |
| transition->evaluatedRandomWeight(0); | |
| if (allowed == AllowTransition::waitingForExit) | |
| { | |
| m_waitingForExit = true; | |
| } | |
| } | |
| } | |
| } | |
| return nullptr; | |
| } | |
| void buildAnimationResetForTransition(StateMachineInstance* smi) | |
| { | |
| m_animationReset = AnimationResetFactory::fromStates( | |
| m_stateFrom, | |
| m_currentState, | |
| artboardOf(smi)); | |
| } | |
| void clearAnimationReset() | |
| { | |
| if (m_animationReset != nullptr) | |
| { | |
| AnimationResetFactory::release(std::move(m_animationReset)); | |
| m_animationReset = nullptr; | |
| } | |
| } | |
| bool tryChangeState( | |
| StateMachineInstance* smi, | |
| StateInstance* stateFromInstance) | |
| { | |
| if (stateFromInstance == nullptr) | |
| { | |
| return false; | |
| } | |
| auto outState = m_currentState; | |
| auto transition = findAllowedTransition(smi, stateFromInstance); | |
| if (transition != nullptr) | |
| { | |
| clearAnimationReset(); | |
| changeState(smi, transition->stateTo()); | |
| m_stateMachineChangedOnAdvance = true; | |
| #ifdef RIVE_MICROPROFILE | |
| RiveProfile::instance().recordTransition( | |
| smi->artboard()->name(), | |
| smi->name(), | |
| m_layer->name(), | |
| getStateName(outState), | |
| getStateName(m_currentState), | |
| smi->artboard()); | |
| #endif | |
| // state actually has changed | |
| m_transition = transition; | |
| m_transitionDurationProperty = smi->findTransitionPropertyInstance( | |
| transition, | |
| StateTransitionBase::durationPropertyKey); | |
| fireEvents(smi, StateMachineFireOccurance::atStart, transition->events()); | |
| performListenerActions( | |
| smi, | |
| StateMachineFireOccurance::atStart, | |
| transition->listenerActions()); | |
| if (resolvedDuration() == 0) | |
| { | |
| m_transitionCompleted = true; | |
| fireEvents(smi, StateMachineFireOccurance::atEnd, transition->events()); | |
| performListenerActions( | |
| smi, | |
| StateMachineFireOccurance::atEnd, | |
| transition->listenerActions()); | |
| } | |
| else | |
| { | |
| m_transitionCompleted = false; | |
| } | |
| if (m_stateFrom != m_anyStateInstance) | |
| { | |
| // Old state from is done. | |
| delete m_stateFrom; | |
| } | |
| m_stateFrom = outState; | |
| if (!m_transitionCompleted) | |
| { | |
| buildAnimationResetForTransition(smi); | |
| } | |
| // If we had an exit time and wanted to pause on exit, make | |
| // sure to hold the exit time. Delegate this to the | |
| // transition by telling it that it was completed. | |
| if (outState != nullptr && transition->applyExitCondition(outState)) | |
| { | |
| // Make sure we apply this state. This only returns true | |
| // when it's an animation state instance. | |
| auto instance = static_cast<AnimationStateInstance*>(m_stateFrom) | |
| ->animationInstance(); | |
| m_holdAnimation = instance->animation(); | |
| m_holdTime = instance->time(); | |
| } | |
| m_mixFrom = m_mix; | |
| // Keep mixing last animation that was mixed in. | |
| if (m_mix != 0.0f) | |
| { | |
| m_holdAnimationFrom = transition->pauseOnExit(); | |
| } | |
| if (m_currentState != nullptr) | |
| { | |
| auto advanceTime = 0.0f; | |
| if (m_stateFrom != nullptr) | |
| { | |
| if (m_stateFrom->state()->is<AnimationState>()) | |
| { | |
| auto instance = static_cast<AnimationStateInstance*>(m_stateFrom) | |
| ->animationInstance(); | |
| advanceTime = instance->spilledTime(); | |
| } | |
| } | |
| m_currentState->advance(advanceTime, smi); | |
| } | |
| m_mix = 0.0f; | |
| updateMix(smi, 0.0f); | |
| m_waitingForExit = false; | |
| return true; | |
| } | |
| return false; | |
| } | |
| void apply(StateMachineInstance* smi) | |
| { | |
| auto artboardInstance = artboardOf(smi); | |
| if (m_animationReset != nullptr) | |
| { | |
| m_animationReset->apply(artboardInstance); | |
| } | |
| if (m_holdAnimation != nullptr) | |
| { | |
| m_holdAnimation->apply(artboardInstance, m_holdTime, m_mixFrom); | |
| m_holdAnimation = nullptr; | |
| } | |
| KeyFrameInterpolator* interpolator = nullptr; | |
| if (m_transition != nullptr && m_transition->interpolator() != nullptr) | |
| { | |
| interpolator = m_transition->interpolator(); | |
| // A scripted one runs as this instance's clone (StateMachine | |
| // registers it with its scripted objects). | |
| if (interpolator->is<ScriptedInterpolator>()) | |
| { | |
| if (auto* stateful = smi->scriptedObject( | |
| interpolator->as<ScriptedInterpolator>())) | |
| { | |
| interpolator = static_cast<ScriptedInterpolator*>(stateful); | |
| } | |
| } | |
| } | |
| if (m_stateFrom != nullptr && m_mix < 1.0f) | |
| { | |
| auto fromMix = interpolator != nullptr | |
| ? interpolator->transform(m_mixFrom) | |
| : m_mixFrom; | |
| m_stateFrom->apply(artboardInstance, fromMix); | |
| } | |
| if (m_currentState != nullptr) | |
| { | |
| auto mix = interpolator != nullptr ? interpolator->transform(m_mix) | |
| : m_mix; | |
| m_currentState->apply(artboardInstance, mix); | |
| } | |
| } | |
| bool stateChangedOnAdvance() const { return m_stateMachineChangedOnAdvance; } | |
| const LayerState* currentState() | |
| { | |
| return m_currentState == nullptr ? nullptr : m_currentState->state(); | |
| } | |
| const LinearAnimationInstance* currentAnimation() const | |
| { | |
| if (m_currentState == nullptr || | |
| !m_currentState->state()->is<AnimationState>()) | |
| { | |
| return nullptr; | |
| } | |
| return static_cast<AnimationStateInstance*>(m_currentState) | |
| ->animationInstance(); | |
| } | |
| private: | |
| static const int maxIterations = 100; | |
| // One of these exists per layer of every StateMachineInstance, which in an | |
| // ArtboardComponentList means per layer per row. Keep the pointers, then | |
| // the floats, then the bools: interleaving them costs 8 B of padding for | |
| // nothing. The owning instance and its artboard used to be stored here | |
| // too; both are the same for every layer of an instance, so they are now | |
| // derived from the `smi` argument threaded through the methods above. The | |
| // layer definition stays stored — see artboardOf() for why deriving it | |
| // from the array index is not safe in editor builds. | |
| const StateMachineLayer* m_layer = nullptr; | |
| StateInstance* m_anyStateInstance = nullptr; | |
| StateInstance* m_currentState = nullptr; | |
| StateInstance* m_stateFrom = nullptr; | |
| const StateTransition* m_transition = nullptr; | |
| BindablePropertyNumber* m_transitionDurationProperty = nullptr; | |
| std::unique_ptr<AnimationReset> m_animationReset = nullptr; | |
| /// Used to ensure a specific animation is applied on the next apply. | |
| const LinearAnimation* m_holdAnimation = nullptr; | |
| float m_mix = 1.0f; | |
| float m_mixFrom = 1.0f; | |
| float m_holdTime = 0.0f; | |
| // One bit each, so m_settled shares the byte the four flags before it | |
| // already take and sizeof stays 80. Bit-fields can't have default member | |
| // initializers before C++20, so the constructor sets them. | |
| bool m_transitionCompleted : 1; | |
| bool m_holdAnimationFrom : 1; | |
| bool m_stateMachineChangedOnAdvance : 1; | |
| bool m_waitingForExit : 1; | |
| // The last search found nothing and nothing it read has changed since, | |
| // so the next one would find nothing too. See updateState. | |
| bool m_settled : 1; | |
| }; | |
| // Instances with a capture-phase pointer listener of their own. While there are | |
| // none, no dispatch anywhere runs the capture phase, so files without capture | |
| // listeners keep exactly the single pass they always had. | |
| static std::atomic<uint32_t> s_liveCaptureMachines{0}; | |
| // Moves whenever the set of trees that hold a capture listener may have | |
| // changed, see StateMachineInstance::captureTopologyChanged. | |
| static std::atomic<uint32_t> s_captureEpoch{1}; | |
| static HitResult combineHitResults(HitResult a, HitResult b) | |
| { | |
| if (a == HitResult::hitOpaque || b == HitResult::hitOpaque) | |
| { | |
| return HitResult::hitOpaque; | |
| } | |
| if (a == HitResult::hit || b == HitResult::hit) | |
| { | |
| return HitResult::hit; | |
| } | |
| return HitResult::none; | |
| } | |
| // Whether component is scope or sits under it in the artboard's hierarchy. | |
| static bool componentWithin(const Component* component, const Component* scope) | |
| { | |
| for (auto c = component; c != nullptr; c = c->parent()) | |
| { | |
| if (c == scope) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // The component a hit component stands for in the hierarchy: a layout's | |
| // listeners hit through its proxy, which isn't part of the hierarchy itself. | |
| static Component* hierarchyComponent(HitComponent* hitComponent) | |
| { | |
| auto component = hitComponent->component(); | |
| if (component != nullptr && component->is<Drawable>() && | |
| component->as<Drawable>()->isProxy()) | |
| { | |
| return component->as<Drawable>()->hittableComponent(); | |
| } | |
| return component; | |
| } | |
| /// Marks a machine as receiving a pointer event from the machine hosting it, | |
| /// which ran the capture phase over it already. | |
| class HostDispatchScope | |
| { | |
| public: | |
| HostDispatchScope(StateMachineInstance* stateMachineInstance) | |
| : m_stateMachineInstance(stateMachineInstance) | |
| { | |
| if (m_stateMachineInstance != nullptr) | |
| { | |
| m_previous = m_stateMachineInstance->m_hostDispatching; | |
| m_stateMachineInstance->m_hostDispatching = true; | |
| } | |
| } | |
| ~HostDispatchScope() | |
| { | |
| if (m_stateMachineInstance != nullptr) | |
| { | |
| m_stateMachineInstance->m_hostDispatching = m_previous; | |
| } | |
| } | |
| private: | |
| StateMachineInstance* m_stateMachineInstance; | |
| bool m_previous = false; | |
| }; | |
| /// Representation of a Component from the Artboard Instance and all the | |
| /// listeners it triggers. Allows tracking hover and performing hit detection | |
| /// only once on components that trigger multiple listeners. | |
| // A down, up or move a hosted machine gets while its host can't hit | |
| // anything: past the edge of a mesh the host draws it through, or while the | |
| // host is itself missing the pointer (see pointerMiss). | |
| static bool isMissableEvent(ListenerType hitType) | |
| { | |
| return hitType == ListenerType::down || hitType == ListenerType::up || | |
| hitType == ListenerType::move; | |
| } | |
| // The passes a host's own drag sets off in what it hosts, to turn their | |
| // pointers off and back on. Past a mesh's edge they miss like the above, so | |
| // dragEnd's settling move can't reach anything undrawn. | |
| static bool isDragPass(ListenerType hitType) | |
| { | |
| return hitType == ListenerType::dragStart || hitType == ListenerType::dragEnd; | |
| } | |
| class HitDrawable : public HitComponent | |
| { | |
| public: | |
| HitDrawable( | |
| Drawable* drawable, | |
| Component* component, | |
| StateMachineInstance* stateMachineInstance, | |
| bool isOpaque) | |
| : HitComponent(component, stateMachineInstance) | |
| { | |
| this->m_drawable = drawable; | |
| this->isOpaque = isOpaque; | |
| if (drawable->isTargetOpaque()) | |
| { | |
| canEarlyOut = false; | |
| } | |
| } | |
| float hitRadius = 2; | |
| bool isHovered = false; | |
| bool canEarlyOut = true; | |
| bool hasDownListener = false; | |
| bool hasUpListener = false; | |
| bool isOpaque = false; | |
| Drawable* m_drawable; | |
| std::vector<ListenerGroup*> listeners; | |
| bool hitTest(Vec2D position) const override { return false; } | |
| DraggableProxy* scrollProxyFor(const ScrollEvent& event) | |
| { | |
| // A live gesture keeps the view it started on, even once that view | |
| // can't move; only then does an idle proxy get a chance at the delta. | |
| for (auto listenerGroup : listeners) | |
| { | |
| auto proxy = listenerGroup->scrollProxy(); | |
| if (proxy != nullptr && proxy->isScrollGestureActive()) | |
| { | |
| return proxy; | |
| } | |
| } | |
| for (auto listenerGroup : listeners) | |
| { | |
| auto proxy = listenerGroup->scrollProxy(); | |
| if (proxy != nullptr && proxy->wantsScroll(event)) | |
| { | |
| return proxy; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| bool wantsScroll(Vec2D position, const ScrollEvent& event) override | |
| { | |
| return hitTest(position) && scrollProxyFor(event) != nullptr; | |
| } | |
| bool hasScrollTarget(Vec2D position) override | |
| { | |
| if (!hitTest(position)) | |
| { | |
| return false; | |
| } | |
| for (auto listenerGroup : listeners) | |
| { | |
| auto proxy = listenerGroup->scrollProxy(); | |
| if (proxy != nullptr && proxy->acceptsScroll()) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool listensToButtonAt(Vec2D position, PointerButton button) override | |
| { | |
| if (!hitTest(position)) | |
| { | |
| return false; | |
| } | |
| for (auto listenerGroup : listeners) | |
| { | |
| auto listener = listenerGroup->listener(); | |
| if (listener != nullptr && listener->listensToButton(button)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool occludesPointer(Vec2D position) override | |
| { | |
| return (isOpaque || m_drawable->isTargetOpaque()) && hitTest(position); | |
| } | |
| bool occludesScroll(Vec2D position) override | |
| { | |
| // A scroll view never occludes: at its edge it declines and the | |
| // gesture chains outward. | |
| for (auto listenerGroup : listeners) | |
| { | |
| if (listenerGroup->scrollProxy() != nullptr) | |
| { | |
| return false; | |
| } | |
| } | |
| // Same opacity test as processEvent's hitOpaque. | |
| return (isOpaque || m_drawable->isTargetOpaque()) && hitTest(position); | |
| } | |
| bool scrollGestureActive() override | |
| { | |
| for (auto listenerGroup : listeners) | |
| { | |
| auto proxy = listenerGroup->scrollProxy(); | |
| if (proxy != nullptr && proxy->isScrollGestureActive()) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| void cancelScroll() override | |
| { | |
| for (auto listenerGroup : listeners) | |
| { | |
| auto proxy = listenerGroup->scrollProxy(); | |
| if (proxy != nullptr) | |
| { | |
| proxy->cancelScroll(); | |
| } | |
| } | |
| } | |
| HitResult processScroll( | |
| Vec2D position, | |
| const ScrollEvent& event, | |
| float timeStamp, | |
| int pointerId) override | |
| { | |
| auto proxy = scrollProxyFor(event); | |
| if (proxy == nullptr || !proxy->scroll(event, timeStamp)) | |
| { | |
| return HitResult::none; | |
| } | |
| return isOpaque ? HitResult::hitOpaque : HitResult::hit; | |
| } | |
| void prepareEvent(Vec2D position, ListenerType hitType, int pointerId) | |
| override | |
| { | |
| if (canEarlyOut && (hitType != ListenerType::down || !hasDownListener) && | |
| (hitType != ListenerType::up || !hasUpListener)) | |
| { | |
| #ifdef TESTING | |
| earlyOutCount++; | |
| #endif | |
| return; | |
| } | |
| isHovered = hitType != ListenerType::exit && hitTest(position); | |
| // // iterate all listeners associated with this hit shape | |
| if (isHovered) | |
| { | |
| for (auto listenerGroup : listeners) | |
| { | |
| listenerGroup->hover(pointerId); | |
| } | |
| } | |
| } | |
| HitResult processGamepadInvocation( | |
| const ListenerInvocation& invocation, | |
| ScriptedDrawable* alreadyDispatched) override | |
| { | |
| return HitResult::none; | |
| } | |
| HitResult processEvent( | |
| Vec2D position, | |
| ListenerType hitType, | |
| bool canHit, | |
| float timeStamp, | |
| int pointerId, | |
| PointerButton button) override | |
| { | |
| // If the shape doesn't have any ListenerType::move / enter / exit and | |
| // the event being processed is not of the type it needs to handle. | |
| // There is no need to perform a hitTest (which is relatively expensive | |
| // and would be happening on every pointer move) so we early out. | |
| if (canEarlyOut && (hitType != ListenerType::down || !hasDownListener) && | |
| (hitType != ListenerType::up || !hasUpListener)) | |
| { | |
| return HitResult::none; | |
| } | |
| bool isBlockingEvent = false; | |
| // // iterate all listeners associated with this hit shape | |
| for (auto listenerGroup : listeners) | |
| { | |
| // Disabled is per pointer (a drag, or a capture listener's claim); | |
| // consumed is shared, so another pointer's event clears it. | |
| if (listenerGroup->isConsumed() || | |
| listenerGroup->isPointerDisabled(pointerId)) | |
| { | |
| continue; | |
| } | |
| if (listenerGroup->processEvent( | |
| m_component, | |
| position, | |
| pointerId, | |
| hitType, | |
| button, | |
| canHit, | |
| timeStamp, | |
| m_stateMachineInstance) == ProcessEventResult::scroll) | |
| { | |
| isBlockingEvent = true; | |
| } | |
| } | |
| return (isHovered && canHit) | |
| ? (isOpaque || m_drawable->isTargetOpaque() || isBlockingEvent) | |
| ? HitResult::hitOpaque | |
| : HitResult::hit | |
| : HitResult::none; | |
| } | |
| void addListener(ListenerGroup* listenerGroup) | |
| { | |
| if (!listenerGroup->canEarlyOut(m_component)) | |
| { | |
| canEarlyOut = false; | |
| } | |
| else | |
| { | |
| if (listenerGroup->needsDownListener(m_component)) | |
| { | |
| hasDownListener = true; | |
| } | |
| if (listenerGroup->needsUpListener(m_component)) | |
| { | |
| hasUpListener = true; | |
| } | |
| } | |
| listeners.push_back(listenerGroup); | |
| } | |
| void enablePointerEvents(int pointerId) override | |
| { | |
| for (auto listenerGroup : listeners) | |
| { | |
| listenerGroup->enable(pointerId); | |
| } | |
| } | |
| void disablePointerEvents(int pointerId) override | |
| { | |
| for (auto listenerGroup : listeners) | |
| { | |
| listenerGroup->disable(pointerId); | |
| } | |
| } | |
| }; | |
| /// Representation of a HitDrawable with a Hittable component | |
| class HitExpandable : public HitDrawable | |
| { | |
| public: | |
| HitExpandable( | |
| Drawable* drawable, | |
| Component* component, | |
| StateMachineInstance* stateMachineInstance, | |
| bool isOpaque = false) | |
| : HitDrawable(drawable, component, stateMachineInstance, isOpaque) | |
| {} | |
| bool hitTest(Vec2D position) const override | |
| { | |
| return m_component->hitTestPoint(position, true, true); | |
| } | |
| }; | |
| class HitTextRun : public HitExpandable | |
| { | |
| public: | |
| HitTextRun( | |
| Drawable* drawable, | |
| TextValueRun* component, | |
| StateMachineInstance* stateMachineInstance, | |
| bool isOpaque = false) | |
| : HitExpandable(drawable, component, stateMachineInstance, isOpaque) | |
| { | |
| if (component) | |
| { | |
| component->isHitTarget(true); | |
| } | |
| } | |
| }; | |
| class HitLayout : public HitDrawable | |
| { | |
| public: | |
| HitLayout( | |
| Drawable* layout, | |
| StateMachineInstance* stateMachineInstance, | |
| bool isOpaque = false) | |
| : HitDrawable(layout, layout, stateMachineInstance, isOpaque) | |
| {} | |
| bool hitTest(Vec2D position) const override | |
| { | |
| return m_component->hitTestPoint(position, false, true); | |
| } | |
| }; | |
| // The pointers a host turned off (its capture claim, or a drag above it) for | |
| // the machines a hit component hosts. Those machines can change while one is | |
| // off -- a list realizes or adds a row, a nested artboard swaps its machine -- | |
| // so each is turned off as an event reaches it, not only when the host turned | |
| // the pointer off. | |
| class HostedPointersOff | |
| { | |
| public: | |
| void add(int pointerId) { m_ids.pushUnique(pointerId); } | |
| void remove(int pointerId) { m_ids.eraseAll(pointerId); } | |
| bool has(int pointerId) const | |
| { | |
| const auto& ids = m_ids.view(); | |
| return std::find(ids.begin(), ids.end(), pointerId) != ids.end(); | |
| } | |
| void keepOff(StateMachineInstance* machine, int pointerId) const | |
| { | |
| if (machine != nullptr && has(pointerId) && | |
| !machine->isPointerDisabled(pointerId)) | |
| { | |
| machine->disablePointerEvents(pointerId); | |
| } | |
| } | |
| private: | |
| LazyVector<int> m_ids; | |
| }; | |
| class HitNestedArtboard : public HitComponent | |
| { | |
| public: | |
| HitNestedArtboard( | |
| Component* nestedArtboard, | |
| StateMachineInstance* stateMachineInstance) | |
| : HitComponent(nestedArtboard, stateMachineInstance) | |
| {} | |
| ~HitNestedArtboard() override {} | |
| bool hitTest(Vec2D position) const override | |
| { | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| if (nestedArtboard->isCollapsed() || nestedArtboard->isHidden() || | |
| nestedArtboard->isPaused()) | |
| { | |
| return false; | |
| } | |
| Vec2D nestedPosition; | |
| if (!drawnPosition(nestedArtboard, position, &nestedPosition)) | |
| { | |
| // Mounted artboard isn't ready, has a 0 scale transform, or isn't | |
| // drawn under the pointer. | |
| return false; | |
| } | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (nestedAnimation->is<NestedStateMachine>()) | |
| { | |
| auto nestedStateMachine = nestedAnimation->as<NestedStateMachine>(); | |
| if (nestedStateMachine->hitTest(nestedPosition)) | |
| { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| bool hitTestBounded(Vec2D position) const override | |
| { | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| if (nestedArtboard->isCollapsed() || nestedArtboard->isHidden() || | |
| nestedArtboard->isPaused()) | |
| { | |
| return false; | |
| } | |
| Vec2D nestedPosition; | |
| if (!drawnPosition(nestedArtboard, position, &nestedPosition)) | |
| { | |
| return false; | |
| } | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (nestedAnimation->is<NestedStateMachine>() && | |
| nestedAnimation->as<NestedStateMachine>()->hitTestBounded( | |
| nestedPosition)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /// Maps `position` into the nested artboard for a query only something | |
| /// drawn there can answer: false past the edge of a mesh the artboard is | |
| /// drawn through. | |
| static bool drawnPosition( | |
| NestedArtboard* nestedArtboard, | |
| Vec2D position, | |
| Vec2D* nestedPosition, | |
| Mat2D* toNested = nullptr) | |
| { | |
| bool offMesh; | |
| return nestedArtboard | |
| ->worldToLocal(position, nestedPosition, &offMesh, toNested) && | |
| !offMesh; | |
| } | |
| /// Maps a parent-space delta into the nested artboard. A delta is a | |
| /// vector, so it takes only the linear part of the mapping at the pointer: | |
| /// through a mesh, that of the triangle under it, wherever the delta would | |
| /// end. Mapping the end point instead could land in another N-Slice tile. | |
| static Vec2D nestedDelta(const Mat2D& toNested, Vec2D delta) | |
| { | |
| return Vec2D::transformDir(delta, toNested); | |
| } | |
| bool wantsScroll(Vec2D position, const ScrollEvent& event) override | |
| { | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| if (nestedArtboard->isCollapsed() || nestedArtboard->isPaused()) | |
| { | |
| return false; | |
| } | |
| Vec2D nestedPosition; | |
| Mat2D toNested; | |
| if (!drawnPosition(nestedArtboard, position, &nestedPosition, &toNested)) | |
| { | |
| return false; | |
| } | |
| ScrollEvent mapped = event; | |
| mapped.delta = nestedDelta(toNested, event.delta); | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (nestedAnimation->is<NestedStateMachine>() && | |
| nestedAnimation->as<NestedStateMachine>()->wantsScroll( | |
| nestedPosition, | |
| mapped)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool hasScrollTarget(Vec2D position) override | |
| { | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| if (nestedArtboard->isCollapsed() || nestedArtboard->isPaused()) | |
| { | |
| return false; | |
| } | |
| Vec2D nestedPosition; | |
| if (!drawnPosition(nestedArtboard, position, &nestedPosition)) | |
| { | |
| return false; | |
| } | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (nestedAnimation->is<NestedStateMachine>() && | |
| nestedAnimation->as<NestedStateMachine>()->hasScrollTargetAt( | |
| nestedPosition)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool listensToButtonAt(Vec2D position, PointerButton button) override | |
| { | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| if (nestedArtboard->isCollapsed() || nestedArtboard->isHidden() || | |
| nestedArtboard->isPaused()) | |
| { | |
| return false; | |
| } | |
| Vec2D nestedPosition; | |
| if (!drawnPosition(nestedArtboard, position, &nestedPosition)) | |
| { | |
| return false; | |
| } | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (nestedAnimation->is<NestedStateMachine>() && | |
| nestedAnimation->as<NestedStateMachine>()->listensToButtonAt( | |
| nestedPosition, | |
| button)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool occludesScroll(Vec2D position) override | |
| { | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| if (nestedArtboard->isCollapsed() || nestedArtboard->isPaused()) | |
| { | |
| return false; | |
| } | |
| Vec2D nestedPosition; | |
| if (!drawnPosition(nestedArtboard, position, &nestedPosition)) | |
| { | |
| return false; | |
| } | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (nestedAnimation->is<NestedStateMachine>() && | |
| nestedAnimation->as<NestedStateMachine>()->scrollOccludedAt( | |
| nestedPosition)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool scrollGestureActive() override | |
| { | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| if (nestedArtboard->isCollapsed() || nestedArtboard->isPaused()) | |
| { | |
| // A paused machine never advances, so its idle timer can never | |
| // clear the latch. End it outright, or unpausing resurrects it. | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (nestedAnimation->is<NestedStateMachine>()) | |
| { | |
| nestedAnimation->as<NestedStateMachine>()->cancelScroll(); | |
| } | |
| } | |
| return false; | |
| } | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (nestedAnimation->is<NestedStateMachine>() && | |
| nestedAnimation->as<NestedStateMachine>()->hasScrollLatch()) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| HitResult processScroll( | |
| Vec2D position, | |
| const ScrollEvent& event, | |
| float timeStamp, | |
| int pointerId) override | |
| { | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| Vec2D nestedPosition; | |
| bool offMesh; | |
| Mat2D toNested; | |
| if (!nestedArtboard | |
| ->worldToLocal(position, &nestedPosition, &offMesh, &toNested)) | |
| { | |
| return HitResult::none; | |
| } | |
| ScrollEvent mapped = event; | |
| mapped.delta = nestedDelta(toNested, event.delta); | |
| // A latched machine gets the event first; only then can another | |
| // take it. | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (nestedAnimation->is<NestedStateMachine>() && | |
| nestedAnimation->as<NestedStateMachine>()->hasScrollLatch()) | |
| { | |
| return nestedAnimation->as<NestedStateMachine>() | |
| ->pointerScroll(nestedPosition, mapped, timeStamp, pointerId); | |
| } | |
| } | |
| // Past the mesh's edge nothing is drawn to start a gesture on. | |
| if (offMesh) | |
| { | |
| return HitResult::none; | |
| } | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (!nestedAnimation->is<NestedStateMachine>()) | |
| { | |
| continue; | |
| } | |
| auto result = nestedAnimation->as<NestedStateMachine>()->pointerScroll( | |
| nestedPosition, | |
| mapped, | |
| timeStamp, | |
| pointerId); | |
| if (result != HitResult::none) | |
| { | |
| return result; | |
| } | |
| } | |
| return HitResult::none; | |
| } | |
| HitResult processGamepadInvocation( | |
| const ListenerInvocation& invocation, | |
| ScriptedDrawable* alreadyDispatched) override | |
| { | |
| auto hitResult = HitResult::none; | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (nestedAnimation->is<NestedStateMachine>()) | |
| { | |
| auto nestedStateMachine = nestedAnimation->as<NestedStateMachine>(); | |
| nestedStateMachine->stateMachineInstance() | |
| ->broadcastGamepadToScriptedDrawables( | |
| invocation, | |
| alreadyDispatched); | |
| } | |
| } | |
| return hitResult; | |
| } | |
| HitResult processEvent( | |
| Vec2D position, | |
| ListenerType hitType, | |
| bool canHit, | |
| float timeStamp, | |
| int pointerId, | |
| PointerButton button) override | |
| { | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| HitResult hitResult = HitResult::none; | |
| if (hitType == ListenerType::exit) | |
| { | |
| // The pointer left: the machines below forget it, and so does this. | |
| m_pointersOff.remove(pointerId); | |
| } | |
| if (nestedArtboard->isCollapsed() || nestedArtboard->isHidden() || | |
| nestedArtboard->isPaused()) | |
| { | |
| return hitResult; | |
| } | |
| Vec2D nestedPosition; | |
| bool offMesh; | |
| if (!nestedArtboard->worldToLocal(position, &nestedPosition, &offMesh)) | |
| { | |
| // Mounted artboard isn't ready or has a 0 scale transform. | |
| return hitResult; | |
| } | |
| // Past the edge of the mesh it's drawn through, or while this machine | |
| // misses the pointer itself, nothing in the artboard can be hit, but a | |
| // drag in progress keeps following the pointer. Past the edge, this | |
| // machine's own drags (unless it's turned off, as below) still start and | |
| // end in there as misses too. | |
| const bool pastMesh = canHit && offMesh; | |
| const bool missesMesh = (isMissableEvent(hitType) && | |
| (pastMesh || | |
| m_stateMachineInstance->inPointerMiss())) || | |
| (isDragPass(hitType) && pastMesh && | |
| !m_pointersOff.has(pointerId)); | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (nestedAnimation->is<NestedStateMachine>()) | |
| { | |
| auto nestedStateMachine = nestedAnimation->as<NestedStateMachine>(); | |
| m_pointersOff.keepOff( | |
| nestedStateMachine->stateMachineInstance(), | |
| pointerId); | |
| HostDispatchScope dispatch(nestedStateMachine->stateMachineInstance()); | |
| if (missesMesh) | |
| { | |
| nestedStateMachine->pointerMiss( | |
| nestedPosition, | |
| hitType, | |
| timeStamp, | |
| pointerId, | |
| button); | |
| } | |
| else if (canHit) | |
| { | |
| switch (hitType) | |
| { | |
| case ListenerType::down: | |
| hitResult = nestedStateMachine->pointerDown( | |
| nestedPosition, | |
| pointerId, | |
| button); | |
| break; | |
| case ListenerType::up: | |
| hitResult = nestedStateMachine->pointerUp( | |
| nestedPosition, | |
| pointerId, | |
| button); | |
| break; | |
| case ListenerType::move: | |
| hitResult = nestedStateMachine->pointerMove( | |
| nestedPosition, | |
| timeStamp, | |
| pointerId); | |
| break; | |
| // A machine this host turned off (its capture claim, or a drag | |
| // above) sits the host's drags out: their end would turn it | |
| // back on from below, inside the claim. | |
| case ListenerType::dragStart: | |
| if (!m_pointersOff.has(pointerId)) | |
| { | |
| nestedStateMachine | |
| ->dragStart(nestedPosition, timeStamp, pointerId, button); | |
| } | |
| break; | |
| case ListenerType::dragEnd: | |
| if (!m_pointersOff.has(pointerId)) | |
| { | |
| nestedStateMachine | |
| ->dragEnd(nestedPosition, timeStamp, pointerId, button); | |
| } | |
| break; | |
| case ListenerType::exit: | |
| hitResult = nestedStateMachine->pointerExit( | |
| nestedPosition, | |
| pointerId); | |
| break; | |
| case ListenerType::enter: | |
| case ListenerType::event: | |
| case ListenerType::click: | |
| case ListenerType::componentProvided: | |
| case ListenerType::textInput: | |
| case ListenerType::viewModel: | |
| case ListenerType::drag: | |
| case ListenerType::focus: | |
| case ListenerType::blur: | |
| case ListenerType::keyboard: | |
| case ListenerType::semanticAction: | |
| case ListenerType::gamepad: | |
| break; | |
| } | |
| } | |
| else | |
| { | |
| switch (hitType) | |
| { | |
| case ListenerType::down: | |
| case ListenerType::up: | |
| case ListenerType::move: | |
| // Something drawn above covers the pointer, which hasn't | |
| // left: a drag or claim here that turned it off still holds. | |
| if (auto smi = nestedStateMachine->stateMachineInstance()) | |
| { | |
| smi->hoverExit(nestedPosition, pointerId); | |
| } | |
| break; | |
| case ListenerType::exit: | |
| nestedStateMachine->pointerExit(nestedPosition, pointerId); | |
| break; | |
| case ListenerType::dragStart: | |
| case ListenerType::dragEnd: | |
| case ListenerType::enter: | |
| case ListenerType::event: | |
| case ListenerType::click: | |
| case ListenerType::componentProvided: | |
| case ListenerType::textInput: | |
| case ListenerType::viewModel: | |
| case ListenerType::drag: | |
| case ListenerType::focus: | |
| case ListenerType::blur: | |
| case ListenerType::keyboard: | |
| case ListenerType::semanticAction: | |
| case ListenerType::gamepad: | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| return hitResult; | |
| } | |
| void prepareEvent(Vec2D position, ListenerType hitType, int pointerId) | |
| override | |
| {} | |
| // A drag or a capture listener's claim turns off a pointer for the | |
| // nested machines too, not just for the host's own hit components. | |
| void enablePointerEvents(int pointerId) override | |
| { | |
| m_pointersOff.remove(pointerId); | |
| forEachStateMachine([pointerId](StateMachineInstance* smi) { | |
| smi->enablePointerEvents(pointerId); | |
| }); | |
| } | |
| void disablePointerEvents(int pointerId) override | |
| { | |
| m_pointersOff.add(pointerId); | |
| forEachStateMachine([pointerId](StateMachineInstance* smi) { | |
| smi->disablePointerEvents(pointerId); | |
| }); | |
| } | |
| bool hasCaptureListeners() override | |
| { | |
| bool has = false; | |
| forEachStateMachine([&](StateMachineInstance* smi) { | |
| if (!has && smi->hasCaptureListeners()) | |
| { | |
| has = true; | |
| } | |
| }); | |
| return has; | |
| } | |
| bool blocksCapture(Vec2D position) override | |
| { | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| if (nestedArtboard->isCollapsed() || nestedArtboard->isHidden() || | |
| nestedArtboard->isPaused()) | |
| { | |
| return false; | |
| } | |
| Vec2D nestedPosition; | |
| if (!drawnPosition(nestedArtboard, position, &nestedPosition)) | |
| { | |
| return false; | |
| } | |
| bool blocks = false; | |
| forEachStateMachine([&](StateMachineInstance* smi) { | |
| if (!blocks && smi->pointerOccludedAt(nestedPosition)) | |
| { | |
| blocks = true; | |
| } | |
| }); | |
| return blocks; | |
| } | |
| HitResult capturePointer( | |
| Vec2D position, | |
| ListenerType hitType, | |
| int pointerId, | |
| float timeStamp, | |
| PointerButton button) override | |
| { | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| if (nestedArtboard->isCollapsed() || nestedArtboard->isHidden() || | |
| nestedArtboard->isPaused()) | |
| { | |
| return HitResult::none; | |
| } | |
| Vec2D nestedPosition; | |
| bool offMesh; | |
| if (!nestedArtboard->worldToLocal(position, &nestedPosition, &offMesh)) | |
| { | |
| return HitResult::none; | |
| } | |
| // Past the edge of the mesh it's drawn through, its capture listeners | |
| // only see the pointer leave, as under an opaque hit drawn above. | |
| if (offMesh && isMissableEvent(hitType)) | |
| { | |
| hitType = ListenerType::exit; | |
| } | |
| HitResult result = HitResult::none; | |
| forEachStateMachine([&](StateMachineInstance* smi) { | |
| m_pointersOff.keepOff(smi, pointerId); | |
| result = combineHitResults( | |
| result, | |
| smi->capturePointer( | |
| nestedPosition, | |
| hitType, | |
| pointerId, | |
| timeStamp, | |
| button)); | |
| }); | |
| return result; | |
| } | |
| private: | |
| HostedPointersOff m_pointersOff; | |
| template <typename F> void forEachStateMachine(F&& visit) | |
| { | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (!nestedAnimation->is<NestedStateMachine>()) | |
| { | |
| continue; | |
| } | |
| if (auto smi = nestedAnimation->as<NestedStateMachine>() | |
| ->stateMachineInstance()) | |
| { | |
| visit(smi); | |
| } | |
| } | |
| } | |
| }; | |
| class HitComponentList : public HitComponent | |
| { | |
| public: | |
| HitComponentList( | |
| Component* componentList, | |
| StateMachineInstance* stateMachineInstance) | |
| : HitComponent(componentList, stateMachineInstance) | |
| {} | |
| ~HitComponentList() override {} | |
| bool hitTest(Vec2D position) const override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| if (componentList->isCollapsed() || componentList->isHidden()) | |
| { | |
| return false; | |
| } | |
| const auto& order = componentList->orderedListIndices(); | |
| for (auto it = order.rbegin(); it != order.rend(); ++it) | |
| { | |
| const int i = *it; | |
| Vec2D listPosition; | |
| if (!componentList->worldToLocal(position, &listPosition, i)) | |
| { | |
| // Mounted artboard isn't ready or has a 0 scale transform. | |
| continue; | |
| } | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine != nullptr && stateMachine->hitTest(listPosition)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool hitTestBounded(Vec2D position) const override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| if (componentList->isCollapsed() || componentList->isHidden()) | |
| { | |
| return false; | |
| } | |
| const auto& order = componentList->orderedListIndices(); | |
| for (auto it = order.rbegin(); it != order.rend(); ++it) | |
| { | |
| const int i = *it; | |
| Vec2D listPosition; | |
| if (!componentList->worldToLocal(position, &listPosition, i)) | |
| { | |
| continue; | |
| } | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine != nullptr && stateMachine->hitTestBounded(listPosition)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /// Maps a parent-space delta into item i. A delta is a vector, so it | |
| /// takes the mapping's linear part only. | |
| bool itemDelta( | |
| ArtboardComponentList* componentList, | |
| int i, | |
| Vec2D position, | |
| Vec2D listPosition, | |
| Vec2D delta, | |
| Vec2D& out) | |
| { | |
| Vec2D shifted; | |
| if (!componentList->worldToLocal(position + delta, &shifted, i)) | |
| { | |
| return false; | |
| } | |
| out = shifted - listPosition; | |
| return true; | |
| } | |
| // Items walk top-most first, like processEvent, and an opaque item blocks | |
| // the items behind it the way hitOpaque clears runningCanHit there. | |
| bool wantsScroll(Vec2D position, const ScrollEvent& event) override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| if (componentList->isCollapsed()) | |
| { | |
| return false; | |
| } | |
| const auto& order = componentList->orderedListIndices(); | |
| for (auto it = order.rbegin(); it != order.rend(); ++it) | |
| { | |
| const int i = *it; | |
| Vec2D listPosition; | |
| if (!componentList->worldToLocal(position, &listPosition, i)) | |
| { | |
| continue; | |
| } | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine == nullptr) | |
| { | |
| continue; | |
| } | |
| ScrollEvent mapped = event; | |
| if (!itemDelta( | |
| componentList, | |
| i, | |
| position, | |
| listPosition, | |
| event.delta, | |
| mapped.delta)) | |
| { | |
| continue; | |
| } | |
| if (stateMachine->wantsScroll(listPosition, mapped)) | |
| { | |
| return true; | |
| } | |
| if (stateMachine->scrollOccludedAt(listPosition)) | |
| { | |
| return false; | |
| } | |
| } | |
| return false; | |
| } | |
| bool hasScrollTarget(Vec2D position) override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| if (componentList->isCollapsed()) | |
| { | |
| return false; | |
| } | |
| const auto& order = componentList->orderedListIndices(); | |
| for (auto it = order.rbegin(); it != order.rend(); ++it) | |
| { | |
| const int i = *it; | |
| Vec2D listPosition; | |
| if (!componentList->worldToLocal(position, &listPosition, i)) | |
| { | |
| continue; | |
| } | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine == nullptr) | |
| { | |
| continue; | |
| } | |
| if (stateMachine->hasScrollTargetAt(listPosition)) | |
| { | |
| return true; | |
| } | |
| if (stateMachine->scrollOccludedAt(listPosition)) | |
| { | |
| return false; | |
| } | |
| } | |
| return false; | |
| } | |
| bool listensToButtonAt(Vec2D position, PointerButton button) override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| if (componentList->isCollapsed() || componentList->isHidden()) | |
| { | |
| return false; | |
| } | |
| const auto& order = componentList->orderedListIndices(); | |
| for (auto it = order.rbegin(); it != order.rend(); ++it) | |
| { | |
| const int i = *it; | |
| Vec2D listPosition; | |
| if (!componentList->worldToLocal(position, &listPosition, i)) | |
| { | |
| continue; | |
| } | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine != nullptr && | |
| stateMachine->listensToButtonAt(listPosition, button)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool occludesScroll(Vec2D position) override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| if (componentList->isCollapsed()) | |
| { | |
| return false; | |
| } | |
| const auto& order = componentList->orderedListIndices(); | |
| for (auto it = order.rbegin(); it != order.rend(); ++it) | |
| { | |
| const int i = *it; | |
| Vec2D listPosition; | |
| if (!componentList->worldToLocal(position, &listPosition, i)) | |
| { | |
| continue; | |
| } | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine != nullptr && | |
| stateMachine->scrollOccludedAt(listPosition)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool scrollGestureActive() override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| const auto& order = componentList->orderedListIndices(); | |
| if (componentList->isCollapsed()) | |
| { | |
| // A collapsed list never advances its items, so an idle timer | |
| // there can never clear the latch. End it outright. | |
| for (int i : order) | |
| { | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine != nullptr) | |
| { | |
| stateMachine->cancelScroll(); | |
| } | |
| } | |
| return false; | |
| } | |
| for (int i : order) | |
| { | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine != nullptr && stateMachine->hasScrollLatch()) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| void cancelScroll() override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| for (int i : componentList->orderedListIndices()) | |
| { | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine != nullptr) | |
| { | |
| stateMachine->cancelScroll(); | |
| } | |
| } | |
| } | |
| HitResult processScroll( | |
| Vec2D position, | |
| const ScrollEvent& event, | |
| float timeStamp, | |
| int pointerId) override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| if (componentList->isCollapsed()) | |
| { | |
| return HitResult::none; | |
| } | |
| const auto& order = componentList->orderedListIndices(); | |
| // A latched item gets the event first; only then can another take it. | |
| for (int pass = 0; pass < 2; pass++) | |
| { | |
| for (auto it = order.rbegin(); it != order.rend(); ++it) | |
| { | |
| const int i = *it; | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine == nullptr || | |
| (pass == 0 && !stateMachine->hasScrollLatch())) | |
| { | |
| continue; | |
| } | |
| Vec2D listPosition; | |
| if (!componentList->worldToLocal(position, &listPosition, i)) | |
| { | |
| continue; | |
| } | |
| ScrollEvent mapped = event; | |
| if (!itemDelta( | |
| componentList, | |
| i, | |
| position, | |
| listPosition, | |
| event.delta, | |
| mapped.delta)) | |
| { | |
| continue; | |
| } | |
| auto result = stateMachine->pointerScroll( | |
| listPosition, | |
| mapped, | |
| timeStamp, | |
| pointerId); | |
| if (pass == 0 || result != HitResult::none) | |
| { | |
| return result; | |
| } | |
| if (stateMachine->scrollOccludedAt(listPosition)) | |
| { | |
| return HitResult::none; | |
| } | |
| } | |
| } | |
| return HitResult::none; | |
| } | |
| HitResult processEvent( | |
| Vec2D position, | |
| ListenerType hitType, | |
| bool canHit, | |
| float timeStamp, | |
| int pointerId, | |
| PointerButton button) override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| HitResult hitResult = HitResult::none; | |
| bool runningCanHit = canHit; | |
| if (hitType == ListenerType::exit) | |
| { | |
| // See HitNestedArtboard::processEvent. | |
| m_pointersOff.remove(pointerId); | |
| } | |
| if (componentList->isCollapsed() || componentList->isHidden()) | |
| { | |
| return hitResult; | |
| } | |
| const auto& order = componentList->orderedListIndices(); | |
| for (auto it = order.rbegin(); it != order.rend(); ++it) | |
| { | |
| const int i = *it; | |
| Vec2D listPosition; | |
| bool hit = componentList->worldToLocal(position, &listPosition, i); | |
| if (!hit) | |
| { | |
| continue; | |
| } | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine != nullptr) | |
| { | |
| m_pointersOff.keepOff(stateMachine, pointerId); | |
| HostDispatchScope dispatch(stateMachine); | |
| HitResult itemHitResult = HitResult::none; | |
| if (runningCanHit) | |
| { | |
| switch (hitType) | |
| { | |
| case ListenerType::down: | |
| itemHitResult = stateMachine->pointerDown( | |
| listPosition, | |
| pointerId, | |
| button); | |
| break; | |
| case ListenerType::up: | |
| itemHitResult = stateMachine->pointerUp( | |
| listPosition, | |
| pointerId, | |
| button); | |
| break; | |
| case ListenerType::move: | |
| itemHitResult = stateMachine->pointerMove( | |
| listPosition, | |
| timeStamp, | |
| pointerId); | |
| break; | |
| case ListenerType::exit: | |
| itemHitResult = stateMachine->pointerExit( | |
| listPosition, | |
| pointerId); | |
| break; | |
| // See HitNestedArtboard::processEvent. | |
| case ListenerType::dragStart: | |
| if (!m_pointersOff.has(pointerId)) | |
| { | |
| stateMachine | |
| ->dragStart(listPosition, 0, true, pointerId, button); | |
| } | |
| break; | |
| case ListenerType::dragEnd: | |
| if (!m_pointersOff.has(pointerId)) | |
| { | |
| stateMachine->dragEnd(listPosition, 0, pointerId, button); | |
| } | |
| break; | |
| case ListenerType::enter: | |
| case ListenerType::event: | |
| case ListenerType::click: | |
| case ListenerType::componentProvided: | |
| case ListenerType::textInput: | |
| case ListenerType::viewModel: | |
| case ListenerType::drag: | |
| case ListenerType::focus: | |
| case ListenerType::blur: | |
| case ListenerType::keyboard: | |
| case ListenerType::semanticAction: | |
| case ListenerType::gamepad: | |
| break; | |
| } | |
| } | |
| else if ( | |
| isMissableEvent(hitType) && m_stateMachineInstance->inPointerMiss()) | |
| { | |
| stateMachine->pointerMiss( | |
| listPosition, | |
| hitType, | |
| timeStamp, | |
| pointerId, | |
| button); | |
| } | |
| else | |
| { | |
| switch (hitType) | |
| { | |
| case ListenerType::down: | |
| case ListenerType::up: | |
| case ListenerType::move: | |
| // Covered by an item or something drawn above, not left: see | |
| // HitNestedArtboard::processEvent. | |
| stateMachine->hoverExit(listPosition, pointerId); | |
| break; | |
| case ListenerType::exit: | |
| stateMachine->pointerExit(listPosition, pointerId); | |
| break; | |
| case ListenerType::dragStart: | |
| case ListenerType::dragEnd: | |
| case ListenerType::enter: | |
| case ListenerType::event: | |
| case ListenerType::click: | |
| case ListenerType::componentProvided: | |
| case ListenerType::textInput: | |
| case ListenerType::viewModel: | |
| case ListenerType::drag: | |
| case ListenerType::focus: | |
| case ListenerType::blur: | |
| case ListenerType::keyboard: | |
| case ListenerType::semanticAction: | |
| case ListenerType::gamepad: | |
| break; | |
| } | |
| } | |
| if ((hitResult == HitResult::none && | |
| (itemHitResult == HitResult::hit || | |
| itemHitResult == HitResult::hitOpaque)) || | |
| (hitResult == HitResult::hit && | |
| itemHitResult == HitResult::hitOpaque)) | |
| { | |
| hitResult = itemHitResult; | |
| } | |
| if (hitResult == HitResult::hitOpaque) | |
| { | |
| runningCanHit = false; | |
| } | |
| } | |
| } | |
| return hitResult; | |
| } | |
| HitResult processGamepadInvocation( | |
| const ListenerInvocation& invocation, | |
| ScriptedDrawable* alreadyDispatched) override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| HitResult hitResult = HitResult::none; | |
| bool runningCanHit = true; | |
| if (componentList->isCollapsed()) | |
| { | |
| return hitResult; | |
| } | |
| const auto& order = componentList->orderedListIndices(); | |
| for (auto it = order.rbegin(); it != order.rend(); ++it) | |
| { | |
| const int i = *it; | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine != nullptr) | |
| { | |
| HitResult itemHitResult = HitResult::none; | |
| if (runningCanHit) | |
| { | |
| itemHitResult = stateMachine->broadcastGamepadToScriptedDrawables( | |
| invocation, | |
| alreadyDispatched); | |
| } | |
| if ((hitResult == HitResult::none && | |
| (itemHitResult == HitResult::hit || | |
| itemHitResult == HitResult::hitOpaque)) || | |
| (hitResult == HitResult::hit && | |
| itemHitResult == HitResult::hitOpaque)) | |
| { | |
| hitResult = itemHitResult; | |
| } | |
| if (hitResult == HitResult::hitOpaque) | |
| { | |
| runningCanHit = false; | |
| } | |
| } | |
| } | |
| return hitResult; | |
| } | |
| void prepareEvent(Vec2D position, ListenerType hitType, int pointerId) | |
| override | |
| {} | |
| void enablePointerEvents(int pointerId) override | |
| { | |
| m_pointersOff.remove(pointerId); | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| for (int i : componentList->orderedListIndices()) | |
| { | |
| if (auto stateMachine = componentList->stateMachineInstance(i)) | |
| { | |
| stateMachine->enablePointerEvents(pointerId); | |
| } | |
| } | |
| } | |
| void disablePointerEvents(int pointerId) override | |
| { | |
| m_pointersOff.add(pointerId); | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| for (int i : componentList->orderedListIndices()) | |
| { | |
| if (auto stateMachine = componentList->stateMachineInstance(i)) | |
| { | |
| stateMachine->disablePointerEvents(pointerId); | |
| } | |
| } | |
| } | |
| bool hasCaptureListeners() override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| for (int i : componentList->orderedListIndices()) | |
| { | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine != nullptr && stateMachine->hasCaptureListeners()) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool blocksCapture(Vec2D position) override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| if (componentList->isCollapsed() || componentList->isHidden()) | |
| { | |
| return false; | |
| } | |
| for (int i : componentList->orderedListIndices()) | |
| { | |
| Vec2D listPosition; | |
| if (!componentList->worldToLocal(position, &listPosition, i)) | |
| { | |
| continue; | |
| } | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine != nullptr && | |
| stateMachine->pointerOccludedAt(listPosition)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // Items run top-most first, and an opaque item hides the items behind | |
| // it, which then only see the pointer leave -- as in processEvent. Only | |
| // items with a capture listener somewhere inside are visited, and the | |
| // items above them only tested as far as needed. | |
| HitResult capturePointer( | |
| Vec2D position, | |
| ListenerType hitType, | |
| int pointerId, | |
| float timeStamp, | |
| PointerButton button) override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| if (componentList->isCollapsed() || componentList->isHidden()) | |
| { | |
| return HitResult::none; | |
| } | |
| HitResult result = HitResult::none; | |
| bool occluded = false; | |
| const auto& order = componentList->orderedListIndices(); | |
| const size_t count = order.size(); | |
| // Items are counted top-most first; this many have been tested. | |
| size_t tested = 0; | |
| for (size_t item = 0; item < count; item++) | |
| { | |
| const int i = order[count - 1 - item]; | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine == nullptr || !stateMachine->hasCaptureListeners()) | |
| { | |
| continue; | |
| } | |
| Vec2D listPosition; | |
| if (!componentList->worldToLocal(position, &listPosition, i)) | |
| { | |
| continue; | |
| } | |
| for (; !occluded && tested < item; tested++) | |
| { | |
| const int above = order[count - 1 - tested]; | |
| auto aboveMachine = componentList->stateMachineInstance(above); | |
| Vec2D abovePosition; | |
| if (aboveMachine != nullptr && | |
| componentList->worldToLocal(position, &abovePosition, above) && | |
| aboveMachine->pointerOccludedAt(abovePosition)) | |
| { | |
| occluded = true; | |
| } | |
| } | |
| m_pointersOff.keepOff(stateMachine, pointerId); | |
| result = combineHitResults( | |
| result, | |
| stateMachine->capturePointer( | |
| listPosition, | |
| occluded ? ListenerType::exit : hitType, | |
| pointerId, | |
| timeStamp, | |
| button)); | |
| } | |
| return result; | |
| } | |
| private: | |
| HostedPointersOff m_pointersOff; | |
| }; | |
| #ifdef WITH_RIVE_TEXT | |
| /// Forwards pointer events to the state machines of a Text's styles that | |
| /// listen to the runs drawn with them (see | |
| /// StateMachineInstance::serveTextStyle). Styles and their instances come and | |
| /// go (list updates, style swaps), so they are looked up on every event, | |
| /// never kept. | |
| class HitTextStyleRuns : public HitComponent | |
| { | |
| public: | |
| HitTextStyleRuns(Text* text, StateMachineInstance* stateMachineInstance) | |
| : HitComponent(text, stateMachineInstance) | |
| {} | |
| Text* text() const { return m_component->as<Text>(); } | |
| bool hidden() const | |
| { | |
| auto text = this->text(); | |
| return text->isCollapsed() || text->isHidden(); | |
| } | |
| bool hitTest(Vec2D position) const override | |
| { | |
| if (hidden()) | |
| { | |
| return false; | |
| } | |
| std::vector<StateMachineInstance*> stateMachines; | |
| text()->runStateMachines(stateMachines); | |
| for (auto stateMachine : stateMachines) | |
| { | |
| if (stateMachine->hitTest(position)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool listensToButtonAt(Vec2D position, PointerButton button) override | |
| { | |
| if (hidden()) | |
| { | |
| return false; | |
| } | |
| m_stateMachines.clear(); | |
| text()->runStateMachines(m_stateMachines); | |
| for (auto stateMachine : m_stateMachines) | |
| { | |
| if (stateMachine->listensToButtonAt(position, button)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| HitResult processEvent( | |
| Vec2D position, | |
| ListenerType hitType, | |
| bool canHit, | |
| float timeStamp, | |
| int pointerId, | |
| PointerButton button) override | |
| { | |
| HitResult hitResult = HitResult::none; | |
| bool runningCanHit = canHit && !hidden(); | |
| // Collected first: a listener may change the styles while this walks | |
| // them. | |
| m_stateMachines.clear(); | |
| text()->runStateMachines(m_stateMachines); | |
| for (auto stateMachine : m_stateMachines) | |
| { | |
| HitResult styleHitResult = HitResult::none; | |
| if (runningCanHit) | |
| { | |
| switch (hitType) | |
| { | |
| case ListenerType::down: | |
| styleHitResult = stateMachine->pointerDown( | |
| position, | |
| pointerId, | |
| button); | |
| break; | |
| case ListenerType::up: | |
| styleHitResult = stateMachine->pointerUp( | |
| position, | |
| pointerId, | |
| button); | |
| break; | |
| case ListenerType::move: | |
| styleHitResult = stateMachine->pointerMove( | |
| position, | |
| timeStamp, | |
| pointerId); | |
| break; | |
| case ListenerType::exit: | |
| styleHitResult = stateMachine->pointerExit(position, pointerId); | |
| break; | |
| case ListenerType::dragStart: | |
| stateMachine->dragStart(position, 0, true, pointerId, button); | |
| break; | |
| case ListenerType::dragEnd: | |
| stateMachine->dragEnd(position, 0, pointerId, button); | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| else if ( | |
| isMissableEvent(hitType) && m_stateMachineInstance->inPointerMiss()) | |
| { | |
| stateMachine | |
| ->pointerMiss(position, hitType, timeStamp, pointerId, button); | |
| } | |
| else | |
| { | |
| switch (hitType) | |
| { | |
| case ListenerType::down: | |
| case ListenerType::up: | |
| case ListenerType::move: | |
| case ListenerType::exit: | |
| stateMachine->pointerExit(position, pointerId); | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| if (stateMachine->needsAdvance()) | |
| { | |
| m_stateMachineInstance->markNeedsAdvance(); | |
| } | |
| if ((hitResult == HitResult::none && | |
| (styleHitResult == HitResult::hit || | |
| styleHitResult == HitResult::hitOpaque)) || | |
| (hitResult == HitResult::hit && | |
| styleHitResult == HitResult::hitOpaque)) | |
| { | |
| hitResult = styleHitResult; | |
| } | |
| if (hitResult == HitResult::hitOpaque) | |
| { | |
| runningCanHit = false; | |
| } | |
| } | |
| return hitResult; | |
| } | |
| HitResult processGamepadInvocation( | |
| const ListenerInvocation& invocation, | |
| ScriptedDrawable* alreadyDispatched) override | |
| { | |
| return HitResult::none; | |
| } | |
| void prepareEvent(Vec2D position, ListenerType hitType, int pointerId) | |
| override | |
| {} | |
| private: | |
| // Reused across events. | |
| std::vector<StateMachineInstance*> m_stateMachines; | |
| }; | |
| /// The runs of a Text drawn with one style, which the pointer listeners with | |
| /// no target in that style's state machine listen to as one target (see | |
| /// StateMachineInstance::serveTextStyle). Runs come and go and change styles | |
| /// (list updates, swaps), so they're found on every hit test, never kept; the | |
| /// Text makes them hit targets. | |
| class HitTextStyleTarget : public HitDrawable | |
| { | |
| public: | |
| HitTextStyleTarget( | |
| Text* text, | |
| const TextStyleReference* style, | |
| StateMachineInstance* stateMachineInstance) | |
| : HitDrawable(text, text, stateMachineInstance, false) | |
| , m_style(style) | |
| {} | |
| bool hitTest(Vec2D position) const override | |
| { | |
| for (auto run : m_drawable->as<Text>()->runs()) | |
| { | |
| if (run->styleReference() == m_style && | |
| run->hitTestPoint(position, true, true)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| private: | |
| const TextStyleReference* m_style; | |
| }; | |
| #endif | |
| class ListenerViewModel; | |
| // Helper that holds one view model property reference, listens to its dirt, | |
| // and reports the parent ListenerViewModel when the property changes. | |
| class ListenerViewModelPropertyBinding : public ViewModelValueDependent | |
| { | |
| public: | |
| ListenerViewModelPropertyBinding( | |
| ListenerViewModel* parent, | |
| ViewModelInstanceValue* vmProp); | |
| virtual ~ListenerViewModelPropertyBinding(); | |
| void addDirt(ComponentDirt value, bool recurse) override; | |
| void relinkDataBind() override; | |
| ViewModelInstanceValue* value() { return m_viewModelInstanceValue.get(); } | |
| protected: | |
| ListenerViewModel* m_parent = nullptr; | |
| rive::rcp<ViewModelInstanceValue> m_viewModelInstanceValue = nullptr; | |
| void clearDataContext(); | |
| }; | |
| class ListenerViewModelPropertyBindingListener | |
| : public ListenerViewModelPropertyBinding | |
| { | |
| public: | |
| ListenerViewModelPropertyBindingListener( | |
| ListenerViewModel* parent, | |
| ViewModelInstanceValue* vmProp, | |
| const StateMachineListenerSingle* listener); | |
| void relinkDataBind() override; | |
| private: | |
| const StateMachineListenerSingle* m_listener; | |
| }; | |
| class ListenerViewModelPropertyBindingInput | |
| : public ListenerViewModelPropertyBinding | |
| { | |
| public: | |
| ListenerViewModelPropertyBindingInput( | |
| ListenerViewModel* parent, | |
| ViewModelInstanceValue* vmProp, | |
| const ListenerInputTypeViewModel* listenerInput); | |
| void relinkDataBind() override; | |
| private: | |
| const ListenerInputTypeViewModel* m_listenerInput; | |
| }; | |
| class ListenerViewModel | |
| { | |
| public: | |
| virtual ~ListenerViewModel(); | |
| ListenerViewModel( | |
| StateMachineInstance* smInstance, | |
| const StateMachineListener* listener) | |
| : m_stateMachineInstance(smInstance) | |
| , m_listener(listener) | |
| {} | |
| void clearDataContext() { m_propertyBindings.clear(); } | |
| void bindFromContext(rcp<DataContext> dataContext) | |
| { | |
| m_dataContext = dataContext; | |
| clearDataContext(); | |
| if (m_listener->is<StateMachineListenerSingle>()) | |
| { | |
| auto vmProp = dataContext->getViewModelProperty( | |
| m_listener->as<StateMachineListenerSingle>()->dataBindPath()); | |
| if (vmProp != nullptr) | |
| { | |
| m_propertyBindings.push_back( | |
| std::make_unique<ListenerViewModelPropertyBindingListener>( | |
| this, | |
| vmProp, | |
| m_listener->as<StateMachineListenerSingle>())); | |
| } | |
| } | |
| else | |
| { | |
| size_t index = 0; | |
| while (index < m_listener->listenerInputTypeCount()) | |
| { | |
| auto listenerInputType = m_listener->listenerInputType(index); | |
| if (listenerInputType->is<ListenerInputTypeViewModel>()) | |
| { | |
| auto listenerInputTypeVM = listenerInputType | |
| ->as<ListenerInputTypeViewModel>(); | |
| auto vmProp = dataContext->getViewModelProperty( | |
| listenerInputTypeVM->dataBindPath()); | |
| if (vmProp != nullptr) | |
| { | |
| m_propertyBindings.push_back( | |
| std::make_unique<ListenerViewModelPropertyBindingInput>( | |
| this, | |
| vmProp, | |
| listenerInputTypeVM)); | |
| } | |
| } | |
| index++; | |
| } | |
| } | |
| for (auto& binding : m_propertyBindings) | |
| { | |
| reportIfPending(binding->value()); | |
| } | |
| } | |
| void reportToStateMachine() | |
| { | |
| m_stateMachineInstance->reportListenerViewModel(this); | |
| } | |
| // A trigger fired before this listener was listening to it can still be | |
| // pending (fired by a script's init(), which runs as the artboard binds, | |
| // before this does, or by the host before binding): report it so it isn't | |
| // lost. A fire whose frame has passed isn't, so it isn't replayed. | |
| void reportIfPending(ViewModelInstanceValue* value) | |
| { | |
| if (value != nullptr && value->is<ViewModelInstanceTrigger>() && | |
| m_stateMachineInstance->viewModelTriggerPendingForListeners(value)) | |
| { | |
| reportToStateMachine(); | |
| } | |
| } | |
| const StateMachineListener* listener() { return m_listener; } | |
| DataContext* dataContext() | |
| { | |
| if (m_dataContext) | |
| { | |
| return m_dataContext.get(); | |
| } | |
| return nullptr; | |
| } | |
| private: | |
| StateMachineInstance* m_stateMachineInstance = nullptr; | |
| const StateMachineListener* m_listener = nullptr; | |
| rcp<DataContext> m_dataContext = nullptr; | |
| std::vector<std::unique_ptr<ListenerViewModelPropertyBinding>> | |
| m_propertyBindings; | |
| }; | |
| ListenerViewModelPropertyBinding::ListenerViewModelPropertyBinding( | |
| ListenerViewModel* parent, | |
| ViewModelInstanceValue* vmProp) | |
| : m_parent(parent) | |
| , m_viewModelInstanceValue(rive::ref_rcp(vmProp)) | |
| { | |
| vmProp->addDependent(this); | |
| } | |
| void ListenerViewModelPropertyBinding::relinkDataBind() {} | |
| ListenerViewModelPropertyBinding::~ListenerViewModelPropertyBinding() | |
| { | |
| clearDataContext(); | |
| } | |
| void ListenerViewModelPropertyBinding::clearDataContext() | |
| { | |
| if (m_viewModelInstanceValue != nullptr) | |
| { | |
| m_viewModelInstanceValue->removeDependent(this); | |
| m_viewModelInstanceValue = nullptr; | |
| } | |
| } | |
| ListenerViewModelPropertyBindingListener:: | |
| ListenerViewModelPropertyBindingListener( | |
| ListenerViewModel* parent, | |
| ViewModelInstanceValue* vmProp, | |
| const StateMachineListenerSingle* listener) | |
| : ListenerViewModelPropertyBinding(parent, vmProp) | |
| , m_listener(listener) | |
| {} | |
| void ListenerViewModelPropertyBindingListener::relinkDataBind() | |
| { | |
| auto dataContext = m_parent->dataContext(); | |
| if (dataContext) | |
| { | |
| auto vmProp = dataContext->getViewModelProperty(m_listener->dataBindPath()); | |
| if (vmProp != m_viewModelInstanceValue.get()) | |
| { | |
| clearDataContext(); | |
| if (vmProp != nullptr) | |
| { | |
| m_viewModelInstanceValue = ref_rcp(vmProp); | |
| vmProp->addDependent(this); | |
| m_parent->reportIfPending(vmProp); | |
| } | |
| } | |
| } | |
| } | |
| ListenerViewModelPropertyBindingInput::ListenerViewModelPropertyBindingInput( | |
| ListenerViewModel* parent, | |
| ViewModelInstanceValue* vmProp, | |
| const ListenerInputTypeViewModel* listenerInput) | |
| : ListenerViewModelPropertyBinding(parent, vmProp) | |
| , m_listenerInput(listenerInput) | |
| {} | |
| void ListenerViewModelPropertyBindingInput::relinkDataBind() | |
| { | |
| auto dataContext = m_parent->dataContext(); | |
| if (dataContext) | |
| { | |
| auto vmProp = dataContext->getViewModelProperty( | |
| m_listenerInput->dataBindPath()); | |
| if (vmProp != m_viewModelInstanceValue.get()) | |
| { | |
| clearDataContext(); | |
| if (vmProp != nullptr) | |
| { | |
| m_viewModelInstanceValue = ref_rcp(vmProp); | |
| vmProp->addDependent(this); | |
| m_parent->reportIfPending(vmProp); | |
| } | |
| } | |
| } | |
| } | |
| void ListenerViewModelPropertyBinding::addDirt( | |
| ComponentDirt value, | |
| bool recurse) | |
| { | |
| // Every change reports, a trigger's included: its count only changes | |
| // when it fires. | |
| if (m_parent != nullptr && m_viewModelInstanceValue != nullptr) | |
| { | |
| m_parent->reportToStateMachine(); | |
| } | |
| } | |
| ListenerViewModel::~ListenerViewModel() { clearDataContext(); } | |
| } // namespace rive | |
| HitResult StateMachineInstance::updateListeners( | |
| Vec2D position, | |
| ListenerType hitType, | |
| int pointerId, | |
| float timeStamp, | |
| PointerButton button) | |
| { | |
| const Vec2D hostPosition = position; | |
| // A primary press may take focus from the focused component when it lands | |
| // outside it (FocusManager::pressEnded). Only the root sees every press in | |
| // root space; a nested instance gets a press in its own space, and only | |
| // when it lands inside it. It begins ahead of the capture phase, whose | |
| // listeners can move focus too: focus a press moved is not blurred by it. | |
| FocusManager* pressFocusManager = nullptr; | |
| if (hitType == ListenerType::down && button == PointerButton::primary && | |
| m_artboardInstance != nullptr && m_artboardInstance->isRoot()) | |
| { | |
| pressFocusManager = focusManager(); | |
| } | |
| if (pressFocusManager != nullptr) | |
| { | |
| pressFocusManager->pressBegan(); | |
| } | |
| // The capture phase runs once per event, from the root of the dispatch, | |
| // over every machine in the tree; the machines it reaches through the | |
| // normal phase below must not run it again. Skipped outright while | |
| // nothing anywhere has a capture listener. A miss reaches capture | |
| // listeners as the pointer leaving, as off a nested mesh in a host's | |
| // capture pass. | |
| HitResult captureResult = HitResult::none; | |
| if (!m_hostDispatching && !m_captureRunByHost && | |
| s_liveCaptureMachines.load(std::memory_order_relaxed) != 0 && | |
| treeHasCaptureListeners()) | |
| { | |
| captureResult = capturePointer( | |
| position, | |
| m_inPointerMiss && isMissableEvent(hitType) ? ListenerType::exit | |
| : hitType, | |
| pointerId, | |
| timeStamp, | |
| button); | |
| } | |
| // Listener hit-testing runs in content space, so undo the frame origin and | |
| // the artboard's own rotation/scale first. | |
| // | |
| // A degenerate (0 scale) self transform has no inverse: the contents | |
| // collapse to nothing, so nothing in them can be hit. We still run the pass | |
| // with every group forced to miss rather than returning early, so hover | |
| // unwinds and pending exits fire, and we cancel any gesture in flight so a | |
| // press held across the collapse can't resume when the scale comes back. | |
| bool contentsCollapsed = !mapToContentSpace(position); | |
| // First reset all listener groups before processing the events | |
| for (const auto& listenerGroup : m_listenerGroups) | |
| { | |
| listenerGroup.get()->reset(pointerId); | |
| } | |
| // Drag ends owed by cancellation, dispatched once the pass below has had a | |
| // chance to emit its hover exits. | |
| std::vector<std::pair<int, PointerButton>> dragEnded; | |
| if (contentsCollapsed) | |
| { | |
| // canHit alone won't do this: it marks a target as occluded, and an | |
| // occluded target deliberately keeps its press so a drag survives the | |
| // pointer moving over other things (see ListenerGroup::processEvent, | |
| // where the phase only resets on down/up and the drag branch ignores | |
| // canHit). Collapsed content isn't occluded, it's gone. | |
| // | |
| // Every tracked pointer is cancelled, not just the one that delivered | |
| // this event: the contents are gone for all of them. The drag ends are | |
| // only collected here -- dispatching one re-enters updateListeners, | |
| // whose reset() overwrites isPrevHovered with the isHovered this pass | |
| // already cleared, so any exit still pending would be swallowed, and | |
| // whose enablePointerEvents() resets every group's phase, so groups not | |
| // yet cancelled would look like they had nothing in flight. | |
| for (const auto& listenerGroup : m_listenerGroups) | |
| { | |
| listenerGroup.get()->cancelPointers(position, timeStamp, dragEnded); | |
| } | |
| } | |
| else | |
| { | |
| // Next prepare the event to set the common hover status for each group. | |
| // Skipped when collapsed so every group stays unhovered. | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| hitShape->prepareEvent(position, hitType, pointerId); | |
| } | |
| } | |
| bool hitSomething = false; | |
| bool hitOpaque = false; | |
| // Process the events | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| HitResult hitResult = hitShape->processEvent( | |
| position, | |
| hitType, | |
| !m_inPointerMiss && !hitOpaque && !contentsCollapsed, | |
| timeStamp, | |
| pointerId, | |
| button); | |
| if (hitResult != HitResult::none) | |
| { | |
| hitSomething = true; | |
| if (hitResult == HitResult::hitOpaque) | |
| { | |
| hitOpaque = true; | |
| } | |
| } | |
| } | |
| // Hover exits have been emitted, so it's safe to let dragEnd re-enter now. | |
| for (auto& ended : dragEnded) | |
| { | |
| dragEnd(position, timeStamp, ended.first, ended.second); | |
| } | |
| if (pressFocusManager != nullptr) | |
| { | |
| pressFocusManager->pressEnded(position); | |
| } | |
| // Finally release events that are complete | |
| if (hitType == ListenerType::exit) | |
| { | |
| for (const auto& listenerGroup : m_listenerGroups) | |
| { | |
| listenerGroup.get()->releaseEvent(pointerId); | |
| } | |
| // A pointer that left is forgotten everywhere here: the capture groups | |
| // too, which a host's capture pass reached first, and whatever turned | |
| // it off from outside. hoverExit turns it back off for a pointer that | |
| // only passed under something drawn above. | |
| if (auto* extras = inputExtras()) | |
| { | |
| for (const auto& group : extras->captureGroups) | |
| { | |
| group->releaseEvent(pointerId); | |
| } | |
| } | |
| m_disabledPointerIds.eraseAll(pointerId); | |
| } | |
| // A claimed gesture ends with its release, which the pass above has just | |
| // kept from the claimed targets. They come back on now, and a move puts | |
| // their hover back where the pointer is, as dragEnd does. | |
| if ((hitType == ListenerType::up || hitType == ListenerType::exit) && | |
| releasePointerClaims(pointerId, button, hitType == ListenerType::exit) && | |
| hitType == ListenerType::up) | |
| { | |
| pointerMove(hostPosition, timeStamp, pointerId); | |
| } | |
| return combineHitResults( | |
| captureResult, | |
| hitSomething ? hitOpaque ? HitResult::hitOpaque : HitResult::hit | |
| : HitResult::none); | |
| } | |
| HitResult StateMachineInstance::capturePointer( | |
| Vec2D position, | |
| ListenerType hitType, | |
| int pointerId, | |
| float timeStamp, | |
| PointerButton button) | |
| { | |
| if (s_liveCaptureMachines.load(std::memory_order_relaxed) == 0) | |
| { | |
| return HitResult::none; | |
| } | |
| #ifdef TESTING | |
| sm_capturePassVisits++; | |
| #endif | |
| auto* extras = inputExtras(); | |
| const bool hasLocal = extras != nullptr && !extras->captureHits.empty(); | |
| if (!hasLocal && m_artboardInstance->nestedArtboards().empty() && | |
| m_artboardInstance->artboardComponentLists().empty()) | |
| { | |
| return HitResult::none; | |
| } | |
| // Collapsed contents hit nothing; the pass still runs so hover unwinds. | |
| const bool collapsed = !mapToContentSpace(position); | |
| HitResult result = HitResult::none; | |
| if (hasLocal) | |
| { | |
| result = captureLocalPointer( | |
| position, | |
| hitType, | |
| pointerId, | |
| timeStamp, | |
| button, | |
| collapsed); | |
| } | |
| // Then the nested machines, which only come after every capture listener | |
| // here: a host's capture listener runs before any of its contents'. Only | |
| // hosts with a capture listener somewhere inside are visited, and what is | |
| // drawn above them only tested as far as the lowest of those. One under an | |
| // opaque hit drawn above it only sees the pointer leave, as in the normal | |
| // phase. | |
| bool occluded = collapsed; | |
| size_t tested = 0; | |
| for (size_t i = 0; i < m_hitComponents.size(); i++) | |
| { | |
| auto hitComponent = m_hitComponents[i].get(); | |
| if (!hitComponent->hasCaptureListeners()) | |
| { | |
| continue; | |
| } | |
| for (; !occluded && tested < i; tested++) | |
| { | |
| if (m_hitComponents[tested]->blocksCapture(position)) | |
| { | |
| occluded = true; | |
| } | |
| } | |
| result = combineHitResults( | |
| result, | |
| hitComponent->capturePointer( | |
| position, | |
| occluded ? ListenerType::exit : hitType, | |
| pointerId, | |
| timeStamp, | |
| button)); | |
| } | |
| return result; | |
| } | |
| HitResult StateMachineInstance::captureLocalPointer( | |
| Vec2D position, | |
| ListenerType hitType, | |
| int pointerId, | |
| float timeStamp, | |
| PointerButton button, | |
| bool collapsed) | |
| { | |
| auto& extras = *inputExtras(); | |
| // A claim lasts until its release. One still here at the next press of | |
| // its button lost its release on the way (the machine wasn't reached for | |
| // it), so it goes before this press can take a new one. A press of | |
| // another button says nothing about it: both can be held. | |
| if (hitType == ListenerType::down) | |
| { | |
| releasePointerClaims(pointerId, button, false); | |
| } | |
| for (const auto& group : extras.captureGroups) | |
| { | |
| group->reset(pointerId); | |
| } | |
| std::vector<std::pair<int, PointerButton>> dragEnded; | |
| if (collapsed) | |
| { | |
| // As in updateListeners: contents that are gone can't hold a press. | |
| for (const auto& group : extras.captureGroups) | |
| { | |
| group->cancelPointers(position, timeStamp, dragEnded); | |
| } | |
| } | |
| else if (hitType != ListenerType::exit) | |
| { | |
| for (const auto& captureHit : extras.captureHits) | |
| { | |
| if (!captureHit.group->isPointerDisabled(pointerId) && | |
| captureHit.hit->hitTest(position)) | |
| { | |
| captureHit.group->hover(pointerId); | |
| } | |
| } | |
| } | |
| // What is drawn above each capture target and opaque under the pointer. | |
| // A target's own contents don't count: a layout's children always draw | |
| // over it, and an opaque one among them is part of what it captures for. | |
| struct Occluder | |
| { | |
| size_t drawIndex; | |
| Component* component; | |
| }; | |
| std::vector<Occluder> occluders; | |
| if (!collapsed && hitType != ListenerType::exit) | |
| { | |
| size_t lowest = 0; | |
| for (const auto& captureHit : extras.captureHits) | |
| { | |
| lowest = std::max(lowest, captureHit.drawIndex); | |
| } | |
| for (size_t i = 0; i < lowest && i < m_hitComponents.size(); i++) | |
| { | |
| auto hitComponent = m_hitComponents[i].get(); | |
| if (hitComponent->blocksCapture(position)) | |
| { | |
| occluders.push_back({i, hierarchyComponent(hitComponent)}); | |
| } | |
| } | |
| } | |
| HitResult result = HitResult::none; | |
| for (size_t i = 0; i < extras.captureHits.size(); i++) | |
| { | |
| // By index: a listener's actions can claim, which only grows | |
| // pointerClaims, but nothing here may hold on to a reference across | |
| // them. | |
| auto captureHit = extras.captureHits[i]; | |
| auto group = captureHit.group; | |
| if (group->isConsumed() || group->isPointerDisabled(pointerId)) | |
| { | |
| continue; | |
| } | |
| bool canHit = !collapsed; | |
| for (const auto& occluder : occluders) | |
| { | |
| if (occluder.drawIndex < captureHit.drawIndex && | |
| !componentWithin(occluder.component, captureHit.target)) | |
| { | |
| canHit = false; | |
| break; | |
| } | |
| } | |
| group->processEvent( | |
| captureHit.hit->component(), | |
| position, | |
| pointerId, | |
| hitType, | |
| button, | |
| canHit, | |
| timeStamp, | |
| this); | |
| if (canHit && group->isPointerHovered(pointerId)) | |
| { | |
| result = HitResult::hit; | |
| if (hitType == ListenerType::down && | |
| group->listener()->claimsPress(button)) | |
| { | |
| claimPointer(captureHit.target, pointerId, button); | |
| } | |
| } | |
| } | |
| for (auto& ended : dragEnded) | |
| { | |
| dragEnd(position, timeStamp, ended.first, ended.second); | |
| } | |
| if (hitType == ListenerType::exit) | |
| { | |
| for (const auto& group : extras.captureGroups) | |
| { | |
| group->releaseEvent(pointerId); | |
| } | |
| } | |
| return result; | |
| } | |
| void StateMachineInstance::claimPointer( | |
| Component* scope, | |
| int pointerId, | |
| PointerButton button) | |
| { | |
| auto& extras = ensureInputExtras(); | |
| const PointerClaim* otherButton = nullptr; | |
| for (const auto& claim : extras.pointerClaims) | |
| { | |
| if (claim.pointerId != pointerId || claim.scope != scope) | |
| { | |
| continue; | |
| } | |
| // Peers on one target, or one listener over several of its shapes. | |
| if (claim.button == button) | |
| { | |
| return; | |
| } | |
| otherButton = &claim; | |
| } | |
| PointerClaim claim; | |
| claim.pointerId = pointerId; | |
| claim.button = button; | |
| claim.scope = scope; | |
| if (otherButton != nullptr) | |
| { | |
| // The same scope, already off: it holds the same things, so that | |
| // whichever button lets go first leaves them off for the other. | |
| claim.hits = otherButton->hits; | |
| claim.captureGroups = otherButton->captureGroups; | |
| extras.pointerClaims.push_back(std::move(claim)); | |
| return; | |
| } | |
| for (const auto& hitComponent : m_hitComponents) | |
| { | |
| if (componentWithin(hierarchyComponent(hitComponent.get()), scope)) | |
| { | |
| hitComponent->disablePointerEvents(pointerId); | |
| claim.hits.push_back(hitComponent.get()); | |
| } | |
| } | |
| // Capture listeners deeper inside it too: the outermost claim wins, as | |
| // it does for keys. Peers on the same target keep running. One already | |
| // off still counts: an inner claim for another button may hold it, and | |
| // when that one lets go, this one keeps it off. | |
| for (const auto& captureHit : extras.captureHits) | |
| { | |
| if (captureHit.target != scope && | |
| componentWithin(captureHit.target, scope) && | |
| std::find( | |
| claim.captureGroups.begin(), | |
| claim.captureGroups.end(), | |
| captureHit.group) == claim.captureGroups.end()) | |
| { | |
| captureHit.group->disable(pointerId); | |
| claim.captureGroups.push_back(captureHit.group); | |
| } | |
| } | |
| extras.pointerClaims.push_back(std::move(claim)); | |
| } | |
| bool StateMachineInstance::releasePointerClaims( | |
| int pointerId, | |
| PointerButton button, | |
| bool anyButton) | |
| { | |
| auto* extras = inputExtras(); | |
| if (extras == nullptr || extras->pointerClaims.empty()) | |
| { | |
| return false; | |
| } | |
| bool released = false; | |
| auto& claims = extras->pointerClaims; | |
| for (size_t i = 0; i < claims.size();) | |
| { | |
| if (claims[i].pointerId != pointerId || | |
| (!anyButton && claims[i].button != button)) | |
| { | |
| i++; | |
| continue; | |
| } | |
| PointerClaim claim = std::move(claims[i]); | |
| claims.erase(claims.begin() + i); | |
| for (auto hitComponent : claim.hits) | |
| { | |
| hitComponent->enablePointerEvents(pointerId); | |
| } | |
| for (auto group : claim.captureGroups) | |
| { | |
| group->enable(pointerId); | |
| } | |
| released = true; | |
| } | |
| if (released) | |
| { | |
| // Another claim of the pointer's may hold some of the same things. | |
| applyPointerClaims(pointerId); | |
| } | |
| return released; | |
| } | |
| void StateMachineInstance::applyPointerClaims(int pointerId) | |
| { | |
| auto* extras = inputExtras(); | |
| if (isPointerDisabled(pointerId)) | |
| { | |
| // Turned off from outside (a host's claim or drag), which still holds: | |
| // everything goes back off, not just what the claims here cover. | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| hitShape->disablePointerEvents(pointerId); | |
| } | |
| if (extras != nullptr) | |
| { | |
| for (const auto& group : extras->captureGroups) | |
| { | |
| group->disable(pointerId); | |
| } | |
| } | |
| return; | |
| } | |
| if (extras == nullptr) | |
| { | |
| return; | |
| } | |
| for (const auto& claim : extras->pointerClaims) | |
| { | |
| if (claim.pointerId != pointerId) | |
| { | |
| continue; | |
| } | |
| for (auto hitComponent : claim.hits) | |
| { | |
| hitComponent->disablePointerEvents(pointerId); | |
| } | |
| for (auto group : claim.captureGroups) | |
| { | |
| group->disable(pointerId); | |
| } | |
| } | |
| } | |
| uint32_t StateMachineInstance::liveCaptureMachines() | |
| { | |
| return s_liveCaptureMachines.load(std::memory_order_relaxed); | |
| } | |
| void StateMachineInstance::captureTopologyChanged() | |
| { | |
| // Never 0, which marks a cache that was never filled. | |
| if (s_captureEpoch.fetch_add(1, std::memory_order_relaxed) + 1 == 0) | |
| { | |
| s_captureEpoch.fetch_add(1, std::memory_order_relaxed); | |
| } | |
| } | |
| bool StateMachineInstance::treeHasCaptureListeners() | |
| { | |
| const uint32_t epoch = s_captureEpoch.load(std::memory_order_relaxed); | |
| auto& extras = ensureInputExtras(); | |
| if (extras.captureTreeEpoch != epoch) | |
| { | |
| extras.captureTreeHas = hasCaptureListeners(); | |
| extras.captureTreeEpoch = epoch; | |
| } | |
| return extras.captureTreeHas; | |
| } | |
| bool StateMachineInstance::hasCaptureListeners() | |
| { | |
| auto* extras = inputExtras(); | |
| if (extras != nullptr && !extras->captureHits.empty()) | |
| { | |
| return true; | |
| } | |
| for (auto nestedArtboard : m_artboardInstance->nestedArtboards()) | |
| { | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (!nestedAnimation->is<NestedStateMachine>()) | |
| { | |
| continue; | |
| } | |
| auto smi = nestedAnimation->as<NestedStateMachine>() | |
| ->stateMachineInstance(); | |
| if (smi != nullptr && smi->hasCaptureListeners()) | |
| { | |
| return true; | |
| } | |
| } | |
| } | |
| for (auto componentList : m_artboardInstance->artboardComponentLists()) | |
| { | |
| for (int i : componentList->orderedListIndices()) | |
| { | |
| auto smi = componentList->stateMachineInstance(i); | |
| if (smi != nullptr && smi->hasCaptureListeners()) | |
| { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| bool StateMachineInstance::pointerOccludedAt(Vec2D position) | |
| { | |
| if (!mapToContentSpace(position)) | |
| { | |
| // Collapsed contents hit nothing, so they hide nothing either. | |
| return false; | |
| } | |
| for (const auto& hitComponent : m_hitComponents) | |
| { | |
| if (hitComponent->blocksCapture(position)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| bool StateMachineInstance::hitTest(Vec2D position) const | |
| { | |
| return hitTestInternal(position, false); | |
| } | |
| bool StateMachineInstance::hitTestBounded(Vec2D position) const | |
| { | |
| return hitTestInternal(position, true); | |
| } | |
| bool StateMachineInstance::hitTestInternal(Vec2D position, bool bounded) const | |
| { | |
| if (m_artboardInstance->frameOrigin() && !m_hostSpacePointers) | |
| { | |
| position -= Vec2D( | |
| m_artboardInstance->originX() * m_artboardInstance->layoutWidth(), | |
| m_artboardInstance->originY() * m_artboardInstance->layoutHeight()); | |
| } | |
| // Invert the artboard's own rotation/scale (applied in drawInternal after | |
| // the frame-origin translation) so the pointer maps into content space. | |
| // Covers nested state machines too, which funnel through here. | |
| if (m_artboardInstance->hasSelfTransform() && !m_hostSpacePointers) | |
| { | |
| Mat2D inverse; | |
| if (!m_artboardInstance->selfTransform().invert(&inverse)) | |
| { | |
| // A degenerate (0 scale) self transform collapses the contents to | |
| // nothing, so there's nothing to hit. | |
| return false; | |
| } | |
| position = inverse * position; | |
| } | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| // TODO: quick reject. | |
| if (bounded ? hitShape->hitTestBounded(position) | |
| : hitShape->hitTest(position)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| void StateMachineInstance::serveTextStyle( | |
| Text* text, | |
| const TextStyleReference* style) | |
| { | |
| #ifdef WITH_RIVE_TEXT | |
| if (text == nullptr || style == nullptr) | |
| { | |
| return; | |
| } | |
| // Pointer listeners with no target listen to the runs drawn with | |
| // [style]. Their groups exist already, inert, so they just get a hit | |
| // area now. | |
| std::unique_ptr<HitTextStyleTarget> hitRuns; | |
| for (const auto& listenerGroup : m_listenerGroups) | |
| { | |
| auto listener = listenerGroup->listener(); | |
| if (listener == nullptr || !listener->hasPointerListeners() || | |
| m_artboardInstance->resolve(listener->targetId()) != nullptr) | |
| { | |
| continue; | |
| } | |
| if (hitRuns == nullptr) | |
| { | |
| hitRuns = std::make_unique<HitTextStyleTarget>(text, style, this); | |
| } | |
| hitRuns->addListener(listenerGroup.get()); | |
| } | |
| if (hitRuns != nullptr) | |
| { | |
| m_hitComponents.push_back(std::move(hitRuns)); | |
| } | |
| m_hostSpacePointers = true; | |
| #endif | |
| } | |
| HitResult StateMachineInstance::pointerMove( | |
| Vec2D position, | |
| float timeStamp, | |
| int id) | |
| { | |
| return updateListeners(position, ListenerType::move, id, timeStamp); | |
| } | |
| HitResult StateMachineInstance::pointerDown( | |
| Vec2D position, | |
| int id, | |
| PointerButton button) | |
| { | |
| return updateListeners(position, ListenerType::down, id, 0, button); | |
| } | |
| HitResult StateMachineInstance::pointerUp( | |
| Vec2D position, | |
| int id, | |
| PointerButton button) | |
| { | |
| return updateListeners(position, ListenerType::up, id, 0, button); | |
| } | |
| HitResult StateMachineInstance::pointerExit(Vec2D position, int id) | |
| { | |
| return updateListeners(position, ListenerType::exit, id); | |
| } | |
| HitResult StateMachineInstance::pointerMiss( | |
| Vec2D position, | |
| ListenerType hitType, | |
| float timeStamp, | |
| int id, | |
| PointerButton button) | |
| { | |
| // Every target counts as occluded: ListenerGroup::processEvent then | |
| // unhovers it and fails presses, but keeps a drag's press alive. Scoped | |
| // rather than passed, so the dragStart/dragEnd passes a group sets off | |
| // (and dragEnd's pointerMove) miss as well. | |
| const bool wasInPointerMiss = m_inPointerMiss; | |
| m_inPointerMiss = true; | |
| HitResult result; | |
| switch (hitType) | |
| { | |
| // A host's drag still turns this machine's pointers off and back on. | |
| case ListenerType::dragStart: | |
| result = dragStart(position, timeStamp, true, id, button); | |
| break; | |
| case ListenerType::dragEnd: | |
| result = dragEnd(position, timeStamp, id, button); | |
| break; | |
| default: | |
| result = updateListeners(position, hitType, id, timeStamp, button); | |
| break; | |
| } | |
| m_inPointerMiss = wasInPointerMiss; | |
| return result; | |
| } | |
| HitResult StateMachineInstance::dragStart( | |
| Vec2D position, | |
| float timeStamp, | |
| bool disablePointer, | |
| int pointerId, | |
| PointerButton button) | |
| { | |
| if (disablePointer) | |
| { | |
| disablePointerEvents(pointerId); | |
| } | |
| auto hit = updateListeners( | |
| position, | |
| ListenerType::dragStart, | |
| pointerId, | |
| 0, | |
| button); | |
| return hit; | |
| } | |
| HitResult StateMachineInstance::dragEnd( | |
| Vec2D position, | |
| float timeStamp, | |
| int pointerId, | |
| PointerButton button) | |
| { | |
| enablePointerEvents(pointerId); | |
| auto hit = updateListeners( | |
| position, | |
| ListenerType::dragEnd, | |
| pointerId, | |
| 0, | |
| button); | |
| pointerMove(position, timeStamp, pointerId); | |
| return hit; | |
| } | |
| bool StateMachineInstance::mapToContentSpace(Vec2D& position, Vec2D* delta) | |
| const | |
| { | |
| if (m_hostSpacePointers) | |
| { | |
| return true; | |
| } | |
| if (m_artboardInstance->frameOrigin()) | |
| { | |
| position -= Vec2D( | |
| m_artboardInstance->originX() * m_artboardInstance->layoutWidth(), | |
| m_artboardInstance->originY() * m_artboardInstance->layoutHeight()); | |
| } | |
| if (m_artboardInstance->hasSelfTransform()) | |
| { | |
| Mat2D inverse; | |
| if (!m_artboardInstance->selfTransform().invert(&inverse)) | |
| { | |
| return false; | |
| } | |
| position = inverse * position; | |
| if (delta != nullptr) | |
| { | |
| // A delta is a vector: linear part only, no translation. | |
| *delta = inverse * *delta - inverse * Vec2D(); | |
| } | |
| } | |
| return true; | |
| } | |
| HitResult StateMachineInstance::pointerScroll( | |
| Vec2D position, | |
| const ScrollEvent& event, | |
| float timeStamp, | |
| int pointerId) | |
| { | |
| ScrollEvent mapped = event; | |
| if (!mapToContentSpace(position, &mapped.delta)) | |
| { | |
| // Nothing can map through a singular transform, so no later event | |
| // could close this gesture. | |
| cancelScroll(); | |
| return HitResult::none; | |
| } | |
| if (m_scrollLatch != nullptr && !m_scrollLatch->scrollGestureActive()) | |
| { | |
| m_scrollLatch = nullptr; | |
| } | |
| HitComponent* target = m_scrollLatch; | |
| HitResult result = HitResult::none; | |
| if (target != nullptr) | |
| { | |
| result = target->processScroll(position, mapped, timeStamp, pointerId); | |
| } | |
| else | |
| { | |
| // Sorted top-most first, so the innermost view that can move wins. | |
| // One that declines, like a script that never calls hit, passes the | |
| // event on. An opaque hit that cannot scroll ends the walk, as it | |
| // would for a pointer: nothing behind it may take the event. | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| if (hitShape->wantsScroll(position, mapped)) | |
| { | |
| result = hitShape | |
| ->processScroll(position, mapped, timeStamp, pointerId); | |
| if (result != HitResult::none) | |
| { | |
| target = hitShape.get(); | |
| break; | |
| } | |
| } | |
| if (hitShape->occludesScroll(position)) | |
| { | |
| break; | |
| } | |
| } | |
| } | |
| m_scrollLatch = target != nullptr && target->scrollGestureActive() ? target | |
| : nullptr; | |
| return result; | |
| } | |
| bool StateMachineInstance::wantsScroll(Vec2D position, const ScrollEvent& event) | |
| { | |
| if (hasScrollLatch()) | |
| { | |
| return true; | |
| } | |
| ScrollEvent mapped = event; | |
| if (!mapToContentSpace(position, &mapped.delta)) | |
| { | |
| return false; | |
| } | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| if (hitShape->wantsScroll(position, mapped)) | |
| { | |
| return true; | |
| } | |
| if (hitShape->occludesScroll(position)) | |
| { | |
| return false; | |
| } | |
| } | |
| return false; | |
| } | |
| bool StateMachineInstance::hasScrollTargetAt(Vec2D position) | |
| { | |
| if (hasScrollLatch()) | |
| { | |
| return true; | |
| } | |
| if (!mapToContentSpace(position)) | |
| { | |
| return false; | |
| } | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| if (hitShape->hasScrollTarget(position)) | |
| { | |
| return true; | |
| } | |
| if (hitShape->occludesScroll(position)) | |
| { | |
| return false; | |
| } | |
| } | |
| return false; | |
| } | |
| bool StateMachineInstance::listensToButtonAt( | |
| Vec2D position, | |
| PointerButton button) | |
| { | |
| if (!mapToContentSpace(position)) | |
| { | |
| return false; | |
| } | |
| // Capture listeners hear the press before anything else does. | |
| if (auto* extras = inputExtras()) | |
| { | |
| for (const auto& captureHit : extras->captureHits) | |
| { | |
| if (captureHit.group->listener()->listensToButton(button) && | |
| captureHit.hit->hitTest(position)) | |
| { | |
| return true; | |
| } | |
| } | |
| } | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| if (hitShape->listensToButtonAt(position, button)) | |
| { | |
| return true; | |
| } | |
| if (hitShape->occludesPointer(position)) | |
| { | |
| return false; | |
| } | |
| } | |
| return false; | |
| } | |
| bool StateMachineInstance::scrollOccludedAt(Vec2D position) | |
| { | |
| if (!mapToContentSpace(position)) | |
| { | |
| // Collapsed contents hit nothing, so they hide nothing either. | |
| return false; | |
| } | |
| // Only asked once nothing here wanted the event, so the first opaque hit | |
| // decides. | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| if (hitShape->occludesScroll(position)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| void StateMachineInstance::cancelScroll() | |
| { | |
| if (m_scrollLatch != nullptr) | |
| { | |
| m_scrollLatch->cancelScroll(); | |
| m_scrollLatch = nullptr; | |
| } | |
| } | |
| bool StateMachineInstance::hasScrollLatch() | |
| { | |
| if (m_scrollLatch != nullptr && !m_scrollLatch->scrollGestureActive()) | |
| { | |
| m_scrollLatch = nullptr; | |
| } | |
| return m_scrollLatch != nullptr; | |
| } | |
| #ifdef TESTING | |
| uint64_t StateMachineInstance::sm_settledLayerSkips = 0; | |
| uint64_t StateMachineInstance::sm_capturePassVisits = 0; | |
| uint64_t StateMachineInstance::sm_frozenLayerAdvances = 0; | |
| uint64_t StateMachineInstance::sm_frozenPoseAdvances = 0; | |
| const LayerState* StateMachineInstance::layerState(size_t index) | |
| { | |
| if (index < m_layerCount) | |
| { | |
| return m_layers[index].currentState(); | |
| } | |
| return nullptr; | |
| } | |
| bool StateMachineInstance::isLayerFrozen(size_t index) const | |
| { | |
| return index < m_layerCount && | |
| m_layers[index].isFrozen(this, m_advanceChangeSerial); | |
| } | |
| bool StateMachineInstance::hasLayerChangeWindows() const | |
| { | |
| auto* bindables = this->bindables(); | |
| return bindables != nullptr && bindables->layerChanges != nullptr; | |
| } | |
| #endif | |
| void StateMachineInstance::addToHitLookup( | |
| Component* target, | |
| bool isLayoutComponent, | |
| std::unordered_map<Component*, HitDrawable*>& hitLookup, | |
| ListenerGroup* listenerGroup, | |
| bool isOpaque, | |
| std::vector<HitDrawable*>* captureHits) | |
| { | |
| // A capture listener's hit drawable is shared with the normal listeners | |
| // on the same target, but doesn't take the listener: the capture phase | |
| // drives it from the outside. | |
| auto addListener = [&](HitDrawable* hitDrawable) { | |
| if (captureHits != nullptr) | |
| { | |
| captureHits->push_back(hitDrawable); | |
| } | |
| else | |
| { | |
| hitDrawable->addListener(listenerGroup); | |
| } | |
| }; | |
| // target could either be a LayoutComponent or a DrawableProxy | |
| if (isLayoutComponent) | |
| { | |
| HitLayout* hitLayout; | |
| auto itr = hitLookup.find(target); | |
| if (itr == hitLookup.end()) | |
| { | |
| auto hs = std::make_unique<HitLayout>( | |
| target->as<Drawable>(), | |
| this, | |
| isOpaque); | |
| hitLookup[target] = hitLayout = hs.get(); | |
| m_hitComponents.push_back(std::move(hs)); | |
| } | |
| else | |
| { | |
| hitLayout = static_cast<HitLayout*>(itr->second); | |
| } | |
| addListener(hitLayout); | |
| if (isOpaque) | |
| { | |
| hitLayout->isOpaque = true; | |
| } | |
| return; | |
| } | |
| if (target->is<Shape>()) | |
| { | |
| HitExpandable* hitShape; | |
| auto itr = hitLookup.find(target); | |
| if (itr == hitLookup.end()) | |
| { | |
| Shape* shape = target->as<Shape>(); | |
| shape->addFlags(PathFlags::neverDeferUpdate); | |
| shape->addDirt(ComponentDirt::Path, true); | |
| auto hs = std::make_unique<HitExpandable>(shape, shape, this); | |
| hitLookup[target] = hitShape = hs.get(); | |
| m_hitComponents.push_back(std::move(hs)); | |
| } | |
| else | |
| { | |
| hitShape = static_cast<HitExpandable*>(itr->second); | |
| } | |
| addListener(hitShape); | |
| return; | |
| } | |
| if (target->is<TextValueRun>()) | |
| { | |
| HitTextRun* hitTextRun; | |
| auto itr = hitLookup.find(target); | |
| if (itr == hitLookup.end()) | |
| { | |
| TextValueRun* run = target->as<TextValueRun>(); | |
| run->textComponent()->addDirt(ComponentDirt::Path, true); | |
| auto hs = std::make_unique<HitTextRun>(run->textComponent(), run, this); | |
| hitLookup[target] = hitTextRun = hs.get(); | |
| m_hitComponents.push_back(std::move(hs)); | |
| } | |
| else | |
| { | |
| hitTextRun = static_cast<HitTextRun*>(itr->second); | |
| } | |
| addListener(hitTextRun); | |
| return; | |
| } | |
| // Older files ignore images: they fall through to the container walk, which | |
| // finds nothing to hit under one. | |
| if (target->is<Image>() && target->as<Image>()->isPointerTarget()) | |
| { | |
| HitExpandable* hitImage; | |
| auto itr = hitLookup.find(target); | |
| if (itr == hitLookup.end()) | |
| { | |
| Image* image = target->as<Image>(); | |
| auto hs = std::make_unique<HitExpandable>(image, image, this); | |
| hitLookup[target] = hitImage = hs.get(); | |
| m_hitComponents.push_back(std::move(hs)); | |
| } | |
| else | |
| { | |
| hitImage = static_cast<HitExpandable*>(itr->second); | |
| } | |
| addListener(hitImage); | |
| return; | |
| } | |
| if (target->is<ContainerComponent>()) | |
| { | |
| target->as<ContainerComponent>()->forEachChild([&](Component* child) { | |
| addToHitLookup( | |
| child, | |
| child->is<LayoutComponent>(), | |
| hitLookup, | |
| listenerGroup, | |
| isOpaque, | |
| captureHits); | |
| return false; | |
| }); | |
| return; | |
| } | |
| } | |
| StateMachineInstance::StateMachineInstance( | |
| const StateMachine* machine, | |
| ArtboardInstance* instance) | |
| : Scene(instance) | |
| , m_machine(machine) | |
| , m_needsAdvance(false) | |
| , m_bindsPending(false) | |
| , m_anySettled(false) | |
| , m_destroying(false) | |
| , m_hostSpacePointers(false) | |
| , m_hostDispatching(false) | |
| , m_captureRunByHost(false) | |
| , m_inPointerMiss(false) | |
| { | |
| const auto count = machine->inputCount(); | |
| m_inputInstances.resize(count); | |
| for (size_t i = 0; i < count; i++) | |
| { | |
| auto input = machine->input(i); | |
| if (input == nullptr) | |
| { | |
| continue; | |
| } | |
| switch (input->coreType()) | |
| { | |
| case StateMachineBool::typeKey: | |
| m_inputInstances[i] = new SMIBool(input->as<StateMachineBool>(), this); | |
| break; | |
| case StateMachineNumber::typeKey: | |
| m_inputInstances[i] = new SMINumber( | |
| input->as<StateMachineNumber>(), | |
| this); | |
| break; | |
| case StateMachineTrigger::typeKey: | |
| m_inputInstances[i] = new SMITrigger( | |
| input->as<StateMachineTrigger>(), | |
| this); | |
| break; | |
| default: | |
| // Sanity check. | |
| break; | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| auto instance = m_inputInstances[i]; | |
| if (instance != nullptr) | |
| { | |
| instance->m_index = i; | |
| } | |
| #endif | |
| } | |
| // Seeded once per state machine instance. This used to run inside the | |
| // per-layer init(), reseeding the global RNG (and, outside deterministic | |
| // mode, reading the clock) once for every layer of every instance. | |
| if (File::deterministicMode) | |
| { | |
| srand((unsigned int)1); | |
| } | |
| else | |
| { | |
| auto now = std::chrono::high_resolution_clock::now(); | |
| auto nanos = std::chrono::duration_cast<std::chrono::nanoseconds>( | |
| now.time_since_epoch()) | |
| .count(); | |
| srand((unsigned int)nanos); | |
| } | |
| // Import refuses a machine with more (StateMachineImporter::resolve). | |
| assert(machine->layerCount() <= UINT16_MAX); | |
| m_layerCount = static_cast<uint16_t>( | |
| std::min<size_t>(machine->layerCount(), UINT16_MAX)); | |
| m_layers = new StateMachineLayerInstance[m_layerCount]; | |
| for (size_t i = 0; i < m_layerCount; i++) | |
| { | |
| m_layers[i].init(this, machine->layer(i)); | |
| } | |
| // A condition comparing a component's trigger reads whether it fired | |
| // within the frame (changePending), which this instance's change tracking | |
| // answers. It has to start now, before anything fires. | |
| if (machine->readsComponentTriggers()) | |
| { | |
| ensureBindables(); | |
| } | |
| // Initialize dataBinds. All databinds are cloned for the state machine | |
| // instance. That enables binding each instance to its own context without | |
| // polluting the rest. | |
| auto dataBindCount = machine->dataBindCount(); | |
| for (size_t i = 0; i < dataBindCount; i++) | |
| { | |
| auto dataBind = machine->dataBind(i); | |
| if (!dataBind->target()) | |
| { | |
| continue; | |
| } | |
| auto dataBindClone = static_cast<DataBind*>(dataBind->clone()); | |
| dataBindClone->file(dataBind->file()); | |
| if (dataBind->converter() != nullptr) | |
| { | |
| dataBindClone->converter( | |
| dataBind->converter()->clone()->as<DataConverter>()); | |
| } | |
| if (dataBind->target()->is<BindableProperty>()) | |
| { | |
| auto& bindables = ensureBindables(); | |
| auto bindableProperty = dataBind->target()->as<BindableProperty>(); | |
| BindableProperty* bindablePropertyClone = bindables.propertyInstances | |
| .find(bindableProperty); | |
| if (bindablePropertyClone == nullptr) | |
| { | |
| bindablePropertyClone = bindableProperty->clone() | |
| ->as<BindableProperty>(); | |
| bindables.propertyInstances.insert( | |
| bindableProperty, | |
| bindablePropertyClone); | |
| } | |
| dataBindClone->target(bindablePropertyClone); | |
| // We are only storing in this unordered map data binds that are | |
| // targetting the source. For now, this is only the case for | |
| // listener actions. | |
| if ((static_cast<DataBindFlags>(dataBindClone->flags()) & | |
| DataBindFlags::ToSource) == DataBindFlags::ToSource) | |
| { | |
| bindables.dataBindsToSource[bindablePropertyClone] = dataBindClone; | |
| } | |
| else | |
| { | |
| bindables.dataBindsToTarget[bindablePropertyClone] = dataBindClone; | |
| } | |
| } | |
| else | |
| { | |
| auto* originalTarget = dataBind->target(); | |
| dataBindClone->target(originalTarget); | |
| if (originalTarget->is<StateTransitionBase>()) | |
| { | |
| // Create a per-instance BindablePropertyNumber to | |
| // receive the data-bound value instead of writing | |
| // to the shared StateTransition. Swap the target | |
| // and propertyKey so the normal apply() path writes | |
| // to our instance-local property. | |
| auto* prop = new BindablePropertyNumber(); | |
| auto& transitionProps = ensureBindables().transitionPropertyInstances; | |
| transitionProps[originalTarget][dataBind->propertyKey()] = prop; | |
| dataBindClone->target(prop); | |
| dataBindClone->propertyKey( | |
| BindablePropertyNumberBase::propertyValuePropertyKey); | |
| } | |
| } | |
| // Added once its target is set: addDataBind decides from the target | |
| // whether a toSource bind hears about changes or has to poll for them, | |
| // and without one it would poll on every pass. | |
| addDataBind(dataBindClone); | |
| } | |
| // Initialize listeners. Store a lookup table of shape id to hit shape | |
| // representation (an object that stores all the listeners triggered by the | |
| // shape producing a listener). | |
| std::unordered_map<Component*, HitDrawable*> hitLookup; | |
| for (std::size_t i = 0; i < machine->listenerCount(); i++) | |
| { | |
| auto listener = machine->listener(i); | |
| if (listener->hasListener(ListenerType::event)) | |
| { | |
| continue; | |
| } | |
| if (listener->hasListener(ListenerType::viewModel)) | |
| { | |
| auto vmListener = new ListenerViewModel(this, listener); | |
| ensureReporting().listenerViewModels.push_back(vmListener); | |
| // Its triggers are checked against this instance's change | |
| // tracking when it binds (viewModelTriggerPendingForListeners), | |
| // which has to start now, before the first bind. | |
| ensureBindables(); | |
| continue; | |
| } | |
| // Handle focus/blur listeners - they're driven by FocusManager, | |
| // not pointer events. | |
| if (listener->hasListener(ListenerType::focus) || | |
| listener->hasListener(ListenerType::blur)) | |
| { | |
| auto target = m_artboardInstance->resolve(listener->targetId()); | |
| if (target != nullptr && target->is<Node>()) | |
| { | |
| auto node = target->as<Node>(); | |
| // Find FocusData child of the node | |
| FocusData* focusData = nullptr; | |
| for (auto child : node->children()) | |
| { | |
| if (child->is<FocusData>()) | |
| { | |
| focusData = child->as<FocusData>(); | |
| break; | |
| } | |
| } | |
| if (focusData != nullptr) | |
| { | |
| auto focusGroup = std::make_unique<FocusListenerGroup>( | |
| focusData, | |
| listener, | |
| this); | |
| ensureInputExtras().focusListenerGroups.push_back( | |
| std::move(focusGroup)); | |
| } | |
| } | |
| } | |
| if (listener->hasListener(ListenerType::keyboard) || | |
| listener->hasListener(ListenerType::textInput)) | |
| { | |
| auto target = m_artboardInstance->resolve(listener->targetId()); | |
| if (target != nullptr && target->is<Node>()) | |
| { | |
| auto node = target->as<Node>(); | |
| // Find FocusData child of the node | |
| FocusData* focusData = nullptr; | |
| for (auto child : node->children()) | |
| { | |
| if (child->is<FocusData>()) | |
| { | |
| focusData = child->as<FocusData>(); | |
| break; | |
| } | |
| } | |
| if (focusData != nullptr) | |
| { | |
| auto keyboardGroup = std::make_unique<KeyboardListenerGroup>( | |
| focusData, | |
| listener, | |
| this); | |
| ensureInputExtras().keyboardListenerGroups.push_back( | |
| std::move(keyboardGroup)); | |
| } | |
| } | |
| } | |
| // Semantic listeners are driven by accessibility actions rather | |
| // than pointer events. The editor enforces that the listener's | |
| // target Node owns a SemanticData child directly; no ancestor | |
| // walk is performed here. | |
| if (listener->hasListener(ListenerType::semanticAction)) | |
| { | |
| auto target = m_artboardInstance->resolve(listener->targetId()); | |
| if (target != nullptr && target->is<Node>()) | |
| { | |
| for (auto* child : target->as<Node>()->children()) | |
| { | |
| if (child->is<SemanticData>()) | |
| { | |
| ensureInputExtras().semanticListenerGroups.push_back( | |
| std::make_unique<SemanticListenerGroup>( | |
| child->as<SemanticData>(), | |
| listener, | |
| this)); | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| if (listener->hasPointerListeners() && listener->isCapture()) | |
| { | |
| auto listenerGroup = std::make_unique<ListenerGroup>(listener); | |
| auto target = m_artboardInstance->resolve(listener->targetId()); | |
| if (target != nullptr && target->is<Component>()) | |
| { | |
| auto scope = target->as<Component>(); | |
| uint32_t depth = 0; | |
| for (auto p = scope->parent(); p != nullptr; p = p->parent()) | |
| { | |
| depth++; | |
| } | |
| bool isLayoutComponent = false; | |
| Component* hitTarget = scope; | |
| if (scope->is<LayoutComponent>()) | |
| { | |
| isLayoutComponent = true; | |
| hitTarget = scope->as<LayoutComponent>()->proxy(); | |
| } | |
| std::vector<HitDrawable*> hits; | |
| addToHitLookup( | |
| hitTarget, | |
| isLayoutComponent, | |
| hitLookup, | |
| listenerGroup.get(), | |
| false, | |
| &hits); | |
| auto& extras = ensureInputExtras(); | |
| for (auto hit : hits) | |
| { | |
| extras.captureHits.push_back( | |
| {hit, listenerGroup.get(), scope, depth, 0}); | |
| } | |
| } | |
| ensureInputExtras().captureGroups.push_back(std::move(listenerGroup)); | |
| } | |
| else if (listener->hasPointerListeners()) | |
| { | |
| auto listenerGroup = std::make_unique<ListenerGroup>(listener); | |
| auto target = m_artboardInstance->resolve(listener->targetId()); | |
| if (target != nullptr && target->is<Component>()) | |
| { | |
| bool isLayoutComponent = false; | |
| if (target->is<LayoutComponent>()) | |
| { | |
| isLayoutComponent = true; | |
| target = target->as<LayoutComponent>()->proxy(); | |
| } | |
| addToHitLookup( | |
| target->as<Component>(), | |
| isLayoutComponent, | |
| hitLookup, | |
| listenerGroup.get(), | |
| false); | |
| } | |
| m_listenerGroups.push_back(std::move(listenerGroup)); | |
| } | |
| if (listener->hasListener(ListenerType::gamepad)) | |
| { | |
| auto target = m_artboardInstance->resolve(listener->targetId()); | |
| if (target != nullptr && target->is<Node>()) | |
| { | |
| auto node = target->as<Node>(); | |
| FocusData* focusData = nullptr; | |
| for (auto child : node->children()) | |
| { | |
| if (child->is<FocusData>()) | |
| { | |
| focusData = child->as<FocusData>(); | |
| break; | |
| } | |
| } | |
| if (focusData != nullptr) | |
| { | |
| auto gamepadGroup = std::make_unique<GamepadListenerGroup>( | |
| focusData, | |
| listener, | |
| this); | |
| ensureInputExtras().gamepadListenerGroups.push_back( | |
| std::move(gamepadGroup)); | |
| } | |
| } | |
| } | |
| } | |
| // Native editor instances keep components in their dependency graph, | |
| // rather than the runtime importer's owning object array. | |
| const auto* listenerObjects = &instance->objects(); | |
| #ifdef WITH_RIVE_EDITOR | |
| std::vector<Core*> nativeListenerObjects; | |
| if (instance->editorResolver()) | |
| { | |
| const auto& components = instance->dependencyOrder(); | |
| nativeListenerObjects.assign(components.begin(), components.end()); | |
| listenerObjects = &nativeListenerObjects; | |
| } | |
| #endif | |
| std::vector<ListenerGroupProvider*> componentProvidedListenerGroups; | |
| for (auto core : *listenerObjects) | |
| { | |
| if (core == nullptr) | |
| { | |
| continue; | |
| } | |
| auto provider = ListenerGroupProvider::from(core); | |
| if (provider != nullptr) | |
| { | |
| componentProvidedListenerGroups.push_back(provider); | |
| } | |
| } | |
| for (auto component : componentProvidedListenerGroups) | |
| { | |
| auto groupsWithTargets = component->listenerGroups(); | |
| for (auto groupWithTargets : groupsWithTargets) | |
| { | |
| auto group = groupWithTargets->group(); | |
| auto targets = groupWithTargets->targets(); | |
| for (auto target : targets) | |
| { | |
| auto component = target->component(); | |
| bool isLayoutComponent = component->is<LayoutComponent>() || | |
| (component->is<Drawable>() && | |
| component->as<Drawable>()->isProxy()); | |
| addToHitLookup( | |
| target->component(), | |
| isLayoutComponent, | |
| hitLookup, | |
| group, | |
| target->isOpaque()); | |
| } | |
| m_listenerGroups.push_back(std::unique_ptr<ListenerGroup>(group)); | |
| for (auto target : targets) | |
| { | |
| delete target; | |
| } | |
| delete groupWithTargets; | |
| } | |
| auto hitComponents = component->hitComponents(this); | |
| for (auto* hitComponent : hitComponents) | |
| { | |
| m_hitComponents.push_back(std::unique_ptr<HitComponent>(hitComponent)); | |
| } | |
| } | |
| for (auto nestedArtboard : instance->nestedArtboards()) | |
| { | |
| // TODO: @hernan as an optimization only create a HitNestedArtboard if | |
| // the nested artboard has state machines or if it is bound via data | |
| // binding | |
| auto hn = std::make_unique<HitNestedArtboard>( | |
| nestedArtboard->as<Component>(), | |
| this); | |
| m_hitComponents.push_back(std::move(hn)); | |
| for (auto animation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (animation->is<NestedStateMachine>()) | |
| { | |
| if (auto notifier = animation->as<NestedStateMachine>() | |
| ->stateMachineInstance()) | |
| { | |
| notifier->setNestedArtboard(nestedArtboard); | |
| notifier->addNestedEventListener(this); | |
| } | |
| } | |
| else if (animation->is<NestedLinearAnimation>()) | |
| { | |
| if (auto notifier = animation->as<NestedLinearAnimation>() | |
| ->animationInstance()) | |
| { | |
| notifier->setNestedArtboard(nestedArtboard); | |
| notifier->addNestedEventListener(this); | |
| } | |
| } | |
| } | |
| } | |
| for (auto componentList : instance->artboardComponentLists()) | |
| { | |
| auto hc = std::make_unique<HitComponentList>( | |
| componentList->as<Component>(), | |
| this); | |
| m_hitComponents.push_back(std::move(hc)); | |
| } | |
| #ifdef WITH_RIVE_TEXT | |
| // Texts whose runs may use a style that listens to them forward pointers | |
| // to those styles' state machines. | |
| for (auto text : instance->objects<Text>()) | |
| { | |
| if (text->mayHaveListeningRuns()) | |
| { | |
| m_hitComponents.push_back(std::make_unique<HitTextStyleRuns>(text, this)); | |
| } | |
| } | |
| // Register TextInputs as hit targets for drag-to-select functionality | |
| for (auto object : *listenerObjects) | |
| { | |
| if (!object || !object->is<TextInput>()) | |
| continue; | |
| auto* textInput = object->as<TextInput>(); | |
| auto textInputGroup = std::make_unique<TextInputListenerGroup>( | |
| textInput, | |
| this); | |
| auto hitExpandable = std::make_unique<HitExpandable>( | |
| textInput->as<Drawable>(), | |
| textInput->as<Component>(), | |
| this, | |
| true); // isOpaque - TextInput blocks hits behind it | |
| hitExpandable->addListener(textInputGroup.get()); | |
| m_hitComponents.push_back(std::move(hitExpandable)); | |
| m_listenerGroups.push_back(std::move(textInputGroup)); | |
| } | |
| #endif | |
| // Initialize local instances of ScriptedObjects, in the state machine's | |
| // authored order so every downstream walk (dataContext, Lua init) is | |
| // deterministic. | |
| auto sharedScriptedObjects = machine->scriptedObjects(); | |
| if (!sharedScriptedObjects.empty()) | |
| { | |
| auto& scripting = ensureScripting(); | |
| scripting.objects.reserve(sharedScriptedObjects.size()); | |
| for (auto& scriptedOb : sharedScriptedObjects) | |
| { | |
| scripting.objects.emplace_back( | |
| scriptedOb, | |
| scriptedOb->cloneScriptedObject(this)); | |
| } | |
| for (auto& scriptedPair : scripting.objects) | |
| { | |
| scriptedPair.second->dataContext(m_artboardInstance->dataContext()); | |
| } | |
| initScriptedObjects(); | |
| } | |
| // Register Scripted objects as keyboard and text targets when expected, | |
| // and collect every scripted drawable that wants gamepad events so we can | |
| // broadcast to it later regardless of focus. | |
| for (auto object : *listenerObjects) | |
| { | |
| if (!object || !object->is<ContainerComponent>()) | |
| continue; | |
| auto scriptedObject = ScriptedObject::from(object); | |
| if (!scriptedObject) | |
| { | |
| continue; | |
| } | |
| if (scriptedObject->wantsKeyboardInput() || | |
| scriptedObject->wantsTextInput()) | |
| { | |
| for (auto& child : object->as<ContainerComponent>()->children()) | |
| { | |
| if (child->is<FocusData>()) | |
| { | |
| auto keyboardGroup = std::make_unique<KeyboardListenerGroup>( | |
| child->as<FocusData>(), | |
| nullptr, | |
| this); | |
| ensureInputExtras().keyboardListenerGroups.push_back( | |
| std::move(keyboardGroup)); | |
| break; | |
| } | |
| } | |
| } | |
| if ((scriptedObject->wantsGamePadConnect() || | |
| scriptedObject->wantsGamePadDisconnect() || | |
| scriptedObject->wantsGamePadEvent()) && | |
| object->is<ScriptedDrawable>()) | |
| { | |
| ensureInputExtras().gamepadScriptedDrawables.push_back( | |
| object->as<ScriptedDrawable>()); | |
| } | |
| } | |
| sortHitComponents(); | |
| if (inputExtras() != nullptr && !inputExtras()->captureHits.empty()) | |
| { | |
| s_liveCaptureMachines.fetch_add(1, std::memory_order_relaxed); | |
| captureTopologyChanged(); | |
| } | |
| } | |
| FocusManager* StateMachineInstance::focusManager() | |
| { | |
| return m_artboardInstance != nullptr ? m_artboardInstance->focusManager() | |
| : nullptr; | |
| } | |
| const FocusManager* StateMachineInstance::focusManager() const | |
| { | |
| return m_artboardInstance != nullptr ? m_artboardInstance->focusManager() | |
| : nullptr; | |
| } | |
| SMIReporting& StateMachineInstance::ensureReporting() | |
| { | |
| return *m_reporting.ensureAllocated(); | |
| } | |
| SMIBindables& StateMachineInstance::ensureBindables() | |
| { | |
| if (m_bindables.get() == nullptr) | |
| { | |
| m_bindables.ensureAllocated(); | |
| // Changes from before this instance existed aren't pending for it, | |
| // beyond the frame it was made in (see forgetViewModelChanges). | |
| forgetViewModelChanges(); | |
| } | |
| return *m_bindables.get(); | |
| } | |
| SMIInputExtras& StateMachineInstance::ensureInputExtras() | |
| { | |
| return *m_inputExtras.ensureAllocated(); | |
| } | |
| SMIScripting& StateMachineInstance::ensureScripting() | |
| { | |
| return *m_scripting.ensureAllocated(); | |
| } | |
| SemanticManager* StateMachineInstance::semanticManager() const | |
| { | |
| auto* extras = inputExtras(); | |
| if (extras == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| return extras->externalSemanticManager ? extras->externalSemanticManager | |
| : extras->semanticManager.get(); | |
| } | |
| ScriptedObject* StateMachineInstance::scriptedObject( | |
| const ScriptedObject* source) const | |
| { | |
| auto* scripting = this->scripting(); | |
| return scripting != nullptr ? scripting->find(source) : nullptr; | |
| } | |
| StateMachineInstance::~StateMachineInstance() | |
| { | |
| m_destroying = true; | |
| if (inputExtras() != nullptr && !inputExtras()->captureHits.empty()) | |
| { | |
| s_liveCaptureMachines.fetch_sub(1, std::memory_order_relaxed); | |
| captureTopologyChanged(); | |
| } | |
| // Clean up semantic tree BEFORE the internal SemanticManager is destroyed. | |
| // Only needed when we own the manager; if external, the parent cleans up. | |
| if (auto* extras = inputExtras()) | |
| { | |
| if (extras->externalSemanticManager == nullptr && | |
| extras->semanticManager != nullptr && m_artboardInstance != nullptr) | |
| { | |
| m_artboardInstance->cleanupSemanticTree(); | |
| } | |
| extras->embedderGamepads.clear(); | |
| } | |
| unbind(); | |
| for (auto inst : m_inputInstances) | |
| { | |
| delete inst; | |
| } | |
| for (auto& listenerGroup : m_listenerGroups) | |
| { | |
| listenerGroup.reset(); | |
| } | |
| deleteDataBinds(); | |
| delete[] m_layers; | |
| // What's torn down below can still dirty a bind, which unsettles layers. | |
| m_layers = nullptr; | |
| m_anySettled = false; | |
| // The bindable clones and per-transition property instances are raw-owning, | |
| // so they are deleted here rather than by the cluster's destructor. | |
| if (auto* bindables = m_bindables.get()) | |
| { | |
| bindables->propertyInstances.forEachInstance( | |
| [](BindableProperty* instance) { delete instance; }); | |
| for (auto& outer : bindables->transitionPropertyInstances) | |
| { | |
| for (auto& inner : outer.second) | |
| { | |
| delete inner.second; | |
| } | |
| } | |
| bindables->transitionPropertyInstances.clear(); | |
| bindables->propertyInstances.clear(); | |
| } | |
| if (auto* reporting = this->reporting()) | |
| { | |
| for (auto& listenerViewModel : reporting->listenerViewModels) | |
| { | |
| delete listenerViewModel; | |
| } | |
| reporting->listenerViewModels.clear(); | |
| } | |
| if (auto* scripting = m_scripting.get()) | |
| { | |
| for (auto& pair : scripting->objects) | |
| { | |
| delete pair.second; | |
| } | |
| scripting->objects.clear(); | |
| } | |
| } | |
| // Stops the nested artboards' animations from reporting events to this | |
| // machine. ~NestedEventListener does the same, so this only matters to a host | |
| // that wants the machine detached before it is deleted. | |
| void StateMachineInstance::dispose() { removeEventListeners(); } | |
| void StateMachineInstance::removeEventListeners() | |
| { | |
| removeFromNestedEventNotifiers(); | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| void StateMachineInstance::onDataBindChanged(DataBindChanged callback) | |
| { | |
| // dataBinds() is the DataBindContainer base's list — the one addDataBind() | |
| // actually fills. A same-named member used to shadow it here, and it was | |
| // never written, so this callback silently never got installed. | |
| for (auto databind : dataBinds()) | |
| { | |
| databind->onChanged(callback); | |
| } | |
| } | |
| #endif | |
| void StateMachineInstance::sortHitComponents() | |
| { | |
| auto hitShapesCount = m_hitComponents.size(); | |
| auto currentSortedIndex = 0; | |
| auto count = 0; | |
| // Since the Artboard is not a drawable, we move all hit components | |
| // pointing to the artboard to the front of the list | |
| for (auto& comp : m_hitComponents) | |
| { | |
| if (comp->component() != nullptr && comp->component()->is<Artboard>()) | |
| { | |
| if (currentSortedIndex != count) | |
| { | |
| std::iter_swap( | |
| m_hitComponents.begin() + currentSortedIndex, | |
| m_hitComponents.begin() + count); | |
| } | |
| currentSortedIndex++; | |
| } | |
| count++; | |
| } | |
| Drawable* last = m_artboardInstance->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) | |
| { | |
| for (size_t i = currentSortedIndex; i < hitShapesCount; i++) | |
| { | |
| if (m_hitComponents[i]->component() == drawable) | |
| { | |
| if (currentSortedIndex != i) | |
| { | |
| std::iter_swap( | |
| m_hitComponents.begin() + currentSortedIndex, | |
| m_hitComponents.begin() + i); | |
| } | |
| currentSortedIndex++; | |
| } | |
| } | |
| if (currentSortedIndex == hitShapesCount) | |
| { | |
| break; | |
| } | |
| } | |
| auto* extras = inputExtras(); | |
| if (extras == nullptr || extras->captureHits.empty()) | |
| { | |
| return; | |
| } | |
| for (auto& captureHit : extras->captureHits) | |
| { | |
| for (size_t i = 0; i < m_hitComponents.size(); i++) | |
| { | |
| if (m_hitComponents[i].get() == captureHit.hit) | |
| { | |
| captureHit.drawIndex = i; | |
| break; | |
| } | |
| } | |
| } | |
| // Ancestors before descendants, then topmost first. | |
| std::stable_sort( | |
| extras->captureHits.begin(), | |
| extras->captureHits.end(), | |
| [](const CaptureHit& a, const CaptureHit& b) { | |
| if (a.depth != b.depth) | |
| { | |
| return a.depth < b.depth; | |
| } | |
| return a.drawIndex < b.drawIndex; | |
| }); | |
| } | |
| bool StateMachineInstance::tryChangeState() | |
| { | |
| updateDataBinds(false); | |
| bool hasChangedState = false; | |
| for (size_t i = 0; i < m_layerCount; i++) | |
| { | |
| if (m_layers[i].updateState(this)) | |
| { | |
| hasChangedState = true; | |
| } | |
| } | |
| return hasChangedState; | |
| } | |
| void StateMachineInstance::applyEvents() | |
| { | |
| auto* reporting = this->reporting(); | |
| if (reporting == nullptr) | |
| { | |
| // Nothing has ever reported on this instance, so there is provably | |
| // nothing to apply and nothing stale to clear. | |
| return; | |
| } | |
| reporting->eventsAppliedDuringLoop.clear(); | |
| int maxIterations = 100; | |
| int currentIteration = 0; | |
| while ((reporting->reportedEvents.size() > 0 || | |
| reporting->reportedListenerViewModels.size() > 0) && | |
| currentIteration++ < maxIterations) | |
| { | |
| updateDataBinds(false); | |
| // The reported/reporting split is load-bearing: notifying below can | |
| // re-enter reportEvent(), and those events must queue for the next | |
| // pass rather than mutate the batch being delivered. | |
| reporting->reportingEvents = reporting->reportedEvents; | |
| reporting->reportingListenerViewModels = reporting | |
| ->reportedListenerViewModels; | |
| reporting->reportedEvents.clear(); | |
| reporting->reportedListenerViewModels.clear(); | |
| if (currentIteration > 1) | |
| { | |
| // These were reported during the loop, so no host has seen them | |
| // yet; keep them visible until the next applyEvents. | |
| reporting->eventsAppliedDuringLoop.insert( | |
| reporting->eventsAppliedDuringLoop.end(), | |
| reporting->reportingEvents.begin(), | |
| reporting->reportingEvents.end()); | |
| } | |
| this->notifyEventListeners(reporting->reportingEvents, nullptr); | |
| this->notifyListenerViewModels(reporting->reportingListenerViewModels); | |
| } | |
| if (currentIteration >= maxIterations) | |
| { | |
| fprintf( | |
| stderr, | |
| "%s StateMachine exceeded max event iterations" | |
| "on artboard %s\n", | |
| stateMachine()->name().c_str(), | |
| artboard()->name().c_str()); | |
| } | |
| } | |
| void StateMachineInstance::setExternalFocusManager(FocusManager* manager) | |
| { | |
| if (m_artboardInstance != nullptr) | |
| { | |
| m_artboardInstance->adoptFocusManager(manager); | |
| } | |
| } | |
| void StateMachineInstance::enableSemantics() | |
| { | |
| if (semanticManager() != nullptr) | |
| { | |
| return; | |
| } | |
| ensureInputExtras().semanticManager = std::make_unique<SemanticManager>(); | |
| if (m_artboardInstance != nullptr) | |
| { | |
| m_artboardInstance->buildSemanticTree(semanticManager(), nullptr); | |
| } | |
| } | |
| void StateMachineInstance::setExternalSemanticManager( | |
| SemanticManager* manager, | |
| rcp<SemanticNode> parentNode) | |
| { | |
| // An unallocated cluster means no external manager is set, so clearing one | |
| // on such an instance is a no-op — check before allocating. | |
| auto* existing = inputExtras(); | |
| if ((existing != nullptr ? existing->externalSemanticManager : nullptr) == | |
| manager) | |
| { | |
| return; | |
| } | |
| auto& extras = ensureInputExtras(); | |
| // Clean up the old semantic tree if one was built with a different manager. | |
| if (m_artboardInstance != nullptr && | |
| m_artboardInstance->semanticManager() != nullptr) | |
| { | |
| m_artboardInstance->cleanupSemanticTree(); | |
| } | |
| extras.externalSemanticManager = manager; | |
| // Rebuild with the new manager. semanticManager() now returns the external | |
| // manager if set, or the internal one if null. | |
| if (m_artboardInstance != nullptr) | |
| { | |
| m_artboardInstance->buildSemanticTree(semanticManager(), parentNode); | |
| } | |
| } | |
| void StateMachineInstance::queueFocusEvent( | |
| FocusListenerGroup* group, | |
| bool isFocus) | |
| { | |
| ensureInputExtras().queuedFocusEvents.push_back({group, isFocus}); | |
| markNeedsAdvance(); | |
| } | |
| void StateMachineInstance::setFocus(FocusData* focusData) | |
| { | |
| if (!focusManager()) | |
| { | |
| return; | |
| } | |
| if (focusData != nullptr) | |
| { | |
| auto node = focusData->focusNode(); | |
| auto* fm = focusManager(); | |
| fm->setFocus(node); | |
| } | |
| else | |
| { | |
| focusManager()->clearFocus(); | |
| } | |
| } | |
| StateMachineInstance::FocusState StateMachineInstance::focusState() const | |
| { | |
| FocusState state; | |
| const FocusManager* fm = focusManager(); | |
| if (fm == nullptr) | |
| { | |
| return state; | |
| } | |
| // primaryFocusPtr() avoids a refcount bump on this poll-friendly path. | |
| FocusNode* focus = fm->primaryFocusPtr(); | |
| if (focus == nullptr) | |
| { | |
| return state; | |
| } | |
| state.hasFocus = true; | |
| if (Focusable* focusable = focus->focusable()) | |
| { | |
| state.expectsKeyboardInput = focusable->acceptsKeyboardInput(); | |
| } | |
| return state; | |
| } | |
| const Artboard* StateMachineInstance::rootArtboard() const | |
| { | |
| const Artboard* artboard = m_artboardInstance; | |
| while (artboard != nullptr && artboard->host() != nullptr && | |
| artboard->host()->parentArtboard() != nullptr) | |
| { | |
| artboard = artboard->host()->parentArtboard(); | |
| } | |
| return artboard; | |
| } | |
| void StateMachineInstance::queueFocusTarget(FocusData* focusData) | |
| { | |
| if (focusData == nullptr) | |
| { | |
| return; | |
| } | |
| if (!focusManager()) | |
| { | |
| return; | |
| } | |
| focusManager()->requestFocus(focusData->focusNode(), rootArtboard()); | |
| markNeedsAdvance(); | |
| } | |
| void StateMachineInstance::queueClearFocus() | |
| { | |
| if (!focusManager()) | |
| { | |
| return; | |
| } | |
| focusManager()->requestClearFocus(rootArtboard()); | |
| markNeedsAdvance(); | |
| } | |
| void StateMachineInstance::queueFocusTraversal(uint32_t traversalKind) | |
| { | |
| if (!focusManager()) | |
| { | |
| return; | |
| } | |
| focusManager()->requestTraversal(traversalKind, rootArtboard()); | |
| markNeedsAdvance(); | |
| } | |
| void StateMachineInstance::processFocusEvents() | |
| { | |
| auto* extras = inputExtras(); | |
| if (extras == nullptr || extras->queuedFocusEvents.empty()) | |
| { | |
| return; | |
| } | |
| // Moved out before dispatch: a listener action can queue further focus | |
| // events, and those belong to the next advance, not this drain. | |
| auto events = std::move(extras->queuedFocusEvents); | |
| extras->queuedFocusEvents.clear(); | |
| for (const auto& event : events) | |
| { | |
| auto listener = event.group->listener(); | |
| bool isFocusEvent = event.isFocus; | |
| // Match listener type to event type | |
| if ((isFocusEvent && listener->hasListener(ListenerType::focus)) || | |
| (!isFocusEvent && listener->hasListener(ListenerType::blur))) | |
| { | |
| listener->performChanges( | |
| this, | |
| ListenerInvocation::focus(event.group, event.isFocus)); | |
| } | |
| } | |
| } | |
| void StateMachineInstance::queueSemanticEvent( | |
| SemanticListenerGroup* group, | |
| SemanticActionType actionType) | |
| { | |
| ensureInputExtras().queuedSemanticEvents.push_back({group, actionType}); | |
| markNeedsAdvance(); | |
| } | |
| void StateMachineInstance::processSemanticEvents() | |
| { | |
| auto* extras = inputExtras(); | |
| if (extras == nullptr || extras->queuedSemanticEvents.empty()) | |
| { | |
| return; | |
| } | |
| auto events = std::move(extras->queuedSemanticEvents); | |
| extras->queuedSemanticEvents.clear(); | |
| for (const auto& event : events) | |
| { | |
| if (event.group == nullptr) | |
| { | |
| continue; | |
| } | |
| auto* listener = event.group->listener(); | |
| if (listener == nullptr) | |
| { | |
| continue; | |
| } | |
| listener->performChanges( | |
| this, | |
| ListenerInvocation::semantic(event.group, event.actionType)); | |
| } | |
| } | |
| void StateMachineInstance::fireSemanticAction( | |
| uint32_t semanticNodeId, | |
| SemanticActionType actionType) | |
| { | |
| // The unified SemanticManager indexes every SD in the tree — top-level, | |
| // nested-artboard, and data-bound list items — so this lookup handles | |
| // all dispatch targets uniformly. SemanticData::fire*() routes the | |
| // event to listeners, which queue on their own owning state machine. | |
| auto* mgr = semanticManager(); | |
| if (mgr == nullptr) | |
| { | |
| return; | |
| } | |
| auto* node = mgr->nodeById(semanticNodeId); | |
| // A hidden node (collapsed, Hidden, or under a collapsed host) stays | |
| // registered; an action the platform sent before it saw the node leave | |
| // must not reach it. | |
| if (node == nullptr || !mgr->isInTree(node)) | |
| { | |
| return; | |
| } | |
| auto* sd = node->semanticData(); | |
| if (sd == nullptr) | |
| { | |
| // Boundary nodes have no owning SemanticData. | |
| return; | |
| } | |
| switch (actionType) | |
| { | |
| case SemanticActionType::tap: | |
| sd->fireSemanticTap(); | |
| break; | |
| case SemanticActionType::increase: | |
| sd->fireSemanticIncrease(); | |
| break; | |
| case SemanticActionType::decrease: | |
| sd->fireSemanticDecrease(); | |
| break; | |
| } | |
| } | |
| namespace | |
| { | |
| // Where the change window of the innermost state machine advancing on this | |
| // thread starts (its SMIBindables::changeBaseline), or kNotAdvancing outside | |
| // any advance. A machine made or reset meanwhile starts in that window, see | |
| // StateMachineInstance::forgetViewModelChanges. | |
| constexpr uint64_t kNotAdvancing = std::numeric_limits<uint64_t>::max(); | |
| thread_local uint64_t s_advancingChangeBaseline = kNotAdvancing; | |
| // Publishes a machine's change window while it advances. One that tracks no | |
| // view model changes has no window, and leaves the enclosing machine's without | |
| // touching the thread local, which most rows of a long list would otherwise | |
| // pay for every advance. | |
| class AdvancingChangeWindow | |
| { | |
| public: | |
| explicit AdvancingChangeWindow(const SMIBindables* bindables) | |
| : m_published(bindables != nullptr) | |
| { | |
| if (m_published) | |
| { | |
| m_outer = s_advancingChangeBaseline; | |
| s_advancingChangeBaseline = bindables->changeBaseline; | |
| } | |
| } | |
| // Restores rather than pops, so the next scope out undoes one a Luau | |
| // error longjmped past. | |
| ~AdvancingChangeWindow() | |
| { | |
| if (m_published) | |
| { | |
| s_advancingChangeBaseline = m_outer; | |
| } | |
| } | |
| AdvancingChangeWindow(const AdvancingChangeWindow&) = delete; | |
| AdvancingChangeWindow& operator=(const AdvancingChangeWindow&) = delete; | |
| private: | |
| bool m_published; | |
| uint64_t m_outer = 0; | |
| }; | |
| } // namespace | |
| bool StateMachineInstance::advance(float seconds, bool newFrame) | |
| { | |
| RIVE_PROF_SCOPE() | |
| if (m_drawOrderChangeCounter != m_artboardInstance->drawOrderChangeCounter()) | |
| { | |
| m_drawOrderChangeCounter = m_artboardInstance->drawOrderChangeCounter(); | |
| sortHitComponents(); | |
| } | |
| // View model changes the last frame's transition searches could see have | |
| // had their frame. See viewModelValueChanged. | |
| auto* bindables = m_bindables.get(); | |
| if (newFrame && bindables != nullptr) | |
| { | |
| // With a window per layer, each moves to where its own latest search | |
| // started, so a change a later layer made after an earlier one | |
| // searched is still pending for the earlier one. | |
| if (auto* windows = bindables->layerChanges.get()) | |
| { | |
| uint64_t earliest = std::numeric_limits<uint64_t>::max(); | |
| for (size_t i = 0; i < m_layerCount; i++) | |
| { | |
| windows[i].baseline = windows[i].searched; | |
| earliest = std::min(earliest, windows[i].baseline); | |
| } | |
| bindables->changeBaseline = earliest; | |
| } | |
| else | |
| { | |
| bindables->changeBaseline = bindables->changeSearched; | |
| } | |
| bindables->changesUsed.clear(); | |
| // applyEvents below handles every listener report so far. | |
| bindables | |
| ->listenerBaseline = ViewModelInstanceValue::latestChangeSequence(); | |
| } | |
| // Machines made or reset while this one advances, such as the ones a list | |
| // makes for its rows as its data binds apply, start in this frame. | |
| AdvancingChangeWindow changeWindow(bindables); | |
| if (newFrame) | |
| { | |
| processFocusEvents(); | |
| processSemanticEvents(); | |
| applyEvents(); | |
| m_needsAdvance = false; | |
| } | |
| // Poses re-apply when anything reached the artboard since the previous | |
| // advance started (see StateMachineLayerInstance::isPoseInPlace). | |
| const uint32_t poseSince = m_advanceChangeSerial; | |
| m_advanceChangeSerial = m_artboardInstance->changeSerial(); | |
| updateDataBinds(false); | |
| for (size_t i = 0; i < m_layerCount; i++) | |
| { | |
| if (m_layers[i].advance(this, seconds, newFrame, poseSince)) | |
| { | |
| m_needsAdvance = true; | |
| } | |
| } | |
| if (advanceDataBinds(seconds)) | |
| { | |
| m_needsAdvance = true; | |
| } | |
| if (m_inputInstances.size() > 0) | |
| { | |
| for (auto inst : m_inputInstances) | |
| { | |
| inst->advanced(); | |
| } | |
| } | |
| return m_needsAdvance || hasPendingReports(); | |
| } | |
| void StateMachineInstance::reset() {} | |
| bool StateMachineInstance::advanceAndApply(float seconds) | |
| { | |
| // This is the host's per-frame entry point, and it reaches the artboard | |
| // through advanceInternal, which skips the poll Artboard::advance does. | |
| // Deliver finished async work (script image decodes, fetches) here so | |
| // their promises resolve before script callbacks run. | |
| m_artboardInstance->pollAsyncWork(); | |
| if (m_artboardInstance->advanceWatermark(seconds)) | |
| { | |
| // The file's watermark is playing: settle the artboard at time zero so | |
| // its first frame is ready the instant the watermark ends, but don't | |
| // let it animate forward. Reporting "keep going" matters here, a false | |
| // would read as settled and stop the host's ticker mid pre-roll. | |
| advanceAndApply(0.0f, true); | |
| return true; | |
| } | |
| // Keep going while this artboard's scripts have async work outstanding, | |
| // even once settled: a host that stops ticking on false would never poll | |
| // the result in. | |
| return advanceAndApply(seconds, true) || | |
| m_artboardInstance->hasPendingAsyncWork(); | |
| } | |
| bool StateMachineInstance::advanceAndApply( | |
| float seconds, | |
| bool /*advanceViewModels*/) | |
| { | |
| RIVE_PROF_SCOPE_L(1) | |
| // Advancing by 0 could return false, when it shouldn't. Force keepGoing | |
| // to true. | |
| bool keepGoing = this->advance(seconds, true) || seconds == 0.0f; | |
| // The artboard's advances and updates below make machines too, in the | |
| // frame advance just started. | |
| AdvancingChangeWindow changeWindow(m_bindables.get()); | |
| if (focusManager()) | |
| { | |
| focusManager()->dropFocusIfFocusTargetHidden(); | |
| } | |
| if (m_artboardInstance->advanceInternal( | |
| seconds, | |
| AdvanceFlags::IsRoot | AdvanceFlags::Animate | | |
| AdvanceFlags::AdvanceNested | AdvanceFlags::NewFrame)) | |
| { | |
| keepGoing = true; | |
| } | |
| for (int outerOptionC = 0; outerOptionC < 5; outerOptionC++) | |
| { | |
| if (m_artboardInstance->updatePass(true)) | |
| { | |
| keepGoing = true; | |
| } | |
| // Authoritative drain: updatePass has recomputed renderOpacity and | |
| // propagated collapse, so target eligibility can be measured against | |
| // real values. Reaches nested artboards and artboard-component-list | |
| // items too, since they share this manager. A target can still need | |
| // several passes to settle, so a request that doesn't take here is | |
| // kept for the next iteration. | |
| if (focusManager()) | |
| { | |
| focusManager()->processPendingFocusRequests(rootArtboard()); | |
| focusManager()->dropFocusIfFocusTargetHidden(rootArtboard()); | |
| } | |
| // Advance all animations. | |
| if (this->tryChangeState()) | |
| { | |
| this->advance(0.0f, false); | |
| keepGoing = true; | |
| } | |
| if (m_artboardInstance->advanceInternal( | |
| 0.0f, | |
| AdvanceFlags::IsRoot | AdvanceFlags::Animate | | |
| AdvanceFlags::AdvanceNested)) | |
| { | |
| keepGoing = true; | |
| } | |
| if (!m_artboardInstance->hasDirt(ComponentDirt::Components) && | |
| !m_artboardInstance->hasDirtyLayout()) | |
| { | |
| break; | |
| } | |
| } | |
| // Last chance for this frame: picks up a request queued by the loop's | |
| // final tryChangeState, and drops anything that still can't take so an | |
| // unreachable target doesn't leave a request queued indefinitely. | |
| if (focusManager()) | |
| { | |
| focusManager()->finishPendingFocusRequests(rootArtboard()); | |
| } | |
| return keepGoing || hasPendingReports(); | |
| } | |
| void StateMachineInstance::markNeedsAdvance() | |
| { | |
| m_needsAdvance = true; | |
| wakeRow(); | |
| } | |
| bool StateMachineInstance::needsAdvance() const { return m_needsAdvance; } | |
| void StateMachineInstance::resetState() | |
| { | |
| wakeRow(); | |
| // Starting over, e.g. as a list reuses this instance for another item: | |
| // nothing pending from before carries into the new run. | |
| forgetViewModelChanges(); | |
| // Focus and semantic events queued for the previous run (the blur a row | |
| // gets when it leaves its list) would act on the next one's data. | |
| if (auto* extras = inputExtras()) | |
| { | |
| extras->queuedFocusEvents.clear(); | |
| extras->queuedSemanticEvents.clear(); | |
| } | |
| // So would a gesture still in flight: a press on the previous item would | |
| // complete as a click on the next one. | |
| for (auto& listenerGroup : m_listenerGroups) | |
| { | |
| listenerGroup->releaseAllPointers(); | |
| } | |
| for (size_t i = 0; i < m_layerCount; i++) | |
| { | |
| m_layers[i].resetState(this); | |
| } | |
| } | |
| std::string StateMachineInstance::name() const { return m_machine->name(); } | |
| SMIInput* StateMachineInstance::input(size_t index) const | |
| { | |
| if (index < m_inputInstances.size()) | |
| { | |
| return m_inputInstances[index]; | |
| } | |
| return nullptr; | |
| } | |
| template <typename SMType, typename InstType> | |
| InstType* StateMachineInstance::getNamedInput(const std::string& name) const | |
| { | |
| for (const auto inst : m_inputInstances) | |
| { | |
| auto input = inst->input(); | |
| if (input->is<SMType>() && input->name() == name) | |
| { | |
| return static_cast<InstType*>(inst); | |
| } | |
| } | |
| return nullptr; | |
| } | |
| SMIBool* StateMachineInstance::getBool(const std::string& name) const | |
| { | |
| return getNamedInput<StateMachineBool, SMIBool>(name); | |
| } | |
| SMINumber* StateMachineInstance::getNumber(const std::string& name) const | |
| { | |
| return getNamedInput<StateMachineNumber, SMINumber>(name); | |
| } | |
| SMITrigger* StateMachineInstance::getTrigger(const std::string& name) const | |
| { | |
| return getNamedInput<StateMachineTrigger, SMITrigger>(name); | |
| } | |
| void StateMachineInstance::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 state machine, | |
| // the artboard, and any sibling state machines sharing the context) off the | |
| // old main and onto the new one. | |
| dataBindContext()->setMainViewModelInstance(viewModelInstance); | |
| } | |
| bool StateMachineInstance::setGlobalViewModelInstance( | |
| const std::string& name, | |
| rcp<ViewModelInstance> viewModelInstance) | |
| { | |
| // A null instance is allowed: it empties the named slot below. | |
| auto file = m_artboardInstance->file(); | |
| if (file == 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 = file->viewModelId(name); | |
| if (slotKey >= file->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 = file->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; | |
| } | |
| void StateMachineInstance::bind() | |
| { | |
| if (dataBindContext() == nullptr) | |
| { | |
| // No data context yet: create an empty one so the view model | |
| // instances it needs can be completed on the fly below. | |
| dataBindContext(make_rcp<DataContext>(rcp<ViewModelInstance>(nullptr))); | |
| dataBindContext()->addDependentContainer(this); | |
| } | |
| // Make sure every view model instance the data context needs exists before | |
| // it is applied: the main instance plus one for each global view model. | |
| // Any that are missing are created (completed) on the fly. | |
| completeViewModelInstances(); | |
| // Apply the current data context: rebind the artboard and state machine | |
| // data binds in a single pass. | |
| m_artboardInstance->internalDataContext(dataBindContext()); | |
| internalDataContext(dataBindContext()); | |
| } | |
| void StateMachineInstance::completeViewModelInstances() | |
| { | |
| auto file = m_artboardInstance->file(); | |
| if (file == nullptr) | |
| { | |
| return; | |
| } | |
| // Ensure a main instance is present. The main is the entry not on the slot | |
| // keys; if there is none, create the artboard's default and place it first. | |
| if (dataBindContext()->mainViewModelInstance() == nullptr) | |
| { | |
| auto main = file->createDefaultViewModelInstance(m_artboardInstance); | |
| if (main != nullptr) | |
| { | |
| // setMainViewModelInstance re-points every attached container onto | |
| // the new instance. | |
| dataBindContext()->setMainViewModelInstance(main); | |
| } | |
| } | |
| // Ensure an instance exists for each global view model slot, creating any | |
| // missing ones. Occupancy is checked by slot key, so a cross-view-model | |
| // override already sitting in a slot is not treated as empty. | |
| for (auto* viewModel : file->globalViewModels()) | |
| { | |
| uint32_t slotKey = file->viewModelId(viewModel->name()); | |
| if (dataBindContext()->instanceForSlot(slotKey) != nullptr) | |
| { | |
| continue; | |
| } | |
| auto instance = file->createDefaultViewModelInstance(viewModel); | |
| if (instance != nullptr) | |
| { | |
| // setViewModelInstanceForSlot re-points every attached container | |
| // onto the new instance. | |
| dataBindContext()->setViewModelInstanceForSlot(slotKey, instance); | |
| } | |
| } | |
| } | |
| void StateMachineInstance::bindViewModelInstance( | |
| rcp<ViewModelInstance> viewModelInstance) | |
| { | |
| if (viewModelInstance == nullptr) | |
| { | |
| clearDataContext(); | |
| m_artboardInstance->unbind(); | |
| return; | |
| } | |
| setViewModelInstance(std::move(viewModelInstance)); | |
| bind(); | |
| } | |
| rcp<ViewModelInstance> StateMachineInstance::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 file = m_artboardInstance->file(); | |
| if (file == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| return dataBindContext()->instanceForSlot(file->viewModelId(name)); | |
| } | |
| void StateMachineInstance::bindDataContext(rcp<DataContext> dataContext) | |
| { | |
| clearDataContext(); | |
| dataContext->addDependentContainer(this); | |
| m_artboardInstance->clearDataContext(); | |
| m_artboardInstance->internalDataContext(dataContext); | |
| internalDataContext(dataContext); | |
| } | |
| void StateMachineInstance::inheritDataContext(rcp<DataContext> dataContext) | |
| { | |
| if (dataContext == nullptr) | |
| { | |
| return; | |
| } | |
| dataContext->addDependentContainer(this); | |
| internalDataContext(dataContext); | |
| } | |
| void StateMachineInstance::dataContext(rcp<DataContext> dataContext) | |
| { | |
| clearDataContext(); | |
| internalDataContext(dataContext); | |
| } | |
| void StateMachineInstance::initScriptedObjects() | |
| { | |
| auto* scripting = m_scripting.get(); | |
| if (scripting == nullptr) | |
| { | |
| return; | |
| } | |
| for (auto& obj : scripting->objects) | |
| { | |
| if (obj.second->scriptAsset() != nullptr) | |
| { | |
| if (!obj.second->userLuaInitDone()) | |
| { | |
| obj.second->scriptAsset()->initScriptedObject(obj.second); | |
| } | |
| obj.second->hydrateScriptInputs(); | |
| } | |
| } | |
| } | |
| void StateMachineInstance::internalDataContext(rcp<DataContext> dataContext) | |
| { | |
| // A root machine whose artboard hosts others tracks view model changes | |
| // from its bind on, however it is bound, even if none of its own | |
| // conditions read them: the machines its lists and nested artboards make | |
| // while it advances start in its frame (see forgetViewModelChanges). A | |
| // hosted machine without a window of its own leaves them the enclosing | |
| // machine's. Checked at bind rather than at creation, since a nested | |
| // artboard makes its machines before it gets its host. | |
| if (m_artboardInstance->host() == nullptr && | |
| m_artboardInstance->hostsArtboards()) | |
| { | |
| ensureBindables(); | |
| } | |
| bindDataBindsFromContext(dataContext); | |
| if (auto* reporting = this->reporting()) | |
| { | |
| for (auto listenerViewModel : reporting->listenerViewModels) | |
| { | |
| listenerViewModel->bindFromContext(dataContext); | |
| } | |
| } | |
| if (auto* scripting = m_scripting.get()) | |
| { | |
| for (auto& scriptedObjectItr : scripting->objects) | |
| { | |
| scriptedObjectItr.second->dataContext(dataContext); | |
| } | |
| } | |
| initScriptedObjects(); | |
| } | |
| void StateMachineInstance::rebind() | |
| { | |
| m_artboardInstance->clearDataContext(); | |
| m_artboardInstance->internalDataContext(dataBindContext()); | |
| internalDataContext(dataBindContext()); | |
| }; | |
| void StateMachineInstance::clearDataContext() | |
| { | |
| if (dataBindContext() != nullptr) | |
| { | |
| dataBindContext()->removeDependentContainer(this); | |
| dataBindContext(nullptr); | |
| } | |
| if (auto* reporting = this->reporting()) | |
| { | |
| for (auto& listenerViewModel : reporting->listenerViewModels) | |
| { | |
| listenerViewModel->clearDataContext(); | |
| } | |
| } | |
| } | |
| void StateMachineInstance::mainViewModelInstanceChanged() | |
| { | |
| unsettleLayers(); | |
| if (m_artboardInstance != nullptr) | |
| { | |
| m_artboardInstance->mainViewModelInstanceChanged(); | |
| } | |
| } | |
| void StateMachineInstance::unsettleLayers() | |
| { | |
| // Whatever unsettles a layer can change a quiet row too. | |
| wakeRow(); | |
| if (!m_anySettled) | |
| { | |
| return; | |
| } | |
| m_anySettled = false; | |
| for (uint32_t i = 0; i < m_layerCount; i++) | |
| { | |
| m_layers[i].unsettle(); | |
| } | |
| } | |
| void StateMachineInstance::wakeRow() | |
| { | |
| if (m_artboardInstance != nullptr && !m_destroying) | |
| { | |
| m_artboardInstance->wakeIfQuietRow(); | |
| } | |
| } | |
| AdvancingComponent::QuietState StateMachineInstance::rowQuietState() | |
| { | |
| using QuietState = AdvancingComponent::QuietState; | |
| // Scripts do work this can't see into. | |
| if (m_scripting.get() != nullptr) | |
| { | |
| return QuietState::never; | |
| } | |
| // Cheapest first: a busy row is checked again every frame. | |
| if (m_needsAdvance || m_bindsPending || hasDataBindWork() || | |
| m_drawOrderChangeCounter != m_artboardInstance->drawOrderChangeCounter()) | |
| { | |
| return QuietState::busy; | |
| } | |
| if (auto* reporting = this->reporting()) | |
| { | |
| if (!reporting->reportedEvents.empty() || | |
| !reporting->reportingEvents.empty() || | |
| !reporting->eventsAppliedDuringLoop.empty() || | |
| !reporting->reportedListenerViewModels.empty() || | |
| !reporting->reportingListenerViewModels.empty()) | |
| { | |
| return QuietState::busy; | |
| } | |
| } | |
| if (auto* extras = inputExtras()) | |
| { | |
| if (!extras->queuedFocusEvents.empty() || | |
| !extras->queuedSemanticEvents.empty()) | |
| { | |
| return QuietState::busy; | |
| } | |
| } | |
| // Every layer frozen, and none changed state last frame (a new frame's | |
| // advance clears that). | |
| for (uint32_t i = 0; i < m_layerCount; i++) | |
| { | |
| if (!m_layers[i].isFrozen(this, m_advanceChangeSerial) || | |
| m_layers[i].stateChangedOnAdvance()) | |
| { | |
| return QuietState::busy; | |
| } | |
| } | |
| // Converters that advance can't say whether they would. | |
| return mayAdvanceDataBinds() ? QuietState::never : QuietState::quiet; | |
| } | |
| void StateMachineInstance::addDirtyDataBind(DataBind* dataBind) | |
| { | |
| // A bound value is about to change. Layers that settled on the old one | |
| // must search again, and none may settle until it has been applied. Set | |
| // before the base, which skips binds already queued or polled. | |
| m_bindsPending = true; | |
| unsettleLayers(); | |
| DataBindContainer::addDirtyDataBind(dataBind); | |
| } | |
| void StateMachineInstance::updateDataBinds(bool applyTargetToSource) | |
| { | |
| // A nested call returns without applying anything, so only the outer pass | |
| // clears this; binds dirtied while it runs set it again. | |
| if (!isProcessingDataBinds()) | |
| { | |
| m_bindsPending = false; | |
| } | |
| DataBindContainer::updateDataBinds(applyTargetToSource); | |
| } | |
| void StateMachineInstance::dataContextChanged() | |
| { | |
| // View model conditions only evaluate with a data context. | |
| unsettleLayers(); | |
| } | |
| void StateMachineInstance::dropInstanceValueBindsTargeting(Core* target) | |
| { | |
| DataBindContainer::dropInstanceValueBindsTargeting(target); | |
| if (m_artboardInstance != nullptr) | |
| { | |
| m_artboardInstance->dropInstanceValueBindsTargeting(target); | |
| } | |
| } | |
| void StateMachineInstance::relinkDataContext() | |
| { | |
| unsettleLayers(); | |
| m_artboardInstance->relinkDataContext(); | |
| } | |
| void StateMachineInstance::rebuildDataBind(DataBind* dataBind) | |
| { | |
| if (dataBind->is<DataBindContext>()) | |
| { | |
| dataBind->as<DataBindContext>()->bindFromContext(dataBindContext().get()); | |
| } | |
| }; | |
| void StateMachineInstance::unbind() | |
| { | |
| clearDataContext(); | |
| unbindDataBinds(); | |
| } | |
| size_t StateMachineInstance::stateChangedCount() const | |
| { | |
| size_t count = 0; | |
| for (size_t i = 0; i < m_layerCount; i++) | |
| { | |
| if (m_layers[i].stateChangedOnAdvance()) | |
| { | |
| count++; | |
| } | |
| } | |
| return count; | |
| } | |
| const LayerState* StateMachineInstance::stateChangedByIndex(size_t index) const | |
| { | |
| size_t count = 0; | |
| for (size_t i = 0; i < m_layerCount; i++) | |
| { | |
| if (m_layers[i].stateChangedOnAdvance()) | |
| { | |
| if (count == index) | |
| { | |
| return m_layers[i].currentState(); | |
| } | |
| count++; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| size_t StateMachineInstance::currentAnimationCount() const | |
| { | |
| size_t count = 0; | |
| for (size_t i = 0; i < m_layerCount; i++) | |
| { | |
| if (m_layers[i].currentAnimation() != nullptr) | |
| { | |
| count++; | |
| } | |
| } | |
| return count; | |
| } | |
| const LinearAnimationInstance* StateMachineInstance::currentAnimationByIndex( | |
| size_t index) const | |
| { | |
| size_t count = 0; | |
| for (size_t i = 0; i < m_layerCount; i++) | |
| { | |
| if (m_layers[i].currentAnimation() != nullptr) | |
| { | |
| if (count == index) | |
| { | |
| return m_layers[i].currentAnimation(); | |
| } | |
| count++; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| bool StateMachineInstance::hasPendingReports() const | |
| { | |
| auto* reporting = this->reporting(); | |
| return reporting != nullptr && | |
| (!reporting->reportedEvents.empty() || | |
| !reporting->reportedListenerViewModels.empty()); | |
| } | |
| void StateMachineInstance::reportEvent(Event* event, float delaySeconds) | |
| { | |
| wakeRow(); | |
| ensureReporting().reportedEvents.push_back(EventReport(event, delaySeconds)); | |
| } | |
| void StateMachineInstance::reportListenerViewModel( | |
| ListenerViewModel* listenerViewModel) | |
| { | |
| wakeRow(); | |
| ensureReporting().reportedListenerViewModels.push_back(listenerViewModel); | |
| } | |
| std::size_t StateMachineInstance::reportedEventCount() const | |
| { | |
| auto* reporting = this->reporting(); | |
| if (reporting == nullptr) | |
| { | |
| return 0; | |
| } | |
| return reporting->eventsAppliedDuringLoop.size() + | |
| reporting->reportedEvents.size(); | |
| } | |
| const EventReport StateMachineInstance::reportedEventAt(std::size_t index) const | |
| { | |
| auto* reporting = this->reporting(); | |
| if (reporting == nullptr) | |
| { | |
| return EventReport(nullptr, 0.0f); | |
| } | |
| if (index < reporting->eventsAppliedDuringLoop.size()) | |
| { | |
| return reporting->eventsAppliedDuringLoop[index]; | |
| } | |
| index -= reporting->eventsAppliedDuringLoop.size(); | |
| if (index >= reporting->reportedEvents.size()) | |
| { | |
| return EventReport(nullptr, 0.0f); | |
| } | |
| return reporting->reportedEvents[index]; | |
| } | |
| void StateMachineInstance::notify( | |
| const std::vector<EventReport>& events, | |
| NestedArtboard* context) | |
| { | |
| notifyEventListeners(events, context); | |
| updateDataBinds(false); | |
| } | |
| void StateMachineInstance::notifyListenerViewModels( | |
| const std::vector<ListenerViewModel*>& events) | |
| { | |
| if (events.size() > 0) | |
| { | |
| for (auto& listenerViewModel : events) | |
| { | |
| listenerViewModel->listener()->performChanges( | |
| this, | |
| ListenerInvocation::viewModelChange(listenerViewModel)); | |
| } | |
| } | |
| } | |
| void StateMachineInstance::notifyEventListeners( | |
| const std::vector<EventReport>& events, | |
| NestedArtboard* source) | |
| { | |
| if (events.size() > 0) | |
| { | |
| // We trigger the listeners in order | |
| for (size_t i = 0; i < m_machine->listenerCount(); i++) | |
| { | |
| auto listener = m_machine->listener(i); | |
| auto target = artboard()->resolve(listener->targetId()); | |
| if (listener != nullptr && listener->hasListener(ListenerType::event) && | |
| (source == nullptr || source == target)) | |
| { | |
| for (const auto event : events) | |
| { | |
| auto sourceArtboard = source == nullptr ? artboard() | |
| : source->artboardInstance(); | |
| // NOTE: this issue can't happen anymore because a new | |
| // fix in the editor prevents selecting other artboard | |
| // as target. But the fix is kept here to fix older | |
| // files. listener->eventId() can point to an id from an | |
| // event in the context of this artboard or the | |
| // context of a nested artboard. Because those ids | |
| // belong to different contexts, they can have the | |
| // same value. So when the eventId is resolved | |
| // within one context, but actually pointing to the | |
| // other, it can return the wrong event object. If, | |
| // by chance, that event exists in the other | |
| // context, and is being reported, it will trigger | |
| // the wrong set of actions. This validation makes | |
| // sure that a listener must be targetting the | |
| // current artboard to disambiguate between external | |
| // and internal events. | |
| if (source == nullptr) | |
| { | |
| auto target = sourceArtboard->resolve(listener->targetId()); | |
| if (target && target != artboard() && !target->is<Event>()) | |
| { | |
| continue; | |
| } | |
| } | |
| if (listener->is<StateMachineListenerSingle>()) | |
| { | |
| auto listenerEvent = sourceArtboard->resolve( | |
| listener->as<StateMachineListenerSingle>()->eventId()); | |
| if (listenerEvent == event.event()) | |
| { | |
| listener->performChanges( | |
| this, | |
| ListenerInvocation::reportedEvent( | |
| event.event(), | |
| event.secondsDelay())); | |
| break; | |
| } | |
| } | |
| else | |
| { | |
| size_t index = 0; | |
| while (index < listener->listenerInputTypeCount()) | |
| { | |
| auto listenerInputType = listener->listenerInputType(index); | |
| if (listenerInputType->is<ListenerInputTypeEvent>()) | |
| { | |
| auto listenerInputTypeEvent = listenerInputType->as< | |
| ListenerInputTypeEvent>(); | |
| auto listenerEvent = sourceArtboard->resolve( | |
| listenerInputTypeEvent->eventId()); | |
| if (listenerEvent == event.event()) | |
| { | |
| listener->performChanges( | |
| this, | |
| ListenerInvocation::reportedEvent( | |
| event.event(), | |
| event.secondsDelay())); | |
| break; | |
| } | |
| } | |
| index += 1; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Bubble the event up to parent artboard state machines | |
| // immediately | |
| for (auto listener : nestedEventListeners()) | |
| { | |
| listener->notify(events, nestedArtboard()); | |
| } | |
| for (auto report : events) | |
| { | |
| auto event = report.event(); | |
| if (event->is<AudioEvent>()) | |
| { | |
| event->as<AudioEvent>()->play(); | |
| } | |
| } | |
| } | |
| } | |
| void StateMachineInstance::enablePointerEvents(int pointerId) | |
| { | |
| m_disabledPointerIds.eraseAll(pointerId); | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| hitShape->enablePointerEvents(pointerId); | |
| } | |
| if (auto* extras = inputExtras()) | |
| { | |
| for (const auto& group : extras->captureGroups) | |
| { | |
| group->enable(pointerId); | |
| } | |
| } | |
| // A drag ending turns everything back on, but a capture claim here still | |
| // holds until its own release. | |
| applyPointerClaims(pointerId); | |
| } | |
| void StateMachineInstance::disablePointerEvents(int pointerId) | |
| { | |
| m_disabledPointerIds.pushUnique(pointerId); | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| hitShape->disablePointerEvents(pointerId); | |
| } | |
| if (auto* extras = inputExtras()) | |
| { | |
| for (const auto& group : extras->captureGroups) | |
| { | |
| group->disable(pointerId); | |
| } | |
| } | |
| } | |
| bool StateMachineInstance::isPointerDisabled(int pointerId) const | |
| { | |
| const auto& ids = m_disabledPointerIds.view(); | |
| return std::find(ids.begin(), ids.end(), pointerId) != ids.end(); | |
| } | |
| HitResult StateMachineInstance::hoverExit(Vec2D position, int pointerId) | |
| { | |
| const bool disabled = isPointerDisabled(pointerId); | |
| // The exit releases every group's state for the pointer, the disabled | |
| // phase among them, so what turned the pointer off has to again. | |
| HitResult result = pointerExit(position, pointerId); | |
| if (disabled) | |
| { | |
| disablePointerEvents(pointerId); | |
| } | |
| return result; | |
| } | |
| BindableProperty* StateMachineInstance::bindablePropertyInstance( | |
| BindableProperty* bindableProperty) const | |
| { | |
| auto* bindables = this->bindables(); | |
| if (bindables == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| return bindables->propertyInstances.find(bindableProperty); | |
| } | |
| DataBind* StateMachineInstance::bindableDataBindToSource( | |
| BindableProperty* bindableProperty) const | |
| { | |
| auto* bindables = this->bindables(); | |
| if (bindables == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| auto dataBind = bindables->dataBindsToSource.find(bindableProperty); | |
| if (dataBind == bindables->dataBindsToSource.end()) | |
| { | |
| return nullptr; | |
| } | |
| return dataBind->second; | |
| } | |
| DataBind* StateMachineInstance::bindableDataBindToTarget( | |
| BindableProperty* bindableProperty) const | |
| { | |
| auto* bindables = this->bindables(); | |
| if (bindables == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| auto dataBind = bindables->dataBindsToTarget.find(bindableProperty); | |
| if (dataBind == bindables->dataBindsToTarget.end()) | |
| { | |
| return nullptr; | |
| } | |
| return dataBind->second; | |
| } | |
| size_t StateMachineInstance::layerIndex( | |
| const StateMachineLayerInstance* layer) const | |
| { | |
| // The layers are one array, so a layer's index is its offset in it. | |
| // std::less orders pointers that aren't into the array too. | |
| std::less<const StateMachineLayerInstance*> before; | |
| if (layer == nullptr || before(layer, m_layers) || | |
| !before(layer, m_layers + m_layerCount)) | |
| { | |
| return m_layerCount; | |
| } | |
| return static_cast<size_t>(layer - m_layers); | |
| } | |
| uint64_t StateMachineInstance::changeBaselineFor( | |
| const StateMachineLayerInstance* layer) const | |
| { | |
| auto* bindables = this->bindables(); | |
| if (bindables == nullptr) | |
| { | |
| return 0; | |
| } | |
| auto* windows = bindables->layerChanges.get(); | |
| size_t index = layerIndex(layer); | |
| return windows != nullptr && index < m_layerCount ? windows[index].baseline | |
| : bindables->changeBaseline; | |
| } | |
| bool StateMachineInstance::changePending( | |
| uint64_t changeSequence, | |
| const StateMachineLayerInstance* layer) const | |
| { | |
| return bindables() != nullptr && changeSequence > changeBaselineFor(layer); | |
| } | |
| bool StateMachineInstance::viewModelTriggerPendingForListeners( | |
| const ViewModelInstanceValue* trigger) const | |
| { | |
| auto* bindables = this->bindables(); | |
| return bindables != nullptr && | |
| trigger->changeSequence() > bindables->listenerBaseline; | |
| } | |
| bool StateMachineInstance::viewModelValueChanged( | |
| const ViewModelInstanceValue* value, | |
| const StateMachineLayerInstance* layer) const | |
| { | |
| auto* bindables = this->bindables(); | |
| if (bindables == nullptr || | |
| value->changeSequence() <= changeBaselineFor(layer)) | |
| { | |
| return false; | |
| } | |
| for (const auto& used : bindables->changesUsed) | |
| { | |
| if (used.first == layer && used.second == value) | |
| { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| void StateMachineInstance::useViewModelValue( | |
| const ViewModelInstanceValue* value, | |
| const StateMachineLayerInstance* layer) const | |
| { | |
| auto* bindables = this->bindables(); | |
| if (bindables == nullptr) | |
| { | |
| return; | |
| } | |
| for (const auto& used : bindables->changesUsed) | |
| { | |
| if (used.first == layer && used.second == value) | |
| { | |
| return; | |
| } | |
| } | |
| bindables->changesUsed.emplace_back(layer, value); | |
| } | |
| void StateMachineInstance::markViewModelChangesSearched( | |
| const StateMachineLayerInstance* layer) | |
| { | |
| auto* bindables = m_bindables.get(); | |
| if (bindables == nullptr) | |
| { | |
| return; | |
| } | |
| auto latest = ViewModelInstanceValue::latestChangeSequence(); | |
| bindables->changeSearched = latest; | |
| SMILayerChangeWindow* window = nullptr; | |
| if (auto* windows = bindables->layerChanges.get()) | |
| { | |
| size_t index = layerIndex(layer); | |
| if (index < m_layerCount) | |
| { | |
| window = &windows[index]; | |
| window->searched = latest; | |
| } | |
| } | |
| // A host above that stopped advancing this instance (collapsed or paused) | |
| // marks the changes made meanwhile. They had their frame while this | |
| // instance couldn't run, so they aren't pending for it now that it does. | |
| uint64_t inactive = 0; | |
| for (const Artboard* artboard = m_artboardInstance; | |
| artboard != nullptr && artboard->host() != nullptr; | |
| artboard = artboard->host()->parentArtboard()) | |
| { | |
| inactive = std::max(inactive, artboard->host()->inactiveChangeSequence()); | |
| } | |
| if (inactive > bindables->changeBaseline) | |
| { | |
| bindables->changeBaseline = inactive; | |
| } | |
| if (window != nullptr && inactive > window->baseline) | |
| { | |
| window->baseline = inactive; | |
| } | |
| } | |
| void StateMachineInstance::markFrozenLayerSearched( | |
| const StateMachineLayerInstance* layer) | |
| { | |
| // Only a layer's own window goes stale this way. The instance's moves with | |
| // every other layer's search, as it always has, and its next search | |
| // applies a collapsed host's mark (markViewModelChangesSearched). | |
| auto* bindables = m_bindables.get(); | |
| auto* windows = bindables == nullptr ? nullptr | |
| : bindables->layerChanges.get(); | |
| if (windows == nullptr) | |
| { | |
| return; | |
| } | |
| size_t index = layerIndex(layer); | |
| if (index < m_layerCount) | |
| { | |
| windows[index].searched = ViewModelInstanceValue::latestChangeSequence(); | |
| } | |
| } | |
| void StateMachineInstance::forgetViewModelChanges() | |
| { | |
| if (auto* bindables = m_bindables.get()) | |
| { | |
| auto latest = ViewModelInstanceValue::latestChangeSequence(); | |
| // Made or reset while another machine advances, as a list makes its | |
| // rows' machines, this instance starts in that machine's frame: what | |
| // is still pending there is pending here too, so a fire made just | |
| // before the frame that makes a row reaches it. Otherwise nothing made | |
| // so far is. | |
| uint64_t start = s_advancingChangeBaseline != kNotAdvancing | |
| ? s_advancingChangeBaseline | |
| : latest; | |
| bindables->changeBaseline = bindables->changeSearched = start; | |
| // Only a machine whose conditions read pending changes keeps a window | |
| // per layer; for the rest, the instance's is all anything reads. | |
| if (m_machine != nullptr && m_machine->readsPendingChanges() && | |
| m_layerCount > 0) | |
| { | |
| if (bindables->layerChanges == nullptr) | |
| { | |
| bindables->layerChanges = std::make_unique<SMILayerChangeWindow[]>( | |
| m_layerCount); | |
| } | |
| std::fill_n( | |
| bindables->layerChanges.get(), | |
| m_layerCount, | |
| SMILayerChangeWindow{start, start}); | |
| } | |
| bindables->listenerBaseline = latest; | |
| bindables->changesUsed.clear(); | |
| } | |
| } | |
| BindablePropertyNumber* StateMachineInstance::findTransitionPropertyInstance( | |
| const StateTransition* transition, | |
| uint32_t propertyKey) const | |
| { | |
| auto* bindables = this->bindables(); | |
| if (bindables == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| auto it = bindables->transitionPropertyInstances.find(transition); | |
| if (it != bindables->transitionPropertyInstances.end()) | |
| { | |
| auto propIt = it->second.find(propertyKey); | |
| if (propIt != it->second.end()) | |
| { | |
| return propIt->second; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| bool StateMachineInstance::hasFocusNodes() | |
| { | |
| if (!focusManager()) | |
| { | |
| return false; | |
| } | |
| auto* fm = focusManager(); | |
| return fm->hasFocusableContent(); | |
| } | |
| bool StateMachineInstance::focusNext() | |
| { | |
| if (!focusManager()) | |
| { | |
| return false; | |
| } | |
| auto* fm = focusManager(); | |
| return fm->focusNext(); | |
| } | |
| bool StateMachineInstance::focusPrevious() | |
| { | |
| if (!focusManager()) | |
| { | |
| return false; | |
| } | |
| auto* fm = focusManager(); | |
| return fm->focusPrevious(); | |
| } | |
| bool StateMachineInstance::focusLeft() | |
| { | |
| if (!focusManager()) | |
| { | |
| return false; | |
| } | |
| auto* fm = focusManager(); | |
| return fm->focusLeft(); | |
| } | |
| bool StateMachineInstance::focusRight() | |
| { | |
| if (!focusManager()) | |
| { | |
| return false; | |
| } | |
| auto* fm = focusManager(); | |
| return fm->focusRight(); | |
| } | |
| bool StateMachineInstance::focusUp() | |
| { | |
| if (!focusManager()) | |
| { | |
| return false; | |
| } | |
| auto* fm = focusManager(); | |
| return fm->focusUp(); | |
| } | |
| bool StateMachineInstance::focusDown() | |
| { | |
| if (!focusManager()) | |
| { | |
| return false; | |
| } | |
| auto* fm = focusManager(); | |
| return fm->focusDown(); | |
| } | |
| void StateMachineInstance::clearFocus() | |
| { | |
| if (!focusManager()) | |
| { | |
| return; | |
| } | |
| auto* fm = focusManager(); | |
| fm->clearFocus(); | |
| } | |
| bool StateMachineInstance::keyInput( | |
| Key key, | |
| KeyModifiers modifiers, | |
| bool isPressed, | |
| bool isRepeat) | |
| { | |
| if (!focusManager()) | |
| { | |
| return false; | |
| } | |
| auto* fm = focusManager(); | |
| return fm->keyInput(key, modifiers, isPressed, isRepeat); | |
| } | |
| bool StateMachineInstance::textInput(const std::string& text) | |
| { | |
| if (!focusManager()) | |
| { | |
| return false; | |
| } | |
| auto* fm = focusManager(); | |
| return fm->textInput(text); | |
| } | |
| std::string StateMachineInstance::selectedText() const | |
| { | |
| const auto* fm = focusManager(); | |
| if (!fm) | |
| { | |
| return ""; | |
| } | |
| return fm->selectedText(); | |
| } |