feature: scripted listener actions (#11468) f3a89390cb

Co-authored-by: hernan <hernan@rive.app>
This commit is contained in:
bodymovin
2026-01-15 19:53:42 +00:00
parent bfe1ebbcf2
commit 6eaf71f485
32 changed files with 389 additions and 25 deletions

View File

@@ -1 +1 @@
9112280455e25db4649d446601f443c763151649
f3a89390cb428a5ea841d21de91f9cb2adc312df

View File

@@ -0,0 +1,20 @@
{
"name": "ScriptedListenerAction",
"key": {
"int": 646,
"string": "scriptedlisteneraction"
},
"extends": "animation/listener_action.json",
"properties": {
"scriptAssetId": {
"type": "Id",
"typeRuntime": "uint",
"initialValue": "Core.missingId",
"initialValueRuntime": "-1",
"key": {
"int": 930,
"string": "scriptassetid"
}
}
}
}

View File

@@ -12,7 +12,8 @@ public:
StatusCode import(ImportStack& importStack) override;
virtual void perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const = 0;
Vec2D previousPosition,
int pointerId) const = 0;
};
} // namespace rive

View File

@@ -9,7 +9,8 @@ class ListenerAlignTarget : public ListenerAlignTargetBase
public:
void perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const override;
Vec2D previousPosition,
int pointerId) const override;
};
} // namespace rive

View File

@@ -12,7 +12,8 @@ public:
bool validateNestedInputType(const NestedInput* input) const override;
void perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const override;
Vec2D previousPosition,
int pointerId) const override;
};
} // namespace rive

View File

@@ -9,7 +9,8 @@ class ListenerFireEvent : public ListenerFireEventBase
public:
void perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const override;
Vec2D previousPosition,
int pointerId) const override;
};
} // namespace rive

View File

@@ -12,7 +12,8 @@ public:
bool validateNestedInputType(const NestedInput* input) const override;
void perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const override;
Vec2D previousPosition,
int pointerId) const override;
};
} // namespace rive

View File

@@ -12,7 +12,8 @@ public:
bool validateNestedInputType(const NestedInput* input) const override;
void perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const override;
Vec2D previousPosition,
int pointerId) const override;
};
} // namespace rive

View File

@@ -11,7 +11,8 @@ public:
~ListenerViewModelChange();
void perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const override;
Vec2D previousPosition,
int pointerId) const override;
StatusCode import(ImportStack& importStack) override;
private:

View File

@@ -0,0 +1,36 @@
#ifndef _RIVE_SCRIPTED_LISTENER_ACTION_HPP_
#define _RIVE_SCRIPTED_LISTENER_ACTION_HPP_
#include "rive/generated/animation/scripted_listener_action_base.hpp"
#include "rive/scripted/scripted_object.hpp"
#include <stdio.h>
namespace rive
{
class ScriptedListenerAction : public ScriptedListenerActionBase,
public ScriptedObject
{
public:
void perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition,
int pointerId) const override;
void performStateful(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition,
int pointerId) const;
uint32_t assetId() override { return scriptAssetId(); }
bool addScriptedDirt(ComponentDirt value, bool recurse = false) override
{
return false;
}
ScriptProtocol scriptProtocol() override
{
return ScriptProtocol::listenerAction;
}
Component* component() override { return nullptr; }
StatusCode import(ImportStack& importStack) override;
Core* clone() const override;
};
} // namespace rive
#endif

View File

@@ -40,6 +40,7 @@ class DataBind;
class BindableProperty;
class HitDrawable;
class ListenerViewModel;
class ScriptedListenerAction;
typedef void (*DataBindChanged)();
#ifdef WITH_RIVE_TOOLS
@@ -205,6 +206,7 @@ public:
bool hasListeners() { return m_hitComponents.size() > 0; }
void clearDataContext();
void internalDataContext(DataContext* dataContext);
ScriptedObject* scriptedObject(const ScriptedObject*);
#ifdef TESTING
size_t hitComponentsCount() { return m_hitComponents.size(); };
HitComponent* hitComponent(size_t index)
@@ -238,6 +240,8 @@ private:
std::vector<ListenerViewModel*> m_reportingListenerViewModels;
std::unordered_map<BindableProperty*, BindableProperty*>
m_bindablePropertyInstances;
std::unordered_map<const ScriptedObject*, ScriptedObject*>
m_scriptedListenerActionsMap;
std::unordered_map<BindableProperty*, DataBind*>
m_bindableDataBindsToTarget;
std::unordered_map<BindableProperty*, DataBind*>

