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