blob: 420839170dfb5af412ed124976bb6defb4afad24 [file]
#ifndef _RIVE_STATE_MACHINE_INSTANCE_HPP_
#define _RIVE_STATE_MACHINE_INSTANCE_HPP_
#include <string>
#include <stddef.h>
#include <vector>
#include <unordered_map>
#include "rive/advancing_component.hpp"
#include "rive/animation/gamepad_listener_group.hpp"
#include "rive/animation/keyboard_listener_group.hpp"
#include "rive/animation/semantic_listener_group.hpp"
#include "rive/animation/linear_animation_instance.hpp"
#include "rive/animation/state_instance.hpp"
#include "rive/animation/state_transition.hpp"
#include "rive/core/field_types/core_callback_type.hpp"
#include "rive/hit_result.hpp"
#include "rive/listener_type.hpp"
#include "rive/nested_animation.hpp"
#include "rive/scene.hpp"
#include "rive/data_bind/bindable_property_number.hpp"
#include "rive/data_bind/data_bind_container.hpp"
#include "rive/input/focusable.hpp"
#include "rive/input/focus_manager.hpp"
#include "rive/semantic/semantic_manager.hpp"
#include "rive/sidecar.hpp"
namespace rive
{
class FocusData;
class FocusListenerGroup;
class StateMachine;
class LayerState;
class SMIInput;
class ArtboardInstance;
class SMIBool;
class SMINumber;
class SMITrigger;
class Shape;
class StateMachineLayerInstance;
class HitComponent;
class HitShape;
class ListenerGroup;
class NestedArtboard;
class NestedEventListener;
class NestedEventNotifier;
class Event;
class KeyedProperty;
class EventReport;
class DataBind;
class BindableProperty;
class StateInstance;
class HitDrawable;
class DraggableProxy;
class ListenerViewModel;
class ScriptedListenerAction;
class ScriptedDrawable;
// Cold state clusters, defined in state_machine_instance_clusters.hpp. Each is
// reached through a Sidecar below and stays unallocated until the feature it
// covers is actually used by the file. Left as forward declarations here so the
// cluster header's includes stay out of this one.
struct SMIReporting;
struct SMIBindables;
struct SMIInputExtras;
struct SMIScripting;
typedef void (*DataBindChanged)();
#ifdef WITH_RIVE_TOOLS
class StateMachineInstance;
typedef void (*InputChanged)(StateMachineInstance*, uint64_t);
#endif
class StateMachineInstance : public Scene,
public NestedEventNotifier,
public NestedEventListener,
public DataBindContainer
{
friend class SMIInput;
friend class KeyedProperty;
friend class HitComponent;
friend class StateMachineLayerInstance;
private:
/// Provide a hitListener if you want to process a down or an up for the
/// pointer position too.
HitResult updateListeners(Vec2D position,
ListenerType hitListener,
int pointerId = 0,
float timeStamp = 0,
PointerButton button = PointerButton::primary);
template <typename SMType, typename InstType>
InstType* getNamedInput(const std::string& name) const;
void notifyEventListeners(const std::vector<EventReport>& events,
NestedArtboard* source);
void sortHitComponents();
/// Maps a host-space position into artboard content space. False when a
/// degenerate self transform has collapsed the contents.
bool mapToContentSpace(Vec2D& position, Vec2D* delta = nullptr) const;
double randomValue();
StateTransition* findRandomTransition(
StateInstance* stateFromInstance,
StateMachineLayerInstance* layerInstance);
StateTransition* findAllowedTransition(
StateInstance* stateFromInstance,
StateMachineLayerInstance* layerInstance);
// Ensures the current data context holds an instance for the artboard's
// main view model and for every global view model in the file, creating
// (completing) any that are missing before the context is applied.
void completeViewModelInstances();
void addToHitLookup(Component* target,
bool isLayoutComponent,
std::unordered_map<Component*, HitDrawable*>& hitLookup,
ListenerGroup* listenerGroup,
bool isOpaque);
public:
StateMachineInstance(const StateMachine* machine,
ArtboardInstance* instance);
StateMachineInstance(StateMachineInstance const&) = delete;
~StateMachineInstance() override;
void markNeedsAdvance();
// Advance the state machine by the specified time. Returns true if the
// state machine will continue to animate after this advance.
bool advance(float seconds, bool newFrame);
bool advance(float seconds) { return advance(seconds, true); }
// Returns true when the StateMachineInstance has more data to process.
bool needsAdvance() const;
void resetState();
// Returns a pointer to the instance's stateMachine
const StateMachine* stateMachine() const { return m_machine; }
size_t inputCount() const override { return m_inputInstances.size(); }
SMIInput* input(size_t index) const override;
SMIBool* getBool(const std::string& name) const override;
SMINumber* getNumber(const std::string& name) const override;
SMITrigger* getTrigger(const std::string& name) const override;
void bindViewModelInstance(
rcp<ViewModelInstance> viewModelInstance) override;
// Sets the main (non-global) view model instance in the data context
// without rebinding. Call bind() to apply. A global instance is routed to
// setGlobalViewModelInstance.
void setViewModelInstance(rcp<ViewModelInstance> viewModelInstance);
// Sets/replaces the global view model instance bound under the given global
// view model name without rebinding, preserving the main instance and the
// other globals' order. Returns false if the name does not match the
// instance's global view model. Call bind() to apply.
bool setGlobalViewModelInstance(const std::string& name,
rcp<ViewModelInstance> viewModelInstance);
// Applies the current data context: rebinds artboard + state machine data
// binds in a single pass. No-op if nothing has been set.
void bind();
// @returns the global view model instance currently bound under the given
// name, or nullptr if none has been set. Never creates.
rcp<ViewModelInstance> globalViewModelInstance(const std::string& name);
void bindDataContext(rcp<DataContext> dataContext);
void inheritDataContext(rcp<DataContext> dataContext);
void dataContext(rcp<DataContext> dataContext);
rcp<DataContext> dataContext() const { return dataBindContext(); }
void rebind() override;
size_t currentAnimationCount() const;
const LinearAnimationInstance* currentAnimationByIndex(size_t index) const;
// The number of state changes that occurred across all layers on the
// previous advance.
std::size_t stateChangedCount() const;
// Returns the state name for states that changed in layers on the
// previously called advance. If the index of out of range, it returns
// the empty string.
const LayerState* stateChangedByIndex(size_t index) const;
bool advanceAndApply(float secs) override;
// When advanceViewModels is false, skips consuming/advancing the bound and
// detached view model instances (used when a script drives a nested
// scripted artboard, whose view models are advanced by the host frame
// instead). Animation and artboard component reset still run.
bool advanceAndApply(float secs, bool advanceViewModels);
void advancedDataContext();
void reset();
std::string name() const override;
HitResult pointerMove(Vec2D position,
float timeStamp = 0,
int pointerId = 0) override;
HitResult pointerDown(
Vec2D position,
int pointerId = 0,
PointerButton button = PointerButton::primary) override;
HitResult pointerUp(Vec2D position,
int pointerId = 0,
PointerButton button = PointerButton::primary) override;
HitResult pointerExit(Vec2D position, int pointerId = 0) override;
HitResult dragStart(Vec2D position,
float timeStamp = 0,
bool disablePointer = true,
int pointerId = 0,
PointerButton button = PointerButton::primary);
HitResult dragEnd(Vec2D position,
float timeStamp = 0,
int pointerId = 0,
PointerButton button = PointerButton::primary);
HitResult pointerScroll(Vec2D position,
const ScrollEvent& event,
float timeStamp = 0,
int pointerId = 0) override;
/// Whether anything under position would move for this scroll event. Lets
/// a caller chain outward without committing the event first.
bool wantsScroll(Vec2D position, const ScrollEvent& event);
/// Whether a scroll gesture is latched here or in a nested artboard.
bool hasScrollLatch();
/// Whether an interactive scroll view sits under position at all, even
/// one that cannot move. Keeps a gesture from escaping to the host.
bool hasScrollTargetAt(Vec2D position);
/// Whether an opaque hit that cannot scroll sits under position. It
/// blocks scroll to whatever lies behind it, as hitOpaque does for
/// pointer events.
bool scrollOccludedAt(Vec2D position);
/// Ends any scroll gesture in flight here, latch included.
void cancelScroll();
/// Whether a listener under position responds to presses of button,
/// looking through nested artboards. Lets a host keep a mouse button for
/// its own use wherever nothing in the scene wants it.
bool listensToButtonAt(Vec2D position, PointerButton button);
bool tryChangeState();
bool hitTest(Vec2D position) const;
/// hitTest, except that a scripted layout counts only inside its own
/// box. Scripts are handed every pointer event and bounds-check
/// themselves, so hitTest has to say yes everywhere once one listens;
/// a host deciding whether a press is the scene's at all (or the
/// window's, say) wants the box. A scripted drawable with no box still
/// counts everywhere.
bool hitTestBounded(Vec2D position) const;
private:
bool hitTestInternal(Vec2D position, bool bounded) const;
public:
float durationSeconds() const override { return -1; }
Loop loop() const override { return Loop::oneShot; }
bool isTranslucent() const override { return true; }
/// Allow anything referencing a concrete StateMachineInstace access to
/// the backing artboard (explicitly not allowed on Scenes).
Artboard* artboard() const { return m_artboardInstance; }
void setParentStateMachineInstance(StateMachineInstance* instance)
{
m_parentStateMachineInstance = instance;
}
StateMachineInstance* parentStateMachineInstance()
{
return m_parentStateMachineInstance;
}
void setParentNestedArtboard(NestedArtboard* artboard)
{
m_parentNestedArtboard = artboard;
}
NestedArtboard* parentNestedArtboard() { return m_parentNestedArtboard; }
void notify(const std::vector<EventReport>& events,
NestedArtboard* context) override;
void notifyListenerViewModels(
const std::vector<ListenerViewModel*>& events);
/// Tracks an event that reported, will be cleared at the end of the next
/// advance.
void reportEvent(Event* event, float secondsDelay = 0.0f) override;
void applyEvents();
void reportListenerViewModel(ListenerViewModel*);
/// Gets the number of events that reported since the last advance.
std::size_t reportedEventCount() const;
/// Gets a reported event at an index < reportedEventCount().
const EventReport reportedEventAt(std::size_t index) const;
bool playsAudio() override { return true; }
BindableProperty* bindablePropertyInstance(
BindableProperty* bindableProperty) const;
DataBind* bindableDataBindToSource(
BindableProperty* bindableProperty) const;
DataBind* bindableDataBindToTarget(
BindableProperty* bindableProperty) const;
/// Find the per-instance BindablePropertyNumber for the given shared
/// StateTransition and property key (e.g. durationPropertyKey).
/// Returns nullptr if no binding exists.
BindablePropertyNumber* findTransitionPropertyInstance(
const StateTransition* transition,
uint32_t propertyKey) const;
bool hasListeners() { return m_hitComponents.size() > 0; }
bool hasFocusNodes();
bool focusNext();
bool focusPrevious();
/// Move focus to the nearest focus stop in the given direction from the
/// primary focus. Returns false when nothing is focused or no candidate
/// lies that way.
bool focusLeft();
bool focusRight();
bool focusUp();
bool focusDown();
void clearFocus();
/// Route a key event to the focused element via the active focus manager.
/// Returns true if the event was handled.
bool keyInput(Key key,
KeyModifiers modifiers,
bool isPressed,
bool isRepeat);
/// Route committed text (a typed character, an Input Method Editor (IME)
/// commit, a paste) to the focused element via the active focus manager.
/// Returns true if the text was handled.
bool textInput(const std::string& text);
/// The selected text of the focused element, via the active focus manager.
/// Empty when nothing with a selection is focused.
std::string selectedText() const;
void clearDataContext();
void relinkDataContext() override;
void rebuildDataBind(DataBind*) override;
// The artboard bound through this machine is not a dependent of the
// context itself, so context hooks reach it from here.
void mainViewModelInstanceChanged() override;
void updateDataBinds(bool applyTargetToSource = true) override;
void addDirtyDataBind(DataBind* dataBind) override;
void dataContextChanged() override;
/// Whether this machine's per-frame work as a list row (advance,
/// tryChangeState and updateDataBinds) would currently do nothing. See
/// Artboard::rowQuietState.
AdvancingComponent::QuietState rowQuietState();
// Something may have changed this machine: wake its artboard if a host
// had it quiet.
void wakeRow();
void dropInstanceValueBindsTargeting(Core* target) override;
void internalDataContext(rcp<DataContext> dataContext);
ScriptedObject* scriptedObject(const ScriptedObject*) const;
/// Queue a focus event for deferred execution during advance().
void queueFocusEvent(FocusListenerGroup* group, bool isFocus);
/// Queue a semantic event for deferred execution during advance().
void queueSemanticEvent(SemanticListenerGroup* group,
SemanticActionType actionType);
/// Fire a semantic action on the SemanticData with the given node ID.
void fireSemanticAction(uint32_t semanticNodeId,
SemanticActionType actionType);
FocusManager* focusManager();
/// Const overload of [focusManager], used by read-only consumers such as
/// condition evaluation.
const FocusManager* focusManager() const;
/// Parses embedder gamepad batch bytes (same format as JS
/// `GAMEPAD_BATCH_VERSION` in `registerGamepadInteractions`). Invokes
/// `focusManager()->gamepadDispatch` for each record. Returns false if the
/// buffer is invalid or truncated.
bool submitGamepadsFromBuffer(const uint8_t* data, size_t length);
/// Forward `invocation` to every `ScriptedDrawable` in this state machine
/// whose script declares a gamepad handler (`wantsGamePad()`), skipping
/// `alreadyDispatched` (typically the focus-tree recipient) so a script
/// is never invoked twice for the same event. Used by gamepad dispatch
/// after the focus bubble so non-focused scripts can still react.
HitResult broadcastGamepadToScriptedDrawables(
const ListenerInvocation& invocation,
ScriptedDrawable* alreadyDispatched);
/// Set an external focus manager to use instead of the internal one.
/// This is used when a nested artboard should share focus with its parent.
/// If the focus tree was already built with a different manager, it will
/// be rebuilt with the new manager.
void setExternalFocusManager(FocusManager* manager);
/// Set focus to a specific FocusData's node.
void setFocus(FocusData* focusData);
/// Queue a focus change requested by a FocusAction, to be applied once the
/// artboard's components are up to date for the frame. See
/// FocusManager::PendingFocusRequest for why this is deferred rather than
/// applied where the action runs.
void queueFocusTarget(FocusData* focusData);
void queueClearFocus();
/// [traversalKind]: 0=next, 1=previous, 2=up, 3=down, 4=left, 5=right
/// (sync with FocusActionTraversal's traversalKind property).
void queueFocusTraversal(uint32_t traversalKind);
/// Snapshot of the current focus state. Designed for host polling (e.g.
/// deciding whether to show a soft keyboard / IME). Cheap to query;
/// future fields (e.g. keyboard type) will be added additively.
struct FocusState
{
/// True if any element currently holds focus in this state machine's
/// active focus manager (internal or external).
bool hasFocus = false;
/// True if the focused element accepts keyboard input — i.e. a
/// TextInput, or a FocusData target with registered key/text-input
/// listeners. False when nothing is focused.
bool expectsKeyboardInput = false;
};
FocusState focusState() const;
/// Get the semantic manager for this state machine instance.
/// Returns the external manager if set, otherwise the internal one.
/// Returns nullptr if semantics has not been enabled. Out of line because
/// both managers live in the SMIInputExtras cluster.
SemanticManager* semanticManager() const;
/// Enable semantics for this state machine instance. Creates the
/// internal SemanticManager (if no external one was set) and builds
/// the semantic tree. This is a no-op if semantics is already enabled.
void enableSemantics();
/// Set an external semantic manager to use instead of the internal one.
/// Used when a nested artboard should share the parent's semantic tree.
/// Rebuilds the semantic tree with the new manager if already built.
/// @param parentNode If provided, the rebuilt tree is attached under it;
/// nullptr attaches at the manager root.
void setExternalSemanticManager(SemanticManager* manager,
rcp<SemanticNode> parentNode = nullptr);
#ifdef TESTING
size_t hitComponentsCount() { return m_hitComponents.size(); };
HitComponent* hitComponent(size_t index)
{
if (index < m_hitComponents.size())
{
return m_hitComponents[index].get();
}
return nullptr;
}
const LayerState* layerState(size_t index);
// Transition searches skipped because the layer had settled, across every
// instance (see StateMachineLayerInstance::updateState).
static uint64_t sm_settledLayerSkips;
// Layer advances skipped because the layer was frozen: settled in a state
// that keys nothing and waits on no exit time.
static uint64_t sm_frozenLayerAdvances;
#endif
void enablePointerEvents(int pointerId = 0);
void disablePointerEvents(int pointerId = 0);
void dispose();
private:
const StateMachine* m_machine;
std::vector<SMIInput*> m_inputInstances; // we own each pointer
StateMachineLayerInstance* m_layers = nullptr;
// Set once from machine->layerCount() and never mutated, so a uint32_t is
// ample. Kept adjacent to the other scalars so they share one 8 B slot
// instead of opening three.
uint32_t m_layerCount = 0;
uint8_t m_drawOrderChangeCounter = 0;
bool m_needsAdvance = false;
// One bit each, sharing the byte after m_needsAdvance; bit-fields can't
// have default member initializers before C++20, so the constructor sets
// them. Settled layers, see StateMachineLayerInstance::updateState: a bind
// was dirtied and not yet applied, so bindables may still hold old values.
bool m_bindsPending : 1;
// Some layer is settled, so unsettleLayers has work to do.
bool m_anySettled : 1;
// Set as the destructor starts. The artboard may already be gone (a host
// can destroy it first), so nothing may wake its row from then on.
bool m_destroying : 1;
std::vector<std::unique_ptr<HitComponent>> m_hitComponents;
std::vector<std::unique_ptr<ListenerGroup>> m_listenerGroups;
// Owns the in-flight scroll gesture. Latched for the whole gesture:
// re-picking per event would hand a flick's tail to the outer view.
HitComponent* m_scrollLatch = nullptr;
StateMachineInstance* m_parentStateMachineInstance = nullptr;
NestedArtboard* m_parentNestedArtboard = nullptr;
void unbind();
void removeEventListeners();
void initScriptedObjects();
// Something a settled layer's conditions read may have changed: search
// every layer again on its next update.
void unsettleLayers();
// Cold clusters. See state_machine_instance_clusters.hpp.
Sidecar<SMIReporting> m_reporting;
Sidecar<SMIBindables> m_bindables;
Sidecar<SMIInputExtras> m_inputExtras;
Sidecar<SMIScripting> m_scripting;
// Read accessors — null when the cluster's feature is unused, which every
// caller must handle. The ensure...() variants allocate and are defined in
// the .cpp, where the cluster types are complete.
const SMIReporting* reporting() const { return m_reporting.get(); }
SMIReporting* reporting() { return m_reporting.get(); }
SMIReporting& ensureReporting();
const SMIBindables* bindables() const { return m_bindables.get(); }
SMIBindables& ensureBindables();
const SMIInputExtras* inputExtras() const { return m_inputExtras.get(); }
SMIInputExtras* inputExtras() { return m_inputExtras.get(); }
SMIInputExtras& ensureInputExtras();
const SMIScripting* scripting() const { return m_scripting.get(); }
SMIScripting& ensureScripting();
// True while any reported event or viewModel listener is still waiting to
// be delivered — the "keep advancing" condition for advance() and
// advanceAndApply(). One null check on the cold cluster replaces two
// .empty() probes on containers that were inline for every instance.
bool hasPendingReports() const;
void processFocusEvents();
// Root artboard of this instance's tree, used to tag deferred focus
// requests so a manager shared across independent roots drains each root's
// requests only after that root has updated. See
// FocusManager::PendingFocusRequest.
const Artboard* rootArtboard() const;
void processSemanticEvents();
#ifdef WITH_RIVE_TOOLS
public:
void onInputChanged(InputChanged callback)
{
m_inputChangedCallback = callback;
}
void onDataBindChanged(DataBindChanged callback);
InputChanged m_inputChangedCallback = nullptr;
#endif
};
class HitComponent
{
public:
Component* component() const { return m_component; }
HitComponent(Component* component,
StateMachineInstance* stateMachineInstance) :
m_component(component), m_stateMachineInstance(stateMachineInstance)
{}
virtual ~HitComponent() {}
virtual HitResult processEvent(
Vec2D position,
ListenerType hitType,
bool canHit,
float timeStamp = 0,
int pointerId = 0,
PointerButton button = PointerButton::primary) = 0;
virtual HitResult processGamepadInvocation(
const ListenerInvocation& invocation,
ScriptedDrawable* alreadyDispatched) = 0;
virtual void prepareEvent(Vec2D position,
ListenerType hitType,
int pointerId) = 0;
virtual bool hitTest(Vec2D position) const = 0;
/// See StateMachineInstance::hitTestBounded; the same as hitTest for
/// everything but scripted drawables.
virtual bool hitTestBounded(Vec2D position) const
{
return hitTest(position);
}
virtual void enablePointerEvents(int pointerId = 0) {}
virtual void disablePointerEvents(int pointerId = 0) {}
/// Scroll dispatch. wantsScroll is the interest test; scrollGestureActive
/// keeps the latch alive between events.
virtual bool wantsScroll(Vec2D position, const ScrollEvent& event)
{
return false;
}
virtual bool scrollGestureActive() { return false; }
/// Ends any scroll gesture this component owns.
virtual void cancelScroll() {}
/// Whether an interactive scroll view sits under position at all, even
/// one that cannot move right now.
virtual bool hasScrollTarget(Vec2D position) { return false; }
/// Whether an opaque hit that cannot scroll sits under position. Ends
/// the walk: nothing behind it may take the event, as with hitOpaque.
virtual bool occludesScroll(Vec2D position) { return false; }
/// Whether a listener under position responds to presses of button.
virtual bool listensToButtonAt(Vec2D position, PointerButton button)
{
return false;
}
/// Whether an opaque hit sits under position, blocking presses from
/// reaching whatever lies behind it, as hitOpaque does in processEvent.
virtual bool occludesPointer(Vec2D position) { return false; }
virtual HitResult processScroll(Vec2D position,
const ScrollEvent& event,
float timeStamp)
{
return HitResult::none;
}
#ifdef TESTING
int earlyOutCount = 0;
#endif
protected:
Component* m_component;
StateMachineInstance* m_stateMachineInstance;
};
} // namespace rive
#endif