View File

@@ -31,7 +31,8 @@ public:
void performChanges(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const;
Vec2D previousPosition,
int pointerId) const;
void decodeViewModelPathIds(Span<const uint8_t> value) override;
void copyViewModelPathIds(const StateMachineListenerBase& object) override;
std::vector<uint32_t> viewModelPathIdsBuffer() const;

View File

@@ -25,7 +25,8 @@ enum ScriptProtocol
node,
layout,
converter,
pathEffect
pathEffect,
listenerAction
};
#ifdef WITH_RIVE_SCRIPTING

View File

@@ -0,0 +1,71 @@
#ifndef _RIVE_SCRIPTED_LISTENER_ACTION_BASE_HPP_
#define _RIVE_SCRIPTED_LISTENER_ACTION_BASE_HPP_
#include "rive/animation/listener_action.hpp"
#include "rive/core/field_types/core_uint_type.hpp"
namespace rive
{
class ScriptedListenerActionBase : public ListenerAction
{
protected:
typedef ListenerAction Super;
public:
static const uint16_t typeKey = 646;
/// Helper to quickly determine if a core object extends another without
/// RTTI at runtime.
bool isTypeOf(uint16_t typeKey) const override
{
switch (typeKey)
{
case ScriptedListenerActionBase::typeKey:
case ListenerActionBase::typeKey:
return true;
default:
return false;
}
}
uint16_t coreType() const override { return typeKey; }
static const uint16_t scriptAssetIdPropertyKey = 930;
protected:
uint32_t m_ScriptAssetId = -1;
public:
inline uint32_t scriptAssetId() const { return m_ScriptAssetId; }
void scriptAssetId(uint32_t value)
{
if (m_ScriptAssetId == value)
{
return;
}
m_ScriptAssetId = value;
scriptAssetIdChanged();
}
Core* clone() const override;
void copy(const ScriptedListenerActionBase& object)
{
m_ScriptAssetId = object.m_ScriptAssetId;
ListenerAction::copy(object);
}
bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
{
switch (propertyKey)
{
case scriptAssetIdPropertyKey:
m_ScriptAssetId = CoreUintType::deserialize(reader);
return true;
}
return ListenerAction::deserialize(propertyKey, reader);
}
protected:
virtual void scriptAssetIdChanged() {}
};
} // namespace rive
#endif

View File

@@ -50,6 +50,7 @@
#include "rive/animation/nested_simple_animation.hpp"
#include "rive/animation/nested_state_machine.hpp"
#include "rive/animation/nested_trigger.hpp"
#include "rive/animation/scripted_listener_action.hpp"
#include "rive/animation/state_machine.hpp"
#include "rive/animation/state_machine_bool.hpp"
#include "rive/animation/state_machine_component.hpp"
@@ -477,6 +478,8 @@ public:
return new AnimationState();
case NestedTriggerBase::typeKey:
return new NestedTrigger();
case ScriptedListenerActionBase::typeKey:
return new ScriptedListenerAction();
case KeyedObjectBase::typeKey:
return new KeyedObject();
case AnimationBase::typeKey:
@@ -1194,6 +1197,9 @@ public:
case NestedInputBase::inputIdPropertyKey:
object->as<NestedInputBase>()->inputId(value);
break;
case ScriptedListenerActionBase::scriptAssetIdPropertyKey:
object->as<ScriptedListenerActionBase>()->scriptAssetId(value);
break;
case KeyedObjectBase::objectIdPropertyKey:
object->as<KeyedObjectBase>()->objectId(value);
break;
@@ -2737,6 +2743,9 @@ public:
return object->as<AnimationStateBase>()->animationId();
case NestedInputBase::inputIdPropertyKey:
return object->as<NestedInputBase>()->inputId();
case ScriptedListenerActionBase::scriptAssetIdPropertyKey:
return object->as<ScriptedListenerActionBase>()
->scriptAssetId();
case KeyedObjectBase::objectIdPropertyKey:
return object->as<KeyedObjectBase>()->objectId();
case BlendAnimationBase::animationIdPropertyKey:
@@ -3725,6 +3734,7 @@ public:
case ListenerInputChangeBase::nestedInputIdPropertyKey:
case AnimationStateBase::animationIdPropertyKey:
case NestedInputBase::inputIdPropertyKey:
case ScriptedListenerActionBase::scriptAssetIdPropertyKey:
case KeyedObjectBase::objectIdPropertyKey:
case BlendAnimationBase::animationIdPropertyKey:
case BlendAnimationDirectBase::inputIdPropertyKey:
@@ -4377,6 +4387,8 @@ public:
return object->is<AnimationStateBase>();
case NestedInputBase::inputIdPropertyKey:
return object->is<NestedInputBase>();
case ScriptedListenerActionBase::scriptAssetIdPropertyKey:
return object->is<ScriptedListenerActionBase>();
case KeyedObjectBase::objectIdPropertyKey:
return object->is<KeyedObjectBase>();
case BlendAnimationBase::animationIdPropertyKey:

