| #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/entry_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/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_layer.hpp" | |
| #include "rive/animation/state_machine_listener.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/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_flags.hpp" | |
| #include "rive/event_report.hpp" | |
| #include "rive/gesture_click_phase.hpp" | |
| #include "rive/hit_result.hpp" | |
| #include "rive/process_event_result.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/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 <unordered_map> | |
| #include <chrono> | |
| using namespace rive; | |
| namespace rive | |
| { | |
| class StateMachineLayerInstance | |
| { | |
| public: | |
| ~StateMachineLayerInstance() | |
| { | |
| delete m_anyStateInstance; | |
| delete m_currentState; | |
| delete m_stateFrom; | |
| } | |
| void init(StateMachineInstance* stateMachineInstance, | |
| const StateMachineLayer* layer, | |
| ArtboardInstance* instance) | |
| { | |
| m_stateMachineInstance = stateMachineInstance; | |
| m_artboardInstance = instance; | |
| assert(m_layer == nullptr); | |
| m_anyStateInstance = | |
| layer->anyState()->makeInstance(instance).release(); | |
| m_layer = layer; | |
| changeState(m_layer->entryState()); | |
| #ifdef TESTING | |
| 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); | |
| #endif | |
| } | |
| void resetState() | |
| { | |
| 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(m_layer->entryState()); | |
| } | |
| void updateMix(float seconds) | |
| { | |
| if (m_transition != nullptr && m_stateFrom != nullptr && | |
| m_transition->duration() != 0) | |
| { | |
| m_mix = std::min( | |
| 1.0f, | |
| std::max(0.0f, | |
| (m_mix + seconds / m_transition->mixTime( | |
| m_stateFrom->state())))); | |
| if (m_mix == 1.0f && !m_transitionCompleted) | |
| { | |
| m_transitionCompleted = true; | |
| clearAnimationReset(); | |
| fireEvents(StateMachineFireOccurance::atEnd, | |
| m_transition->events()); | |
| } | |
| } | |
| else | |
| { | |
| m_mix = 1.0f; | |
| } | |
| } | |
| bool advance(float seconds, bool newFrame) | |
| { | |
| if (newFrame) | |
| { | |
| m_stateMachineChangedOnAdvance = false; | |
| } | |
| m_currentState->advance(seconds, m_stateMachineInstance); | |
| updateMix(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, m_stateMachineInstance); | |
| } | |
| apply(); | |
| bool changedState = false; | |
| for (int i = 0; updateState(); i++) | |
| { | |
| changedState = true; | |
| apply(); | |
| if (i == maxIterations) | |
| { | |
| fprintf(stderr, | |
| "%s StateMachine exceeded max iterations in layer %s " | |
| "on artboard %s\n", | |
| m_stateMachineInstance->stateMachine()->name().c_str(), | |
| m_layer->name().c_str(), | |
| m_stateMachineInstance->artboard()->name().c_str()); | |
| return false; | |
| } | |
| } | |
| m_currentState->clearSpilledTime(); | |
| return changedState || m_mix != 1.0f || m_waitingForExit || | |
| (m_currentState != nullptr && m_currentState->keepGoing()); | |
| } | |
| bool isTransitioning() | |
| { | |
| return m_transition != nullptr && m_stateFrom != nullptr && | |
| m_transition->duration() != 0 && m_mix < 1.0f; | |
| } | |
| bool updateState() | |
| { | |
| // 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()) | |
| { | |
| return false; | |
| } | |
| m_waitingForExit = false; | |
| if (tryChangeState(m_anyStateInstance)) | |
| { | |
| return true; | |
| } | |
| return tryChangeState(m_currentState); | |
| } | |
| void fireEvents(StateMachineFireOccurance occurs, | |
| const std::vector<StateMachineFireAction*>& fireEvents) | |
| { | |
| for (auto event : fireEvents) | |
| { | |
| if (event->occurs() == occurs) | |
| { | |
| event->perform(m_stateMachineInstance); | |
| } | |
| } | |
| } | |
| bool canChangeState(const LayerState* stateTo) | |
| { | |
| return !( | |
| (m_currentState == nullptr ? nullptr : m_currentState->state()) == | |
| stateTo); | |
| } | |
| double randomValue() { return RandomProvider::generateRandomFloat(); } | |
| bool changeState(const LayerState* stateTo) | |
| { | |
| if ((m_currentState == nullptr ? nullptr : m_currentState->state()) == | |
| stateTo) | |
| { | |
| return false; | |
| } | |
| // Fire end events for the state we're changing from. | |
| if (m_currentState != nullptr) | |
| { | |
| fireEvents(StateMachineFireOccurance::atEnd, | |
| m_currentState->state()->events()); | |
| } | |
| m_currentState = | |
| stateTo == nullptr | |
| ? nullptr | |
| : stateTo->makeInstance(m_artboardInstance).release(); | |
| // Fire start events for the state we're changing to. | |
| if (m_currentState != nullptr) | |
| { | |
| fireEvents(StateMachineFireOccurance::atStart, | |
| m_currentState->state()->events()); | |
| } | |
| return true; | |
| } | |
| StateTransition* findRandomTransition(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, | |
| m_stateMachineInstance, | |
| 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(m_stateMachineInstance, | |
| this); | |
| return transition; | |
| } | |
| currentWeight += transitionWeight; | |
| index++; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| StateTransition* findAllowedTransition(StateInstance* stateFromInstance) | |
| { | |
| auto stateFrom = stateFromInstance->state(); | |
| // If it should randomize | |
| if ((static_cast<LayerStateFlags>(stateFrom->flags()) & | |
| LayerStateFlags::Random) == LayerStateFlags::Random) | |
| { | |
| return findRandomTransition(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, | |
| m_stateMachineInstance, | |
| this); | |
| if (allowed == AllowTransition::yes) | |
| { | |
| transition->evaluatedRandomWeight( | |
| transition->randomWeight()); | |
| transition->useLayerInConditions(m_stateMachineInstance, | |
| this); | |
| return transition; | |
| } | |
| else | |
| { | |
| transition->evaluatedRandomWeight(0); | |
| if (allowed == AllowTransition::waitingForExit) | |
| { | |
| m_waitingForExit = true; | |
| } | |
| } | |
| } | |
| } | |
| return nullptr; | |
| } | |
| void buildAnimationResetForTransition() | |
| { | |
| m_animationReset = | |
| AnimationResetFactory::fromStates(m_stateFrom, | |
| m_currentState, | |
| m_artboardInstance); | |
| } | |
| void clearAnimationReset() | |
| { | |
| if (m_animationReset != nullptr) | |
| { | |
| AnimationResetFactory::release(std::move(m_animationReset)); | |
| m_animationReset = nullptr; | |
| } | |
| } | |
| bool tryChangeState(StateInstance* stateFromInstance) | |
| { | |
| if (stateFromInstance == nullptr) | |
| { | |
| return false; | |
| } | |
| auto outState = m_currentState; | |
| auto transition = findAllowedTransition(stateFromInstance); | |
| if (transition != nullptr) | |
| { | |
| clearAnimationReset(); | |
| changeState(transition->stateTo()); | |
| m_stateMachineChangedOnAdvance = true; | |
| // state actually has changed | |
| m_transition = transition; | |
| fireEvents(StateMachineFireOccurance::atStart, | |
| transition->events()); | |
| if (transition->duration() == 0) | |
| { | |
| m_transitionCompleted = true; | |
| fireEvents(StateMachineFireOccurance::atEnd, | |
| transition->events()); | |
| } | |
| else | |
| { | |
| m_transitionCompleted = false; | |
| } | |
| if (m_stateFrom != m_anyStateInstance) | |
| { | |
| // Old state from is done. | |
| delete m_stateFrom; | |
| } | |
| m_stateFrom = outState; | |
| if (!m_transitionCompleted) | |
| { | |
| buildAnimationResetForTransition(); | |
| } | |
| // 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_stateFrom != nullptr && | |
| m_stateFrom->state()->is<AnimationState>() && | |
| m_currentState != nullptr) | |
| { | |
| auto instance = | |
| static_cast<AnimationStateInstance*>(m_stateFrom) | |
| ->animationInstance(); | |
| auto spilledTime = instance->spilledTime(); | |
| m_currentState->advance(spilledTime, m_stateMachineInstance); | |
| } | |
| m_mix = 0.0f; | |
| updateMix(0.0f); | |
| m_waitingForExit = false; | |
| return true; | |
| } | |
| return false; | |
| } | |
| void apply(/*Artboard* artboard*/) | |
| { | |
| if (m_animationReset != nullptr) | |
| { | |
| m_animationReset->apply(m_artboardInstance); | |
| } | |
| if (m_holdAnimation != nullptr) | |
| { | |
| m_holdAnimation->apply(m_artboardInstance, m_holdTime, m_mixFrom); | |
| m_holdAnimation = nullptr; | |
| } | |
| KeyFrameInterpolator* interpolator = nullptr; | |
| if (m_transition != nullptr && m_transition->interpolator() != nullptr) | |
| { | |
| interpolator = m_transition->interpolator(); | |
| } | |
| if (m_stateFrom != nullptr && m_mix < 1.0f) | |
| { | |
| auto fromMix = interpolator != nullptr | |
| ? interpolator->transform(m_mixFrom) | |
| : m_mixFrom; | |
| m_stateFrom->apply(m_artboardInstance, fromMix); | |
| } | |
| if (m_currentState != nullptr) | |
| { | |
| auto mix = interpolator != nullptr ? interpolator->transform(m_mix) | |
| : m_mix; | |
| m_currentState->apply(m_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; | |
| StateMachineInstance* m_stateMachineInstance = nullptr; | |
| const StateMachineLayer* m_layer = nullptr; | |
| ArtboardInstance* m_artboardInstance = nullptr; | |
| StateInstance* m_anyStateInstance = nullptr; | |
| StateInstance* m_currentState = nullptr; | |
| StateInstance* m_stateFrom = nullptr; | |
| const StateTransition* m_transition = nullptr; | |
| std::unique_ptr<AnimationReset> m_animationReset = nullptr; | |
| bool m_transitionCompleted = false; | |
| bool m_holdAnimationFrom = false; | |
| float m_mix = 1.0f; | |
| float m_mixFrom = 1.0f; | |
| bool m_stateMachineChangedOnAdvance = false; | |
| bool m_waitingForExit = false; | |
| /// Used to ensure a specific animation is applied on the next apply. | |
| const LinearAnimation* m_holdAnimation = nullptr; | |
| float m_holdTime = 0.0f; | |
| }; | |
| class _PointerData | |
| { | |
| public: | |
| bool isHovered = false; | |
| bool isPrevHovered = false; | |
| GestureClickPhase phase = GestureClickPhase::out; | |
| Vec2D* previousPosition() { return &m_previousPosition; } | |
| private: | |
| Vec2D m_previousPosition = Vec2D(0, 0); | |
| }; | |
| class ListenerGroup | |
| { | |
| public: | |
| ListenerGroup(const StateMachineListener* listener) : m_listener(listener) | |
| {} | |
| virtual ~ListenerGroup() | |
| { | |
| for (auto& pointerData : m_pointers) | |
| { | |
| delete pointerData.second; | |
| } | |
| for (auto& pointerData : m_pointersPool) | |
| { | |
| delete pointerData; | |
| } | |
| } | |
| _PointerData* pointerData(int id) | |
| { | |
| if (m_pointers.find(id) == m_pointers.end()) | |
| { | |
| if (m_pointersPool.size() > 0) | |
| { | |
| m_pointers[id] = m_pointersPool.back(); | |
| m_pointersPool.pop_back(); | |
| } | |
| else | |
| { | |
| m_pointers[id] = new _PointerData(); | |
| } | |
| } | |
| return m_pointers[id]; | |
| } | |
| void consume() { m_isConsumed = true; } | |
| void hover(int id) | |
| { | |
| auto pointer = pointerData(id); | |
| pointer->isHovered = true; | |
| } | |
| void reset(int pointerId) | |
| { | |
| auto pointer = pointerData(pointerId); | |
| if (pointer->phase != GestureClickPhase::disabled) | |
| { | |
| m_isConsumed = false; | |
| pointer->isPrevHovered = pointer->isHovered; | |
| pointer->isHovered = false; | |
| } | |
| if (pointer->phase == GestureClickPhase::clicked) | |
| { | |
| pointer->phase = GestureClickPhase::out; | |
| } | |
| } | |
| void releaseEvent(int pointerId) | |
| { | |
| if (m_pointers.find(pointerId) != m_pointers.end()) | |
| { | |
| // Reset pointer data before caching it | |
| auto pointerData = m_pointers[pointerId]; | |
| pointerData->isHovered = false; | |
| pointerData->isPrevHovered = false; | |
| pointerData->phase = GestureClickPhase::out; | |
| auto previousPosition = pointerData->previousPosition(); | |
| previousPosition->x = 0; | |
| previousPosition->y = 0; | |
| m_pointersPool.push_back(m_pointers[pointerId]); | |
| m_pointers.erase(pointerId); | |
| } | |
| } | |
| virtual void enable(int pointerId = 0) | |
| { | |
| auto pointer = pointerData(pointerId); | |
| pointer->phase = GestureClickPhase::out; | |
| } | |
| virtual void disable(int pointerId = 0) | |
| { | |
| auto pointer = pointerData(pointerId); | |
| pointer->phase = GestureClickPhase::disabled; | |
| consume(); | |
| } | |
| bool isConsumed() { return m_isConsumed; } | |
| virtual bool canEarlyOut(Component* drawable) | |
| { | |
| auto listenerType = m_listener->listenerType(); | |
| return !(listenerType == ListenerType::enter || | |
| listenerType == ListenerType::exit || | |
| listenerType == ListenerType::move); | |
| } | |
| virtual bool needsDownListener(Component* drawable) | |
| { | |
| auto listenerType = m_listener->listenerType(); | |
| return listenerType == ListenerType::down || | |
| listenerType == ListenerType::click; | |
| } | |
| virtual bool needsUpListener(Component* drawable) | |
| { | |
| auto listenerType = m_listener->listenerType(); | |
| return listenerType == ListenerType::up || | |
| listenerType == ListenerType::click; | |
| } | |
| virtual ProcessEventResult processEvent( | |
| Component* component, | |
| Vec2D position, | |
| int pointerId, | |
| ListenerType hitEvent, | |
| bool canHit, | |
| float timeStamp, | |
| StateMachineInstance* stateMachineInstance) | |
| { | |
| // Because each group is tested individually for its hover state, a | |
| // group could be marked "incorrectly" as hovered at this point. But | |
| // once we iterate each element in the drawing order, that group can be | |
| // occluded by an opaque target on top of it. So although it is hovered | |
| // in isolation, it shouldn't be considered as hovered in the full | |
| // context. In this case, we unhover the group so it is not marked as | |
| // previously hovered. | |
| auto pointer = pointerData(pointerId); | |
| if (!canHit && pointer->isHovered) | |
| { | |
| pointer->isHovered = false; | |
| } | |
| bool isGroupHovered = canHit ? pointer->isHovered : false; | |
| bool hoverChange = pointer->isPrevHovered != isGroupHovered; | |
| // If hover has changes, it means that the element is hovered for the | |
| // first time. Previous positions need to be reset to avoid jumps. | |
| auto previousPosition = pointer->previousPosition(); | |
| if (hoverChange && isGroupHovered) | |
| { | |
| previousPosition->x = position.x; | |
| previousPosition->y = position.y; | |
| } | |
| // Handle click gesture phases. A click gesture has two phases. | |
| // First one attached to a pointer down actions, second one attached to | |
| // a pointer up action. Both need to act on a shape of the listener | |
| // group. | |
| if (isGroupHovered) | |
| { | |
| if (hitEvent == ListenerType::down) | |
| { | |
| pointer->phase = GestureClickPhase::down; | |
| } | |
| else if (hitEvent == ListenerType::up && | |
| pointer->phase == GestureClickPhase::down) | |
| { | |
| pointer->phase = GestureClickPhase::clicked; | |
| } | |
| } | |
| else | |
| { | |
| if (hitEvent == ListenerType::down || hitEvent == ListenerType::up) | |
| { | |
| pointer->phase = GestureClickPhase::out; | |
| } | |
| } | |
| auto _listener = listener(); | |
| // Always update hover states regardless of which specific listener type | |
| // we're trying to trigger. | |
| // If hover has changed and: | |
| // - it's hovering and the listener is of type enter | |
| // - it's not hovering and the listener is of type exit | |
| if (hoverChange && ((isGroupHovered && _listener->listenerType() == | |
| ListenerType::enter) || | |
| (!isGroupHovered && | |
| _listener->listenerType() == ListenerType::exit))) | |
| { | |
| _listener->performChanges( | |
| stateMachineInstance, | |
| position, | |
| Vec2D(previousPosition->x, previousPosition->y)); | |
| stateMachineInstance->markNeedsAdvance(); | |
| consume(); | |
| } | |
| // Perform changes if: | |
| // - the click gesture is complete and the listener is of type click | |
| // - the event type matches the listener type and it is hovering the | |
| // group | |
| if ((pointer->phase == GestureClickPhase::clicked && | |
| _listener->listenerType() == ListenerType::click) || | |
| (isGroupHovered && hitEvent == _listener->listenerType())) | |
| { | |
| _listener->performChanges( | |
| stateMachineInstance, | |
| position, | |
| Vec2D(previousPosition->x, previousPosition->y)); | |
| stateMachineInstance->markNeedsAdvance(); | |
| consume(); | |
| } | |
| previousPosition->x = position.x; | |
| previousPosition->y = position.y; | |
| return ProcessEventResult::pointer; | |
| } | |
| const StateMachineListener* listener() const { return m_listener; }; | |
| // A vector storing the previous position for this specific listener gorup | |
| private: | |
| // Consumed listeners aren't processed again in the current frame | |
| bool m_isConsumed = false; | |
| const StateMachineListener* m_listener; | |
| std::unordered_map<int, _PointerData*> m_pointers; | |
| std::vector<_PointerData*> m_pointersPool; | |
| }; | |
| class DraggableConstraintListenerGroup : public ListenerGroup | |
| { | |
| public: | |
| DraggableConstraintListenerGroup(const StateMachineListener* listener, | |
| DraggableConstraint* constraint, | |
| DraggableProxy* draggable) : | |
| ListenerGroup(listener), | |
| m_constraint(constraint), | |
| m_draggable(draggable) | |
| {} | |
| ~DraggableConstraintListenerGroup() | |
| { | |
| delete listener(); | |
| delete m_draggable; | |
| } | |
| void enable(int pointerId = 0) override {} | |
| void disable(int pointerId = 0) override {} | |
| DraggableConstraint* constraint() { return m_constraint; } | |
| bool canEarlyOut(Component* drawable) override { return false; } | |
| bool needsDownListener(Component* drawable) override { return true; } | |
| bool needsUpListener(Component* drawable) override { return true; } | |
| ProcessEventResult processEvent( | |
| Component* component, | |
| Vec2D position, | |
| int pointerId, | |
| ListenerType hitEvent, | |
| bool canHit, | |
| float timeStamp, | |
| StateMachineInstance* stateMachineInstance) override | |
| { | |
| auto pointer = pointerData(pointerId); | |
| auto prevPhase = pointer->phase; | |
| ListenerGroup::processEvent(component, | |
| position, | |
| pointerId, | |
| hitEvent, | |
| canHit, | |
| timeStamp, | |
| stateMachineInstance); | |
| if (prevPhase == GestureClickPhase::down && | |
| (pointer->phase == GestureClickPhase::clicked || | |
| pointer->phase == GestureClickPhase::out)) | |
| { | |
| m_draggable->endDrag(position, timeStamp); | |
| if (m_hasScrolled) | |
| { | |
| stateMachineInstance->dragEnd(position, timeStamp, pointerId); | |
| m_hasScrolled = false; | |
| return ProcessEventResult::scroll; | |
| } | |
| } | |
| else if (prevPhase != GestureClickPhase::down && | |
| pointer->phase == GestureClickPhase::down) | |
| { | |
| m_draggable->startDrag(position, timeStamp); | |
| m_hasScrolled = false; | |
| } | |
| else if (hitEvent == ListenerType::move && | |
| pointer->phase == GestureClickPhase::down) | |
| { | |
| auto hasDragged = m_draggable->drag(position, timeStamp); | |
| if (hasDragged) | |
| { | |
| if (!m_hasScrolled) | |
| { | |
| stateMachineInstance->dragStart(position, | |
| timeStamp, | |
| false, | |
| pointerId); | |
| } | |
| m_hasScrolled = true; | |
| return ProcessEventResult::scroll; | |
| } | |
| } | |
| return ProcessEventResult::none; | |
| } | |
| private: | |
| DraggableConstraint* m_constraint; | |
| DraggableProxy* m_draggable; | |
| bool m_hasScrolled = 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. | |
| 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; } | |
| 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 processEvent(Vec2D position, | |
| ListenerType hitType, | |
| bool canHit, | |
| float timeStamp, | |
| int pointerId) 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) | |
| { | |
| if (listenerGroup->isConsumed()) | |
| { | |
| continue; | |
| } | |
| if (listenerGroup->processEvent(m_component, | |
| position, | |
| pointerId, | |
| hitType, | |
| 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); | |
| } | |
| }; | |
| 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()) | |
| { | |
| return false; | |
| } | |
| Vec2D nestedPosition; | |
| if (!nestedArtboard->worldToLocal(position, &nestedPosition)) | |
| { | |
| // Mounted artboard isn't ready or has a 0 scale transform. | |
| return false; | |
| } | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (nestedAnimation->is<NestedStateMachine>()) | |
| { | |
| auto nestedStateMachine = | |
| nestedAnimation->as<NestedStateMachine>(); | |
| if (nestedStateMachine->hitTest(nestedPosition)) | |
| { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| HitResult processEvent(Vec2D position, | |
| ListenerType hitType, | |
| bool canHit, | |
| float timeStamp, | |
| int pointerId) override | |
| { | |
| auto nestedArtboard = m_component->as<NestedArtboard>(); | |
| HitResult hitResult = HitResult::none; | |
| if (nestedArtboard->isCollapsed()) | |
| { | |
| return hitResult; | |
| } | |
| Vec2D nestedPosition; | |
| if (!nestedArtboard->worldToLocal(position, &nestedPosition)) | |
| { | |
| // Mounted artboard isn't ready or has a 0 scale transform. | |
| return hitResult; | |
| } | |
| for (auto nestedAnimation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (nestedAnimation->is<NestedStateMachine>()) | |
| { | |
| auto nestedStateMachine = | |
| nestedAnimation->as<NestedStateMachine>(); | |
| if (canHit) | |
| { | |
| switch (hitType) | |
| { | |
| case ListenerType::down: | |
| hitResult = | |
| nestedStateMachine->pointerDown(nestedPosition, | |
| pointerId); | |
| break; | |
| case ListenerType::up: | |
| hitResult = | |
| nestedStateMachine->pointerUp(nestedPosition, | |
| pointerId); | |
| break; | |
| case ListenerType::move: | |
| hitResult = | |
| nestedStateMachine->pointerMove(nestedPosition, | |
| timeStamp, | |
| pointerId); | |
| break; | |
| case ListenerType::dragStart: | |
| nestedStateMachine->dragStart(nestedPosition, | |
| timeStamp, | |
| pointerId); | |
| break; | |
| case ListenerType::dragEnd: | |
| nestedStateMachine->dragEnd(nestedPosition, | |
| timeStamp, | |
| pointerId); | |
| break; | |
| case ListenerType::exit: | |
| hitResult = | |
| nestedStateMachine->pointerExit(nestedPosition, | |
| pointerId); | |
| break; | |
| case ListenerType::enter: | |
| case ListenerType::event: | |
| case ListenerType::click: | |
| case ListenerType::draggableConstraint: | |
| case ListenerType::textInput: | |
| case ListenerType::viewModel: | |
| break; | |
| } | |
| } | |
| else | |
| { | |
| switch (hitType) | |
| { | |
| case ListenerType::down: | |
| case ListenerType::up: | |
| case ListenerType::move: | |
| 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::draggableConstraint: | |
| case ListenerType::textInput: | |
| case ListenerType::viewModel: | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| return hitResult; | |
| } | |
| void prepareEvent(Vec2D position, | |
| ListenerType hitType, | |
| int pointerId) override | |
| {} | |
| }; | |
| 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()) | |
| { | |
| return false; | |
| } | |
| for (int i = (int)componentList->artboardCount(); i >= 0; i--) | |
| { | |
| 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; | |
| } | |
| HitResult processEvent(Vec2D position, | |
| ListenerType hitType, | |
| bool canHit, | |
| float timeStamp, | |
| int pointerId) override | |
| { | |
| auto componentList = m_component->as<ArtboardComponentList>(); | |
| HitResult hitResult = HitResult::none; | |
| bool runningCanHit = canHit; | |
| if (componentList->isCollapsed()) | |
| { | |
| return hitResult; | |
| } | |
| for (int i = (int)componentList->artboardCount(); i >= 0; i--) | |
| { | |
| Vec2D listPosition; | |
| bool hit = componentList->worldToLocal(position, &listPosition, i); | |
| if (!hit) | |
| { | |
| continue; | |
| } | |
| auto stateMachine = componentList->stateMachineInstance(i); | |
| if (stateMachine != nullptr) | |
| { | |
| HitResult itemHitResult = HitResult::none; | |
| if (runningCanHit) | |
| { | |
| switch (hitType) | |
| { | |
| case ListenerType::down: | |
| itemHitResult = | |
| stateMachine->pointerDown(listPosition, | |
| pointerId); | |
| break; | |
| case ListenerType::up: | |
| itemHitResult = | |
| stateMachine->pointerUp(listPosition, | |
| pointerId); | |
| break; | |
| case ListenerType::move: | |
| itemHitResult = | |
| stateMachine->pointerMove(listPosition, | |
| timeStamp, | |
| pointerId); | |
| break; | |
| case ListenerType::exit: | |
| itemHitResult = | |
| stateMachine->pointerExit(listPosition, | |
| pointerId); | |
| break; | |
| case ListenerType::dragStart: | |
| stateMachine->dragStart(listPosition, | |
| 0, | |
| true, | |
| pointerId); | |
| break; | |
| case ListenerType::dragEnd: | |
| stateMachine->dragEnd(listPosition, 0, pointerId); | |
| break; | |
| case ListenerType::enter: | |
| case ListenerType::event: | |
| case ListenerType::click: | |
| case ListenerType::draggableConstraint: | |
| case ListenerType::textInput: | |
| case ListenerType::viewModel: | |
| break; | |
| } | |
| } | |
| else | |
| { | |
| switch (hitType) | |
| { | |
| case ListenerType::down: | |
| case ListenerType::up: | |
| case ListenerType::move: | |
| 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::draggableConstraint: | |
| case ListenerType::textInput: | |
| case ListenerType::viewModel: | |
| 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; | |
| } | |
| void prepareEvent(Vec2D position, | |
| ListenerType hitType, | |
| int pointerId) override | |
| {} | |
| }; | |
| class ListenerViewModel : public Dirtyable | |
| { | |
| public: | |
| virtual ~ListenerViewModel() = default; | |
| ListenerViewModel(StateMachineInstance* smInstance, | |
| const StateMachineListener* listener) : | |
| m_stateMachineInstance(smInstance), m_listener(listener) | |
| {} | |
| void clearDataContext() | |
| { | |
| if (m_viewModelInstanceValue != nullptr) | |
| { | |
| m_viewModelInstanceValue->removeDependent(this); | |
| m_viewModelInstanceValue = nullptr; | |
| } | |
| } | |
| void bindFromContext(DataContext* dataContext) | |
| { | |
| clearDataContext(); | |
| auto path = m_listener->viewModelPathIdsBuffer(); | |
| auto vmProp = dataContext->getViewModelProperty(path); | |
| if (vmProp != nullptr) | |
| { | |
| m_viewModelInstanceValue = vmProp; | |
| vmProp->addDependent(this); | |
| } | |
| } | |
| void addDirt(ComponentDirt value, bool recurse) | |
| { | |
| if (m_viewModelInstanceValue && | |
| m_viewModelInstanceValue->is<ViewModelInstanceTrigger>()) | |
| { | |
| if (m_viewModelInstanceValue->as<ViewModelInstanceTrigger>() | |
| ->propertyValue() != 0) | |
| { | |
| m_stateMachineInstance->reportListenerViewModel(this); | |
| } | |
| } | |
| } | |
| const StateMachineListener* listener() { return m_listener; } | |
| private: | |
| StateMachineInstance* m_stateMachineInstance = nullptr; | |
| const StateMachineListener* m_listener = nullptr; | |
| ViewModelInstanceValue* m_viewModelInstanceValue = nullptr; | |
| }; | |
| } // namespace rive | |
| HitResult StateMachineInstance::updateListeners(Vec2D position, | |
| ListenerType hitType, | |
| int pointerId, | |
| float timeStamp) | |
| { | |
| if (m_artboardInstance->frameOrigin()) | |
| { | |
| position -= Vec2D( | |
| m_artboardInstance->originX() * m_artboardInstance->layoutWidth(), | |
| m_artboardInstance->originY() * m_artboardInstance->layoutHeight()); | |
| } | |
| // First reset all listener groups before processing the events | |
| for (const auto& listenerGroup : m_listenerGroups) | |
| { | |
| listenerGroup.get()->reset(pointerId); | |
| } | |
| // Next prepare the event to set the common hover status for each group | |
| 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, | |
| !hitOpaque, | |
| timeStamp, | |
| pointerId); | |
| if (hitResult != HitResult::none) | |
| { | |
| hitSomething = true; | |
| if (hitResult == HitResult::hitOpaque) | |
| { | |
| hitOpaque = true; | |
| } | |
| } | |
| } | |
| // Finally release events that are complete | |
| if (hitType == ListenerType::exit) | |
| { | |
| for (const auto& listenerGroup : m_listenerGroups) | |
| { | |
| listenerGroup.get()->releaseEvent(pointerId); | |
| } | |
| } | |
| return hitSomething ? hitOpaque ? HitResult::hitOpaque : HitResult::hit | |
| : HitResult::none; | |
| } | |
| bool StateMachineInstance::hitTest(Vec2D position) const | |
| { | |
| if (m_artboardInstance->frameOrigin()) | |
| { | |
| position -= Vec2D( | |
| m_artboardInstance->originX() * m_artboardInstance->layoutWidth(), | |
| m_artboardInstance->originY() * m_artboardInstance->layoutHeight()); | |
| } | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| // TODO: quick reject. | |
| if (hitShape->hitTest(position)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| HitResult StateMachineInstance::pointerMove(Vec2D position, | |
| float timeStamp, | |
| int id) | |
| { | |
| return updateListeners(position, ListenerType::move, id, timeStamp); | |
| } | |
| HitResult StateMachineInstance::pointerDown(Vec2D position, int id) | |
| { | |
| return updateListeners(position, ListenerType::down, id); | |
| } | |
| HitResult StateMachineInstance::pointerUp(Vec2D position, int id) | |
| { | |
| return updateListeners(position, ListenerType::up, id); | |
| } | |
| HitResult StateMachineInstance::pointerExit(Vec2D position, int id) | |
| { | |
| return updateListeners(position, ListenerType::exit, id); | |
| } | |
| HitResult StateMachineInstance::dragStart(Vec2D position, | |
| float timeStamp, | |
| bool disablePointer, | |
| int pointerId) | |
| { | |
| if (disablePointer) | |
| { | |
| disablePointerEvents(pointerId); | |
| } | |
| auto hit = updateListeners(position, ListenerType::dragStart); | |
| return hit; | |
| } | |
| HitResult StateMachineInstance::dragEnd(Vec2D position, | |
| float timeStamp, | |
| int pointerId) | |
| { | |
| enablePointerEvents(pointerId); | |
| auto hit = updateListeners(position, ListenerType::dragEnd); | |
| pointerMove(position, timeStamp, pointerId); | |
| return hit; | |
| } | |
| #ifdef TESTING | |
| const LayerState* StateMachineInstance::layerState(size_t index) | |
| { | |
| if (index < m_machine->layerCount()) | |
| { | |
| return m_layers[index].currentState(); | |
| } | |
| return nullptr; | |
| } | |
| #endif | |
| void StateMachineInstance::addToHitLookup( | |
| Component* target, | |
| bool isLayoutComponent, | |
| std::unordered_map<Component*, HitDrawable*>& hitLookup, | |
| ListenerGroup* listenerGroup, | |
| bool isOpaque) | |
| { | |
| // target could either be a LayoutComponent or a DrawableProxy | |
| if (isLayoutComponent) | |
| { | |
| HitLayout* hitLayout; | |
| auto itr = hitLookup.find(target); | |
| if (itr == hitLookup.end()) | |
| { | |
| auto hs = rivestd::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); | |
| } | |
| hitLayout->addListener(listenerGroup); | |
| 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 = rivestd::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); | |
| } | |
| hitShape->addListener(listenerGroup); | |
| 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 = rivestd::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); | |
| } | |
| hitTextRun->addListener(listenerGroup); | |
| return; | |
| } | |
| if (target->is<ContainerComponent>()) | |
| { | |
| target->as<ContainerComponent>()->forEachChild([&](Component* child) { | |
| addToHitLookup(child, | |
| child->is<LayoutComponent>(), | |
| hitLookup, | |
| listenerGroup, | |
| isOpaque); | |
| return false; | |
| }); | |
| return; | |
| } | |
| } | |
| StateMachineInstance::StateMachineInstance(const StateMachine* machine, | |
| ArtboardInstance* instance) : | |
| Scene(instance), m_machine(machine) | |
| { | |
| 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 | |
| } | |
| m_layerCount = machine->layerCount(); | |
| m_layers = new StateMachineLayerInstance[m_layerCount]; | |
| for (size_t i = 0; i < m_layerCount; i++) | |
| { | |
| m_layers[i].init(this, machine->layer(i), m_artboardInstance); | |
| } | |
| // 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); | |
| auto dataBindClone = static_cast<DataBind*>(dataBind->clone()); | |
| dataBindClone->file(dataBind->file()); | |
| if (dataBind->converter() != nullptr) | |
| { | |
| dataBindClone->converter( | |
| dataBind->converter()->clone()->as<DataConverter>()); | |
| } | |
| addDataBind(dataBindClone); | |
| if (dataBind->target()->is<BindableProperty>()) | |
| { | |
| auto bindableProperty = dataBind->target()->as<BindableProperty>(); | |
| auto bindablePropertyInstance = | |
| m_bindablePropertyInstances.find(bindableProperty); | |
| BindableProperty* bindablePropertyClone; | |
| if (bindablePropertyInstance == m_bindablePropertyInstances.end()) | |
| { | |
| bindablePropertyClone = | |
| bindableProperty->clone()->as<BindableProperty>(); | |
| m_bindablePropertyInstances[bindableProperty] = | |
| bindablePropertyClone; | |
| } | |
| else | |
| { | |
| bindablePropertyClone = bindablePropertyInstance->second; | |
| } | |
| 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) | |
| { | |
| m_bindableDataBindsToSource[bindablePropertyClone] = | |
| dataBindClone; | |
| } | |
| else | |
| { | |
| m_bindableDataBindsToTarget[bindablePropertyClone] = | |
| dataBindClone; | |
| } | |
| } | |
| else | |
| { | |
| dataBindClone->target(dataBind->target()); | |
| } | |
| } | |
| // 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->listenerType() == ListenerType::event) | |
| { | |
| continue; | |
| } | |
| if (listener->listenerType() == ListenerType::viewModel) | |
| { | |
| auto vmListener = new ListenerViewModel(this, listener); | |
| m_listenerViewModels.push_back(vmListener); | |
| continue; | |
| } | |
| auto listenerGroup = rivestd::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)); | |
| } | |
| std::vector<DraggableConstraint*> draggableConstraints; | |
| for (auto core : m_artboardInstance->objects()) | |
| { | |
| if (core == nullptr) | |
| { | |
| continue; | |
| } | |
| if (core->is<DraggableConstraint>()) | |
| { | |
| draggableConstraints.push_back(core->as<DraggableConstraint>()); | |
| } | |
| } | |
| for (auto constraint : draggableConstraints) | |
| { | |
| auto draggables = constraint->draggables(); | |
| for (auto dragProxy : draggables) | |
| { | |
| auto listener = new StateMachineListener(); | |
| listener->listenerTypeValue( | |
| static_cast<uint32_t>(ListenerType::draggableConstraint)); | |
| auto listenerGroup = | |
| rivestd::make_unique<DraggableConstraintListenerGroup>( | |
| listener, | |
| constraint, | |
| dragProxy); | |
| auto hittable = dragProxy->hittable(); | |
| if (hittable != nullptr && hittable->is<Component>()) | |
| { | |
| addToHitLookup(hittable->as<Component>(), | |
| hittable->is<LayoutComponent>() || | |
| hittable->isProxy(), | |
| hitLookup, | |
| listenerGroup.get(), | |
| dragProxy->isOpaque()); | |
| } | |
| m_listenerGroups.push_back(std::move(listenerGroup)); | |
| } | |
| } | |
| 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 = rivestd::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 = rivestd::make_unique<HitComponentList>( | |
| componentList->as<Component>(), | |
| this); | |
| m_hitComponents.push_back(std::move(hc)); | |
| } | |
| sortHitComponents(); | |
| } | |
| StateMachineInstance::~StateMachineInstance() | |
| { | |
| unbind(); | |
| for (auto inst : m_inputInstances) | |
| { | |
| delete inst; | |
| } | |
| for (auto& listenerGroup : m_listenerGroups) | |
| { | |
| listenerGroup.reset(); | |
| } | |
| deleteDataBinds(); | |
| delete[] m_layers; | |
| for (auto pair : m_bindablePropertyInstances) | |
| { | |
| delete pair.second; | |
| pair.second = nullptr; | |
| } | |
| for (auto& listenerViewModel : m_listenerViewModels) | |
| { | |
| delete listenerViewModel; | |
| } | |
| if (m_ownsDataContext) | |
| { | |
| delete m_DataContext; | |
| } | |
| m_bindablePropertyInstances.clear(); | |
| } | |
| void StateMachineInstance::removeEventListeners() | |
| { | |
| if (m_artboardInstance != nullptr) | |
| { | |
| for (auto nestedArtboard : m_artboardInstance->nestedArtboards()) | |
| { | |
| if (nestedArtboard == nullptr) | |
| { | |
| continue; | |
| } | |
| for (auto animation : nestedArtboard->nestedAnimations()) | |
| { | |
| if (animation == nullptr) | |
| { | |
| continue; | |
| } | |
| if (animation->is<NestedStateMachine>()) | |
| { | |
| if (auto notifier = animation->as<NestedStateMachine>() | |
| ->stateMachineInstance()) | |
| { | |
| notifier->removeNestedEventListener(this); | |
| } | |
| } | |
| else if (animation->is<NestedLinearAnimation>()) | |
| { | |
| if (auto notifier = animation->as<NestedLinearAnimation>() | |
| ->animationInstance()) | |
| { | |
| notifier->removeNestedEventListener(this); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| void StateMachineInstance::onDataBindChanged(DataBindChanged callback) | |
| { | |
| for (auto databind : m_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; | |
| } | |
| } | |
| } | |
| bool StateMachineInstance::tryChangeState() | |
| { | |
| updateDataBinds(false); | |
| bool hasChangedState = false; | |
| for (size_t i = 0; i < m_layerCount; i++) | |
| { | |
| if (m_layers[i].updateState()) | |
| { | |
| hasChangedState = true; | |
| } | |
| } | |
| return hasChangedState; | |
| } | |
| void StateMachineInstance::applyEvents() | |
| { | |
| int maxIterations = 100; | |
| int currentIteration = 0; | |
| while ((m_reportedEvents.size() > 0 || | |
| m_reportedListenerViewModels.size() > 0) && | |
| currentIteration++ < maxIterations) | |
| { | |
| m_reportingEvents = m_reportedEvents; | |
| m_reportingListenerViewModels = m_reportedListenerViewModels; | |
| m_reportedEvents.clear(); | |
| m_reportedListenerViewModels.clear(); | |
| this->notifyEventListeners(m_reportingEvents, nullptr); | |
| this->notifyListenerViewModels(m_reportingListenerViewModels); | |
| } | |
| if (currentIteration >= maxIterations) | |
| { | |
| fprintf(stderr, | |
| "%s StateMachine exceeded max event iterations" | |
| "on artboard %s\n", | |
| stateMachine()->name().c_str(), | |
| artboard()->name().c_str()); | |
| } | |
| } | |
| bool StateMachineInstance::advance(float seconds, bool newFrame) | |
| { | |
| if (m_drawOrderChangeCounter != | |
| m_artboardInstance->drawOrderChangeCounter()) | |
| { | |
| m_drawOrderChangeCounter = m_artboardInstance->drawOrderChangeCounter(); | |
| sortHitComponents(); | |
| } | |
| if (newFrame) | |
| { | |
| applyEvents(); | |
| m_needsAdvance = false; | |
| } | |
| updateDataBinds(false); | |
| for (size_t i = 0; i < m_layerCount; i++) | |
| { | |
| if (m_layers[i].advance(seconds, newFrame)) | |
| { | |
| m_needsAdvance = true; | |
| } | |
| } | |
| if (advanceDataBinds(seconds)) | |
| { | |
| m_needsAdvance = true; | |
| } | |
| for (auto inst : m_inputInstances) | |
| { | |
| inst->advanced(); | |
| } | |
| return m_needsAdvance || !m_reportedEvents.empty() || | |
| !m_reportedListenerViewModels.empty(); | |
| } | |
| void StateMachineInstance::advancedDataContext() | |
| { | |
| if (m_DataContext != nullptr) | |
| { | |
| m_DataContext->advanced(); | |
| } | |
| } | |
| void StateMachineInstance::reset() | |
| { | |
| advancedDataContext(); | |
| m_artboardInstance->reset(); | |
| } | |
| bool StateMachineInstance::advanceAndApply(float seconds) | |
| { | |
| RIVE_PROF_SCOPE() | |
| // Advancing by 0 could return false, when it shouldn't. Force keepGoing | |
| // to true. | |
| bool keepGoing = this->advance(seconds, true) || seconds == 0.0f; | |
| 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; | |
| } | |
| // 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; | |
| } | |
| reset(); | |
| if (!m_artboardInstance->hasDirt(ComponentDirt::Components)) | |
| { | |
| break; | |
| } | |
| } | |
| return keepGoing || !m_reportedEvents.empty() || | |
| !m_reportedListenerViewModels.empty(); | |
| } | |
| void StateMachineInstance::markNeedsAdvance() { m_needsAdvance = true; } | |
| bool StateMachineInstance::needsAdvance() const { return m_needsAdvance; } | |
| void StateMachineInstance::resetState() | |
| { | |
| for (size_t i = 0; i < m_layerCount; i++) | |
| { | |
| m_layers[i].resetState(); | |
| } | |
| } | |
| 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::bindViewModelInstance( | |
| rcp<ViewModelInstance> viewModelInstance) | |
| { | |
| clearDataContext(); | |
| m_ownsDataContext = true; | |
| auto dataContext = new DataContext(viewModelInstance); | |
| m_artboardInstance->clearDataContext(); | |
| m_artboardInstance->internalDataContext(dataContext); | |
| internalDataContext(dataContext); | |
| } | |
| void StateMachineInstance::dataContext(DataContext* dataContext) | |
| { | |
| clearDataContext(); | |
| internalDataContext(dataContext); | |
| } | |
| void StateMachineInstance::internalDataContext(DataContext* dataContext) | |
| { | |
| m_DataContext = dataContext; | |
| bindDataBindsFromContext(dataContext); | |
| for (auto listenerViewModel : m_listenerViewModels) | |
| { | |
| listenerViewModel->bindFromContext(dataContext); | |
| } | |
| } | |
| void StateMachineInstance::clearDataContext() | |
| { | |
| if (m_ownsDataContext && m_DataContext != nullptr) | |
| { | |
| delete m_DataContext; | |
| m_DataContext = nullptr; | |
| } | |
| for (auto& listenerViewModel : m_listenerViewModels) | |
| { | |
| listenerViewModel->clearDataContext(); | |
| } | |
| m_ownsDataContext = false; | |
| } | |
| 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; | |
| } | |
| void StateMachineInstance::reportEvent(Event* event, float delaySeconds) | |
| { | |
| m_reportedEvents.push_back(EventReport(event, delaySeconds)); | |
| } | |
| void StateMachineInstance::reportListenerViewModel( | |
| ListenerViewModel* listenerViewModel) | |
| { | |
| m_reportedListenerViewModels.push_back(listenerViewModel); | |
| } | |
| std::size_t StateMachineInstance::reportedEventCount() const | |
| { | |
| return m_reportedEvents.size(); | |
| } | |
| const EventReport StateMachineInstance::reportedEventAt(std::size_t index) const | |
| { | |
| if (index >= m_reportedEvents.size()) | |
| { | |
| return EventReport(nullptr, 0.0f); | |
| } | |
| return m_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, | |
| Vec2D(), | |
| Vec2D()); | |
| } | |
| } | |
| } | |
| 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->listenerType() == 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; | |
| } | |
| } | |
| auto listenerEvent = | |
| sourceArtboard->resolve(listener->eventId()); | |
| if (listenerEvent == event.event()) | |
| { | |
| listener->performChanges(this, Vec2D(), Vec2D()); | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| // 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) | |
| { | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| hitShape->enablePointerEvents(pointerId); | |
| } | |
| } | |
| void StateMachineInstance::disablePointerEvents(int pointerId) | |
| { | |
| for (const auto& hitShape : m_hitComponents) | |
| { | |
| hitShape->disablePointerEvents(pointerId); | |
| } | |
| } | |
| BindableProperty* StateMachineInstance::bindablePropertyInstance( | |
| BindableProperty* bindableProperty) const | |
| { | |
| auto bindablePropertyInstance = | |
| m_bindablePropertyInstances.find(bindableProperty); | |
| if (bindablePropertyInstance == m_bindablePropertyInstances.end()) | |
| { | |
| return nullptr; | |
| } | |
| return bindablePropertyInstance->second; | |
| } | |
| DataBind* StateMachineInstance::bindableDataBindToSource( | |
| BindableProperty* bindableProperty) const | |
| { | |
| auto dataBind = m_bindableDataBindsToSource.find(bindableProperty); | |
| if (dataBind == m_bindableDataBindsToSource.end()) | |
| { | |
| return nullptr; | |
| } | |
| return dataBind->second; | |
| } | |
| DataBind* StateMachineInstance::bindableDataBindToTarget( | |
| BindableProperty* bindableProperty) const | |
| { | |
| auto dataBind = m_bindableDataBindsToTarget.find(bindableProperty); | |
| if (dataBind == m_bindableDataBindsToTarget.end()) | |
| { | |
| return nullptr; | |
| } | |
| return dataBind->second; | |
| } |