blob: 25d00d2be1c9a8d0b88b3ee2557808c59d796f3b [file] [log] [blame] [edit]
#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/constraints/draggable_constraint.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/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 <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());
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);
}
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, "StateMachine exceeded max iterations.\n");
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<StateMachineFireEvent*>& 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 ((double)rand() / (RAND_MAX)); }
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;
float currentWeight = 0;
size_t index = 0;
StateTransition* transition;
while (index < stateFrom->transitionCount())
{
transition = stateFrom->transition(index);
auto transitionWeight = 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 ListenerGroup
{
public:
ListenerGroup(const StateMachineListener* listener) : m_listener(listener)
{}
virtual ~ListenerGroup() {}
void consume() { m_isConsumed = true; }
//
void hover() { m_isHovered = true; }
void unhover() { m_isHovered = false; }
void reset()
{
m_isConsumed = false;
m_prevIsHovered = m_isHovered;
m_isHovered = false;
if (m_clickPhase == GestureClickPhase::clicked)
{
m_clickPhase = GestureClickPhase::out;
}
}
bool isConsumed() { return m_isConsumed; }
bool isHovered() { return m_isHovered; }
bool prevHovered() { return m_prevIsHovered; }
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;
}
// Vec2D position, ListenerType hitType, bool canHit
virtual ProcessEventResult processEvent(
Component* component,
Vec2D position,
ListenerType hitEvent,
bool canHit,
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.
if (!canHit && isHovered())
{
unhover();
}
bool isGroupHovered = canHit ? isHovered() : false;
bool hoverChange = prevHovered() != 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.
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)
{
clickPhase(GestureClickPhase::down);
}
else if (hitEvent == ListenerType::up &&
clickPhase() == GestureClickPhase::down)
{
clickPhase(GestureClickPhase::clicked);
}
}
else
{
if (hitEvent == ListenerType::down || hitEvent == ListenerType::up)
{
clickPhase(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,
previousPosition);
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 ((clickPhase() == GestureClickPhase::clicked &&
_listener->listenerType() == ListenerType::click) ||
(isGroupHovered && hitEvent == _listener->listenerType()))
{
_listener->performChanges(stateMachineInstance,
position,
previousPosition);
stateMachineInstance->markNeedsAdvance();
consume();
}
previousPosition.x = position.x;
previousPosition.y = position.y;
return ProcessEventResult::pointer;
}
void clickPhase(GestureClickPhase value) { m_clickPhase = value; }
GestureClickPhase clickPhase() { return m_clickPhase; }
const StateMachineListener* listener() const { return m_listener; };
// A vector storing the previous position for this specific listener gorup
Vec2D previousPosition;
private:
// Consumed listeners aren't processed again in the current frame
bool m_isConsumed = false;
// This variable holds the hover status of the the listener itself so it can
// be shared between all shapes that target it
bool m_isHovered = false;
// Variable storing the previous hovered state to check for hover changes
bool m_prevIsHovered = false;
// A click gesture is composed of three phases and is shared between all
// shapes
GestureClickPhase m_clickPhase = GestureClickPhase::out;
const StateMachineListener* m_listener;
};
class DraggableConstraintListenerGroup : public ListenerGroup
{
public:
DraggableConstraintListenerGroup(const StateMachineListener* listener,
DraggableConstraint* constraint,
DraggableProxy* draggable) :
ListenerGroup(listener),
m_constraint(constraint),
m_draggable(draggable)
{}
virtual ~DraggableConstraintListenerGroup() {}
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; }
// Vec2D position, ListenerType hitType, bool canHit
ProcessEventResult processEvent(
Component* component,
Vec2D position,
ListenerType hitEvent,
bool canHit,
StateMachineInstance* stateMachineInstance) override
{
auto prevPhase = clickPhase();
ListenerGroup::processEvent(component,
position,
hitEvent,
canHit,
stateMachineInstance);
if (prevPhase == GestureClickPhase::down &&
(clickPhase() == GestureClickPhase::clicked ||
clickPhase() == GestureClickPhase::out))
{
m_draggable->endDrag(position);
if (hasScrolled)
{
return ProcessEventResult::scroll;
hasScrolled = false;
}
}
else if (prevPhase != GestureClickPhase::down &&
clickPhase() == GestureClickPhase::down)
{
m_draggable->startDrag(position);
hasScrolled = false;
}
else if (hitEvent == ListenerType::move &&
clickPhase() == GestureClickPhase::down)
{
m_draggable->drag(position);
hasScrolled = true;
return ProcessEventResult::scroll;
}
return ProcessEventResult::none;
}
private:
DraggableConstraint* m_constraint;
DraggableProxy* m_draggable;
bool 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;
virtual bool hitTestHelper(Vec2D position) const { return false; }
bool hitTest(Vec2D position) const override
{
return hitTestHelper(position);
}
virtual bool testBounds(Component* component,
Vec2D position,
bool skipOnUnclipped) const
{
if (component == nullptr)
{
return true;
}
if (component->is<Drawable>())
{
component = component->as<Drawable>()->hittableComponent();
if (component == nullptr)
{
return true;
}
}
if (component->is<LayoutComponent>())
{
Mat2D inverseWorld;
auto layout = component->as<LayoutComponent>();
if (layout->worldTransform().invert(&inverseWorld))
{
// If the layout is not clipped and skipOnUnclipped is true, we
// don't care about whether it contains the position
auto canSkip = skipOnUnclipped && !layout->clip();
if (!canSkip)
{
auto localWorld = inverseWorld * position;
if (layout->is<Artboard>())
{
auto artboard = layout->as<Artboard>();
if (artboard->originX() != 0 ||
artboard->originY() != 0)
{
localWorld += Vec2D(
artboard->originX() * artboard->layoutWidth(),
artboard->originY() * artboard->layoutHeight());
}
}
if (!layout->localBounds().contains(localWorld))
{
return false;
}
}
return testBounds(layout->parent(), position, true);
}
return false;
}
// Keep going
return testBounds(component->parent(), position, skipOnUnclipped);
}
void prepareEvent(Vec2D position, ListenerType hitType) override
{
if (canEarlyOut &&
(hitType != ListenerType::down || !hasDownListener) &&
(hitType != ListenerType::up || !hasUpListener))
{
#ifdef TESTING
earlyOutCount++;
#endif
return;
}
isHovered = hitTest(position);
// // iterate all listeners associated with this hit shape
if (isHovered)
{
for (auto listenerGroup : listeners)
{
listenerGroup->hover();
}
}
}
HitResult processEvent(Vec2D position,
ListenerType hitType,
bool canHit) 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,
hitType,
canHit,
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);
}
};
/// Representation of a HitDrawable with a Hittable component
class HitExpandable : public HitDrawable
{
public:
bool testBounds(Component* component,
Vec2D position,
bool skipOnUnclipped) const override
{
Hittable* hittable = component ? Hittable::from(component) : nullptr;
if (hittable != nullptr)
{
if (hittable->hitTestAABB(position) &&
HitDrawable::testBounds(component->parent(), position, true))
{
return hittable->hitTestHiFi(position, hitRadius);
}
return false;
}
return HitDrawable::testBounds(component, position, true);
}
HitExpandable(Drawable* drawable,
Component* component,
StateMachineInstance* stateMachineInstance,
bool isOpaque = false) :
HitDrawable(drawable, component, stateMachineInstance, isOpaque)
{}
bool hitTestHelper(Vec2D position) const override
{
return testBounds(m_component, position, true);
}
};
// Wrapper around HitExpandable to garbage collect text run contours.
class HitTextRun : public HitExpandable
{
public:
TextValueRun* m_textValueRun;
HitTextRun(Drawable* drawable,
TextValueRun* component,
StateMachineInstance* stateMachineInstance,
bool isOpaque = false) :
HitExpandable(drawable, component, stateMachineInstance, isOpaque)
{
m_textValueRun = component;
if (m_textValueRun)
{
m_textValueRun->isHitTarget(true);
}
}
~HitTextRun()
{
if (m_textValueRun != nullptr)
{
m_textValueRun->isHitTarget(false);
m_textValueRun->resetHitTest();
}
}
};
class HitLayout : public HitDrawable
{
public:
HitLayout(Drawable* layout,
StateMachineInstance* stateMachineInstance,
bool isOpaque = false) :
HitDrawable(layout, layout, stateMachineInstance, isOpaque)
{}
bool hitTestHelper(Vec2D position) const override
{
return testBounds(m_component, position, false);
}
};
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) 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);
break;
case ListenerType::up:
hitResult =
nestedStateMachine->pointerUp(nestedPosition);
break;
case ListenerType::move:
hitResult =
nestedStateMachine->pointerMove(nestedPosition);
break;
case ListenerType::enter:
case ListenerType::exit:
case ListenerType::event:
case ListenerType::click:
case ListenerType::draggableConstraint:
break;
}
}
else
{
switch (hitType)
{
case ListenerType::down:
case ListenerType::up:
case ListenerType::move:
nestedStateMachine->pointerExit(nestedPosition);
break;
case ListenerType::enter:
case ListenerType::exit:
case ListenerType::event:
case ListenerType::click:
case ListenerType::draggableConstraint:
break;
}
}
}
}
return hitResult;
}
void prepareEvent(Vec2D position, ListenerType hitType) override {}
};
} // namespace rive
HitResult StateMachineInstance::updateListeners(Vec2D position,
ListenerType hitType)
{
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();
}
// Next prepare the event to set the common hover status for each group
for (const auto& hitShape : m_hitComponents)
{
hitShape->prepareEvent(position, hitType);
}
bool hitSomething = false;
bool hitOpaque = false;
// Finally process the events
for (const auto& hitShape : m_hitComponents)
{
// TODO: quick reject.
HitResult hitResult =
hitShape->processEvent(position, hitType, !hitOpaque);
if (hitResult != HitResult::none)
{
hitSomething = true;
if (hitResult == HitResult::hitOpaque)
{
hitOpaque = true;
}
}
}
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)
{
return updateListeners(position, ListenerType::move);
}
HitResult StateMachineInstance::pointerDown(Vec2D position)
{
return updateListeners(position, ListenerType::down);
}
HitResult StateMachineInstance::pointerUp(Vec2D position)
{
return updateListeners(position, ListenerType::up);
}
HitResult StateMachineInstance::pointerExit(Vec2D position)
{
return updateListeners(position, ListenerType::exit);
}
#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());
if (dataBind->converter() != nullptr)
{
dataBindClone->converter(
dataBind->converter()->clone()->as<DataConverter>());
}
m_dataBinds.push_back(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;
}
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())
{
if (nestedArtboard->hasNestedStateMachines())
{
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);
}
}
}
}
sortHitComponents();
}
StateMachineInstance::~StateMachineInstance()
{
for (auto inst : m_inputInstances)
{
delete inst;
}
for (auto databind : m_dataBinds)
{
delete databind;
}
delete[] m_layers;
for (auto pair : m_bindablePropertyInstances)
{
delete pair.second;
pair.second = nullptr;
}
if (m_ownsDataContext)
{
delete m_DataContext;
}
m_bindablePropertyInstances.clear();
}
#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;
}
}
}
void StateMachineInstance::updateDataBinds()
{
for (auto dataBind : m_dataBinds)
{
auto d = dataBind->dirt();
if (d != ComponentDirt::None)
{
dataBind->dirt(ComponentDirt::None);
dataBind->update(d);
}
}
}
bool StateMachineInstance::tryChangeState()
{
bool hasChangedState = false;
for (size_t i = 0; i < m_layerCount; i++)
{
if (m_layers[i].updateState())
{
hasChangedState = true;
}
}
return hasChangedState;
}
bool StateMachineInstance::advance(float seconds, bool newFrame)
{
updateDataBinds();
if (m_drawOrderChangeCounter !=
m_artboardInstance->drawOrderChangeCounter())
{
m_drawOrderChangeCounter = m_artboardInstance->drawOrderChangeCounter();
sortHitComponents();
}
if (newFrame)
{
this->notifyEventListeners(m_reportedEvents, nullptr);
m_reportedEvents.clear();
m_needsAdvance = false;
}
for (size_t i = 0; i < m_layerCount; i++)
{
if (m_layers[i].advance(seconds, newFrame))
{
m_needsAdvance = true;
}
}
for (auto& dataBind : m_dataBinds)
{
if (dataBind->advance(seconds))
{
m_needsAdvance = true;
}
}
for (auto inst : m_inputInstances)
{
inst->advanced();
}
return m_needsAdvance || !m_reportedEvents.empty();
}
void StateMachineInstance::advancedDataContext()
{
if (m_DataContext != nullptr)
{
m_DataContext->advanced();
}
}
bool StateMachineInstance::advanceAndApply(float seconds)
{
bool keepGoing = this->advance(seconds, true);
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;
}
advancedDataContext();
if (!m_artboardInstance->hasDirt(ComponentDirt::Components))
{
break;
}
}
return keepGoing || !m_reportedEvents.empty();
}
void StateMachineInstance::markNeedsAdvance() { m_needsAdvance = true; }
bool StateMachineInstance::needsAdvance() const { return m_needsAdvance; }
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, true);
internalDataContext(dataContext);
}
void StateMachineInstance::dataContext(DataContext* dataContext)
{
clearDataContext();
internalDataContext(dataContext);
}
void StateMachineInstance::internalDataContext(DataContext* dataContext)
{
m_DataContext = dataContext;
for (auto dataBind : m_dataBinds)
{
if (dataBind->is<DataBindContext>())
{
dataBind->as<DataBindContext>()->bindFromContext(dataContext);
}
}
}
void StateMachineInstance::clearDataContext()
{
if (m_ownsDataContext && m_DataContext != nullptr)
{
delete m_DataContext;
m_DataContext = nullptr;
}
m_ownsDataContext = false;
}
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));
}
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);
}
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();
// 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 &&
sourceArtboard->resolve(listener->targetId()) !=
artboard())
{
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();
}
}
}
}
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;
}