View File

@@ -33,6 +33,8 @@ protected:
#ifdef WITH_RIVE_TOOLS
bool hasValidVM();
#endif
private:
DataContext* m_dataContext = nullptr;
public:
virtual ~ScriptedObject() { scriptDispose(); }
@@ -48,7 +50,8 @@ public:
void scriptUpdate();
void reinit();
virtual void markNeedsUpdate();
virtual DataContext* dataContext() { return nullptr; }
virtual DataContext* dataContext() { return m_dataContext; }
void dataContext(DataContext* value) { m_dataContext = value; }
#ifdef WITH_RIVE_SCRIPTING
virtual bool scriptInit(lua_State* state);
lua_State* state() { return m_state; }

View File

@@ -7,7 +7,8 @@ using namespace rive;
void ListenerAlignTarget::perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const
Vec2D previousPosition,
int pointerId) const
{
auto coreTarget = stateMachineInstance->artboard()->resolve(targetId());
if (coreTarget == nullptr || !coreTarget->is<Node>())

View File

@@ -26,7 +26,8 @@ bool ListenerBoolChange::validateNestedInputType(const NestedInput* input) const
void ListenerBoolChange::perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const
Vec2D previousPosition,
int pointerId) const
{
if (nestedInputId() != Core::emptyId)
{

View File

@@ -6,7 +6,8 @@ using namespace rive;
void ListenerFireEvent::perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const
Vec2D previousPosition,
int pointerId) const
{
auto coreEvent = stateMachineInstance->artboard()->resolve(eventId());
if (coreEvent == nullptr || !coreEvent->is<Event>())

View File

@@ -29,7 +29,8 @@ bool ListenerNumberChange::validateNestedInputType(
void ListenerNumberChange::perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const
Vec2D previousPosition,
int pointerId) const
{
if (nestedInputId() != Core::emptyId)
{

View File

@@ -30,7 +30,8 @@ bool ListenerTriggerChange::validateNestedInputType(
void ListenerTriggerChange::perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const
Vec2D previousPosition,
int pointerId) const
{
if (nestedInputId() != Core::emptyId)
{

View File

@@ -37,7 +37,8 @@ StatusCode ListenerViewModelChange::import(ImportStack& importStack)
void ListenerViewModelChange::perform(
StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const
Vec2D previousPosition,
int pointerId) const
{
// Get the bindable property instance from the state machine instance
// context

View File

@@ -0,0 +1,86 @@
#include "rive/animation/scripted_listener_action.hpp"
#include "rive/animation/state_machine_instance.hpp"
using namespace rive;
// Note: performStateful is the actual instance of the ScriptedListenerAction
// that will run the script. perform itself will look for the map between the
// stateless and the stateful instances of this class.
void ScriptedListenerAction::performStateful(
StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition,
int pointerId) const
{
#ifdef WITH_RIVE_SCRIPTING
if (m_state == nullptr)
{
return;
}
// Stack: []
rive_lua_pushRef(m_state, m_self);
// Stack: [self]
lua_getfield(m_state, -1, "perform");
// Stack: [self, field]
lua_pushvalue(m_state, -2);
// Stack: [self, field, self]
lua_newrive<ScriptedPointerEvent>(m_state, pointerId, position);
// Stack: [self, field, self, pointerEvent]
if (static_cast<lua_Status>(rive_lua_pcall(m_state, 2, 0)) == LUA_OK)
{
rive_lua_pop(m_state, 1);
}
else
{
rive_lua_pop(m_state, 2);
}
#endif
}
void ScriptedListenerAction::perform(StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition,
int pointerId) const
{
#ifdef WITH_RIVE_SCRIPTING
auto scriptedObject = stateMachineInstance->scriptedObject(this);
if (scriptedObject != nullptr)
{
auto statefulListenerAction =
static_cast<ScriptedListenerAction*>(scriptedObject);
statefulListenerAction->performStateful(stateMachineInstance,
position,
previousPosition,
pointerId);
}
#endif
}
StatusCode ScriptedListenerAction::import(ImportStack& importStack)
{
auto result = registerReferencer(importStack);
if (result != StatusCode::Ok)
{
return result;
}
return Super::import(importStack);
}
Core* ScriptedListenerAction::clone() const
{
ScriptedListenerAction* twin =
ScriptedListenerActionBase::clone()->as<ScriptedListenerAction>();
if (m_fileAsset != nullptr)
{
twin->setAsset(m_fileAsset);
}
for (auto prop : m_customProperties)
{
auto clonedValue = prop->clone()->as<CustomProperty>();
twin->addProperty(clonedValue);
}
return twin;
}

View File

@@ -19,6 +19,8 @@
#include "rive/animation/state_machine_trigger.hpp"
#include "rive/animation/state_machine.hpp"
#include "rive/animation/state_transition.hpp"
#include "rive/animation/listener_action.hpp"
#include "rive/animation/scripted_listener_action.hpp"
#include "rive/animation/transition_condition.hpp"
#include "rive/animation/transition_comparator.hpp"
#include "rive/animation/transition_property_viewmodel_comparator.hpp"
@@ -1480,9 +1482,41 @@ StateMachineInstance::StateMachineInstance(const StateMachine* machine,
this);
m_hitComponents.push_back(std::move(hc));
}
// Initialize local instances of ScriptedListenerActions
for (std::size_t i = 0; i < machine->listenerCount(); i++)
{
auto listener = machine->listener(i);
for (std::size_t j = 0; j < listener->actionCount(); j++)
{
auto action = listener->action(j);
if (action->is<ScriptedListenerAction>())
{
auto scriptedListenerAction =
action->as<ScriptedListenerAction>();
auto scriptedListenerActionClone =
static_cast<ScriptedListenerAction*>(
scriptedListenerAction->clone());
scriptedListenerActionClone->reinit();
m_scriptedListenerActionsMap[scriptedListenerAction] =
scriptedListenerActionClone;
}
}
}
sortHitComponents();
}
ScriptedObject* StateMachineInstance::scriptedObject(
const ScriptedObject* source)
{
auto itr = m_scriptedListenerActionsMap.find(source);
if (itr != m_scriptedListenerActionsMap.end())
{
return itr->second;
}
return nullptr;
}
StateMachineInstance::~StateMachineInstance()
{
unbind();
@@ -1506,6 +1540,12 @@ StateMachineInstance::~StateMachineInstance()
delete listenerViewModel;
}
m_bindablePropertyInstances.clear();
for (auto& pair : m_scriptedListenerActionsMap)
{
delete pair.second;
pair.second = nullptr;
}
m_scriptedListenerActionsMap.clear();
}
void StateMachineInstance::removeEventListeners()
@@ -1816,6 +1856,10 @@ void StateMachineInstance::internalDataContext(DataContext* dataContext)
{
listenerViewModel->bindFromContext(dataContext);
}
for (auto& scriptedObjectItr : m_scriptedListenerActionsMap)
{
scriptedObjectItr.second->dataContext(dataContext);
}
}
void StateMachineInstance::rebind()
@@ -1955,7 +1999,8 @@ void StateMachineInstance::notifyListenerViewModels(
{
listenerViewModel->listener()->performChanges(this,
Vec2D(),
Vec2D());
Vec2D(),
0);
}
}
}
@@ -2011,7 +2056,7 @@ void StateMachineInstance::notifyEventListeners(
sourceArtboard->resolve(listener->eventId());
if (listenerEvent == event.event())
{
listener->performChanges(this, Vec2D(), Vec2D());
listener->performChanges(this, Vec2D(), Vec2D(), 0);
break;
}
}

View File

@@ -44,11 +44,15 @@ const ListenerAction* StateMachineListener::action(size_t index) const
void StateMachineListener::performChanges(
StateMachineInstance* stateMachineInstance,
Vec2D position,
Vec2D previousPosition) const
Vec2D previousPosition,
int pointerId) const
{
for (auto& action : m_actions)
{
action->perform(stateMachineInstance, position, previousPosition);
action->perform(stateMachineInstance,
position,
previousPosition,
pointerId);
}
}

