#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/listener_action.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_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/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 #include 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( 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& 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(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(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()) { auto instance = static_cast(m_stateFrom) ->animationInstance(); advanceTime = instance->spilledTime(); } } m_currentState->advance(advanceTime, 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()) { return nullptr; } return static_cast(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 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; }; /// 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 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(); if (nestedArtboard->isCollapsed() || nestedArtboard->isPaused()) { 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()) { auto nestedStateMachine = nestedAnimation->as(); 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(); HitResult hitResult = HitResult::none; if (nestedArtboard->isCollapsed() || nestedArtboard->isPaused()) { 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()) { auto nestedStateMachine = nestedAnimation->as(); 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::componentProvided: case ListenerType::textInput: case ListenerType::viewModel: case ListenerType::drag: 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::componentProvided: case ListenerType::textInput: case ListenerType::viewModel: case ListenerType::drag: 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(); 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(); 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::componentProvided: case ListenerType::textInput: case ListenerType::viewModel: case ListenerType::drag: 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::componentProvided: case ListenerType::textInput: case ListenerType::viewModel: case ListenerType::drag: 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 vmProp = dataContext->getViewModelProperty(m_listener->dataBindPath()); if (vmProp != nullptr) { m_viewModelInstanceValue = rive::ref_rcp(vmProp); vmProp->addDependent(this); } } void addDirt(ComponentDirt value, bool recurse) { if (m_viewModelInstanceValue) { if (!m_viewModelInstanceValue->is() || m_viewModelInstanceValue->as() ->propertyValue() != 0) { m_stateMachineInstance->reportListenerViewModel(this); } } } const StateMachineListener* listener() { return m_listener; } private: StateMachineInstance* m_stateMachineInstance = nullptr; const StateMachineListener* m_listener = nullptr; rive::rcp 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& 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(target->as(), this, isOpaque); hitLookup[target] = hitLayout = hs.get(); m_hitComponents.push_back(std::move(hs)); } else { hitLayout = static_cast(itr->second); } hitLayout->addListener(listenerGroup); if (isOpaque) { hitLayout->isOpaque = true; } return; } if (target->is()) { HitExpandable* hitShape; auto itr = hitLookup.find(target); if (itr == hitLookup.end()) { Shape* shape = target->as(); shape->addFlags(PathFlags::neverDeferUpdate); shape->addDirt(ComponentDirt::Path, true); auto hs = rivestd::make_unique(shape, shape, this); hitLookup[target] = hitShape = hs.get(); m_hitComponents.push_back(std::move(hs)); } else { hitShape = static_cast(itr->second); } hitShape->addListener(listenerGroup); return; } if (target->is()) { HitTextRun* hitTextRun; auto itr = hitLookup.find(target); if (itr == hitLookup.end()) { TextValueRun* run = target->as(); run->textComponent()->addDirt(ComponentDirt::Path, true); auto hs = rivestd::make_unique(run->textComponent(), run, this); hitLookup[target] = hitTextRun = hs.get(); m_hitComponents.push_back(std::move(hs)); } else { hitTextRun = static_cast(itr->second); } hitTextRun->addListener(listenerGroup); return; } if (target->is()) { target->as()->forEachChild([&](Component* child) { addToHitLookup(child, child->is(), 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(), this); break; case StateMachineNumber::typeKey: m_inputInstances[i] = new SMINumber(input->as(), this); break; case StateMachineTrigger::typeKey: m_inputInstances[i] = new SMITrigger(input->as(), 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->clone()); dataBindClone->file(dataBind->file()); if (dataBind->converter() != nullptr) { dataBindClone->converter( dataBind->converter()->clone()->as()); } addDataBind(dataBindClone); if (dataBind->target()->is()) { auto bindableProperty = dataBind->target()->as(); auto bindablePropertyInstance = m_bindablePropertyInstances.find(bindableProperty); BindableProperty* bindablePropertyClone; if (bindablePropertyInstance == m_bindablePropertyInstances.end()) { bindablePropertyClone = bindableProperty->clone()->as(); 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(dataBindClone->flags()) & DataBindFlags::ToSource) == 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 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(listener); auto target = m_artboardInstance->resolve(listener->targetId()); if (target != nullptr && target->is()) { bool isLayoutComponent = false; if (target->is()) { isLayoutComponent = true; target = target->as()->proxy(); } addToHitLookup(target->as(), isLayoutComponent, hitLookup, listenerGroup.get(), false); } m_listenerGroups.push_back(std::move(listenerGroup)); } std::vector componentProvidedListenerGroups; for (auto core : m_artboardInstance->objects()) { 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() || (component->is() && component->as()->isProxy()); addToHitLookup(target->component(), isLayoutComponent, hitLookup, group, target->isOpaque()); } m_listenerGroups.push_back(std::unique_ptr(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)); } } 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( nestedArtboard->as(), this); m_hitComponents.push_back(std::move(hn)); for (auto animation : nestedArtboard->nestedAnimations()) { if (animation->is()) { if (auto notifier = animation->as() ->stateMachineInstance()) { notifier->setNestedArtboard(nestedArtboard); notifier->addNestedEventListener(this); } } else if (animation->is()) { if (auto notifier = animation->as() ->animationInstance()) { notifier->setNestedArtboard(nestedArtboard); notifier->addNestedEventListener(this); } } } } for (auto componentList : instance->artboardComponentLists()) { auto hc = rivestd::make_unique( componentList->as(), this); m_hitComponents.push_back(std::move(hc)); } // Initialize local instances of ScriptedListenerActions for (std::size_t i = 0; i < machine->listenerCount(); i++) { auto listener = machine->listener(i); for (std::size_t j = 0; j < listener->actionCount(); j++) { auto action = listener->action(j); if (action->is()) { auto scriptedListenerAction = action->as(); auto scriptedListenerActionClone = static_cast( scriptedListenerAction->clone()); scriptedListenerActionClone->reinit(); m_scriptedListenerActionsMap[scriptedListenerAction] = scriptedListenerActionClone; } } } sortHitComponents(); } ScriptedObject* StateMachineInstance::scriptedObject( const ScriptedObject* source) { auto itr = m_scriptedListenerActionsMap.find(source); if (itr != m_scriptedListenerActionsMap.end()) { return itr->second; } return nullptr; } 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; } m_bindablePropertyInstances.clear(); for (auto& pair : m_scriptedListenerActionsMap) { delete pair.second; pair.second = nullptr; } m_scriptedListenerActionsMap.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()) { if (auto notifier = animation->as() ->stateMachineInstance()) { notifier->removeNestedEventListener(this); } } else if (animation->is()) { if (auto notifier = animation->as() ->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()) { 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) { updateDataBinds(false); 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 InstType* StateMachineInstance::getNamedInput(const std::string& name) const { for (const auto inst : m_inputInstances) { auto input = inst->input(); if (input->is() && input->name() == name) { return static_cast(inst); } } return nullptr; } SMIBool* StateMachineInstance::getBool(const std::string& name) const { return getNamedInput(name); } SMINumber* StateMachineInstance::getNumber(const std::string& name) const { return getNamedInput(name); } SMITrigger* StateMachineInstance::getTrigger(const std::string& name) const { return getNamedInput(name); } void StateMachineInstance::bindViewModelInstance( rcp viewModelInstance) { clearDataContext(); m_ownsDataContext = true; auto dataContext = new DataContext(viewModelInstance); viewModelInstance->addDependent(this); 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); } for (auto& scriptedObjectItr : m_scriptedListenerActionsMap) { scriptedObjectItr.second->dataContext(dataContext); } } void StateMachineInstance::rebind() { m_artboardInstance->clearDataContext(); m_artboardInstance->internalDataContext(m_DataContext); internalDataContext(m_DataContext); }; void StateMachineInstance::clearDataContext() { if (m_DataContext) { if (m_ownsDataContext) { if (m_DataContext->viewModelInstance()) { m_DataContext->viewModelInstance()->removeDependent(this); } 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& events, NestedArtboard* context) { notifyEventListeners(events, context); updateDataBinds(false); } void StateMachineInstance::notifyListenerViewModels( const std::vector& events) { if (events.size() > 0) { for (auto& listenerViewModel : events) { listenerViewModel->listener()->performChanges(this, Vec2D(), Vec2D(), 0); } } } void StateMachineInstance::notifyEventListeners( const std::vector& 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()) { continue; } } auto listenerEvent = sourceArtboard->resolve(listener->eventId()); if (listenerEvent == event.event()) { listener->performChanges(this, Vec2D(), Vec2D(), 0); 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()) { event->as()->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; } bool StateMachineInstance::keyInput(Key value, KeyModifiers modifiers, bool isPressed, bool isRepeat) { // For now just find a text input. auto textInput = m_artboardInstance->objects().first(); if (textInput != nullptr) { return textInput->keyInput(value, modifiers, isPressed, isRepeat); } return false; } bool StateMachineInstance::textInput(const std::string& text) { auto textInput = m_artboardInstance->objects().first(); if (textInput != nullptr) { return textInput->textInput(text); } return false; }