View File

@@ -66,7 +66,8 @@ bool OptionalScriptedMethods::verifyImplementation(ScriptedObject* object,
if (scriptProtocol == ScriptProtocol::node ||
scriptProtocol == ScriptProtocol::layout ||
scriptProtocol == ScriptProtocol::converter ||
scriptProtocol == ScriptProtocol::pathEffect)
scriptProtocol == ScriptProtocol::pathEffect ||
scriptProtocol == ScriptProtocol::listenerAction)
{
if (static_cast<lua_Type>(lua_getfield(state, -1, "update")) ==
LUA_TFUNCTION)

View File

@@ -0,0 +1,11 @@
#include "rive/generated/animation/scripted_listener_action_base.hpp"
#include "rive/animation/scripted_listener_action.hpp"
using namespace rive;
Core* ScriptedListenerActionBase::clone() const
{
auto cloned = new ScriptedListenerAction();
cloned->copy(*this);
return cloned;
}

View File

@@ -191,7 +191,8 @@ ProcessEventResult ListenerGroup::processEvent(
_listener->performChanges(
stateMachineInstance,
position,
Vec2D(previousPosition->x, previousPosition->y));
Vec2D(previousPosition->x, previousPosition->y),
pointerId);
stateMachineInstance->markNeedsAdvance();
consume();
}
@@ -206,7 +207,8 @@ ProcessEventResult ListenerGroup::processEvent(
_listener->performChanges(
stateMachineInstance,
position,
Vec2D(previousPosition->x, previousPosition->y));
Vec2D(previousPosition->x, previousPosition->y),
pointerId);
stateMachineInstance->markNeedsAdvance();
consume();
}
@@ -221,7 +223,8 @@ ProcessEventResult ListenerGroup::processEvent(
_listener->performChanges(
stateMachineInstance,
position,
Vec2D(previousPosition->x, previousPosition->y));
Vec2D(previousPosition->x, previousPosition->y),
pointerId);
stateMachineInstance->markNeedsAdvance();
if (!m_hasDragged)
{

View File

@@ -14,7 +14,14 @@ static int viewmodel_new(lua_State* L)
ViewModel* viewModel = (ViewModel*)lua_touserdata(L, lua_upvalueindex(1));
if (viewModel)
{
#ifdef WITH_RIVE_TOOLS
viewModel->file()->triggerViewModelCreatedCallback(true);
#endif
auto instance = viewModel->createInstance();
#ifdef WITH_RIVE_TOOLS
viewModel->file()->triggerViewModelCreatedCallback(false);
#endif
lua_newrive<ScriptedViewModel>(L, L, ref_rcp(viewModel), instance);
return 1;
}

Binary file not shown.

View File

@@ -0,0 +1,46 @@
#include "catch.hpp"
#include "scripting_test_utilities.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/lua/rive_lua_libs.hpp"
#include "rive/viewmodel/viewmodel_instance_string.hpp"
#include "rive_file_reader.hpp"
using namespace rive;
TEST_CASE("scripted listener action", "[silver]")
{
rive::SerializingFactory silver;
auto file = ReadRiveFile("assets/scripted_listener_action.riv", &silver);
auto artboard = file->artboardDefault();
silver.frameSize(artboard->width(), artboard->height());
REQUIRE(artboard != nullptr);
auto stateMachine = artboard->stateMachineAt(0);
auto vmi = file->createViewModelInstance(artboard.get());
stateMachine->bindViewModelInstance(vmi);
stateMachine->advanceAndApply(0.1f);
auto renderer = silver.makeRenderer();
artboard->draw(renderer.get());
silver.addFrame();
stateMachine->pointerDown(rive::Vec2D(200.0f, 20.0f), 1);
stateMachine->pointerUp(rive::Vec2D(200.0f, 20.0f), 1);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
stateMachine->pointerDown(rive::Vec2D(300.0f, 20.0f), 2);
stateMachine->pointerUp(rive::Vec2D(300.0f, 20.0f), 2);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
stateMachine->pointerDown(rive::Vec2D(400.0f, 20.0f), 3);
stateMachine->pointerUp(rive::Vec2D(400.0f, 20.0f), 3);
stateMachine->advanceAndApply(0.016f);
artboard->draw(renderer.get());
CHECK(silver.matches("scripted_listener_action"));
}

Binary file not shown.