| #ifdef WITH_RIVE_SCRIPTING |
| #include "rive/lua/rive_lua_libs.hpp" |
| #include "rive/animation/listener_invocation.hpp" |
| #endif |
| #include "rive/component_dirt.hpp" |
| #include "rive/assets/script_asset.hpp" |
| #include "rive/scripted/scripted_drawable.hpp" |
| |
| using namespace rive; |
| |
| #ifdef WITH_RIVE_SCRIPTING |
| |
| void ScriptedDrawable::didHydrateScriptInputs() |
| { |
| m_isAdvanceActive = true; |
| addDirt(ComponentDirt::Paint); |
| } |
| |
| void ScriptedDrawable::draw(Renderer* renderer) |
| { |
| if (!draws() || m_vm == nullptr) |
| { |
| return; |
| } |
| |
| lua_State* L = state(); |
| float opacity = renderOpacity(); |
| bool needsOpacitySave = (opacity != 1.0f); |
| if (m_needsSaveOperation || needsOpacitySave) |
| { |
| renderer->save(); |
| } |
| |
| if (needsOpacitySave) |
| { |
| renderer->modulateOpacity(opacity); |
| } |
| |
| renderer->transform(worldTransform()); |
| // Stack: [] |
| auto scriptedRenderer = lua_newrive<ScriptedRenderer>(L, renderer); |
| // Stack: [scriptedRenderer] |
| rive_lua_pushRef(L, m_self); |
| // Stack: [scriptedRenderer, self] |
| if (static_cast<lua_Type>(lua_getfield(L, -1, "draw")) == LUA_TFUNCTION) |
| { |
| // Stack: [scriptedRenderer, self, "draw"] |
| lua_pushvalue(L, -2); |
| // Stack: [scriptedRenderer, self, "draw", self] |
| lua_pushvalue(L, -4); |
| // Stack: [scriptedRenderer, self, "draw", self, scriptedRenderer] |
| if (static_cast<lua_Status>( |
| rive_lua_pcall_with_context(L, this, 2, 0)) != LUA_OK) |
| { |
| // Stack: [scriptedRenderer, self, status] |
| rive_lua_pop(L, 1); |
| } |
| } |
| else |
| { |
| // draw is assumed for legacy files but not implemented; no-op (the |
| // save/transform above stay balanced with the restore below). |
| rive_lua_pop(L, 1); // non-function field |
| } |
| scriptedRenderer->end(); |
| // Stack: [scriptedRenderer, self] |
| rive_lua_pop(L, 2); |
| |
| if (m_needsSaveOperation || needsOpacitySave) |
| { |
| renderer->restore(); |
| } |
| } |
| |
| std::vector<HitComponent*> ScriptedDrawable::hitComponents( |
| StateMachineInstance* sm) |
| { |
| if (!listensToPointerEvents()) |
| { |
| return {}; |
| } |
| auto* hitComponent = new HitScriptedDrawable(this, sm); |
| return std::vector<HitComponent*>{hitComponent}; |
| } |
| |
| bool ScriptedDrawable::gamepadDispatch(const ListenerInvocation& inv) |
| { |
| if (m_vm == nullptr || !state()) |
| { |
| return false; |
| } |
| lua_State* L = state(); |
| const char* method = nullptr; |
| switch (inv.kind()) |
| { |
| case ListenerInvocationKind::gamepadConnected: |
| method = "gamepadConnected"; |
| break; |
| case ListenerInvocationKind::gamepadEvent: |
| method = "gamepadEvent"; |
| break; |
| case ListenerInvocationKind::gamepadDisconnected: |
| method = "gamepadDisconnected"; |
| break; |
| default: |
| return false; |
| } |
| rive_lua_pushRef(L, m_self); |
| lua_getfield(L, -1, method); |
| if (static_cast<lua_Type>(lua_type(L, -1)) != LUA_TFUNCTION) |
| { |
| rive_lua_pop(L, 2); |
| return false; |
| } |
| lua_pushvalue(L, -2); |
| if (const GamepadConnectedInvocation* c = inv.asGamepadConnected()) |
| { |
| lua_newrive<ScriptedGamepadConnected>(L, c->snapshot); |
| } |
| else if (const GamepadEventInvocation* e = inv.asGamepadEvent()) |
| { |
| lua_newrive<ScriptedGamepadEvent>(L, *e); |
| } |
| else if (const GamepadDisconnectedInvocation* d = |
| inv.asGamepadDisconnected()) |
| { |
| lua_newrive<ScriptedGamepadDisconnected>(L, d->deviceId); |
| } |
| else |
| { |
| rive_lua_pop(L, 3); |
| return false; |
| } |
| if (static_cast<lua_Status>(rive_lua_pcall_with_context(L, this, 2, 0)) != |
| LUA_OK) |
| { |
| rive_lua_pop(L, 1); |
| } |
| rive_lua_pop(L, 1); |
| return true; |
| } |
| |
| HitResult HitScriptedDrawable::processEvent(Vec2D position, |
| ListenerType hitType, |
| bool canHit, |
| float timeStamp, |
| int pointerId) |
| { |
| HitResult hitResult = HitResult::none; |
| auto scriptAsset = m_drawable->scriptAsset(); |
| auto state = m_drawable->state(); |
| if (state == nullptr || scriptAsset == nullptr || |
| !handlesEvent(canHit, hitType)) |
| { |
| return HitResult::none; |
| } |
| Vec2D localPos; |
| auto hasLocalPos = m_drawable->worldToLocal(position, &localPos); |
| if (!hasLocalPos) |
| { |
| return hitResult; |
| } |
| rive_lua_pushRef(state, m_drawable->self()); |
| auto mName = methodName(canHit, hitType); |
| if (static_cast<lua_Type>(lua_getfield(state, -1, mName.c_str())) != |
| LUA_TFUNCTION) |
| { |
| // The pointer handler is assumed present for legacy files (all-bits |
| // default) but isn't actually implemented: report "not hit" so the |
| // state machine keeps walking other hit targets. |
| rive_lua_pop(state, 1); |
| } |
| else |
| { |
| lua_pushvalue(state, -2); |
| auto pointerEvent = |
| lua_newrive<ScriptedPointerEvent>(state, pointerId, localPos); |
| if (static_cast<lua_Status>( |
| rive_lua_pcall_with_context(state, m_drawable, 2, 0)) != LUA_OK) |
| { |
| fprintf(stderr, "%s failed\n", mName.c_str()); |
| rive_lua_pop(state, 1); |
| } |
| hitResult = pointerEvent->m_hitResult; |
| } |
| rive_lua_pop(state, 1); |
| return hitResult; |
| } |
| |
| HitResult HitScriptedDrawable::processGamepadInvocation( |
| const ListenerInvocation&, |
| ScriptedDrawable*) |
| { |
| // Gamepad dispatch to ScriptedDrawables runs through |
| // StateMachineInstance::broadcastGamepadToScriptedDrawables's walk over |
| // m_gamepadScriptedDrawables — that list includes gamepad-only scripts |
| // (which never enter m_hitComponents because hitComponents() is gated on |
| // listensToPointerEvents()). Returning none here avoids double-dispatch |
| // for drawables that handle both pointer and gamepad. |
| return HitResult::none; |
| } |
| |
| bool ScriptedDrawable::keyInput(Key key, |
| KeyModifiers modifiers, |
| bool isPressed, |
| bool isRepeat) |
| { |
| if (!wantsKeyboardInput()) |
| { |
| return false; |
| } |
| bool shouldStopPropagation = false; |
| auto L = state(); |
| if (L == nullptr) |
| { |
| return shouldStopPropagation; |
| } |
| // Stack: [] |
| rive_lua_pushRef(L, self()); |
| // Stack: [self] |
| if (static_cast<lua_Type>(lua_getfield(L, -1, "keyboardEvent")) != |
| LUA_TFUNCTION) |
| { |
| // Assumed for legacy files but not implemented; no-op. |
| rive_lua_pop(L, 2); // non-function field + self |
| return shouldStopPropagation; |
| } |
| // Stack: [self, field] |
| lua_pushvalue(L, -2); |
| // Stack: [self, field, self] |
| lua_newrive<ScriptedKeyboardInvocation>(L, |
| key, |
| modifiers, |
| isPressed, |
| isRepeat); |
| |
| // Stack: [self, field, self, keyEvent] |
| if (static_cast<lua_Status>(rive_lua_pcall_with_context(L, this, 2, 1)) != |
| LUA_OK) |
| { |
| fprintf(stderr, "%s failed\n", "keyboardEvent"); |
| // Stack: [self, status] |
| rive_lua_pop(L, 1); |
| } |
| else |
| { |
| if (lua_isboolean(L, -1)) |
| { |
| shouldStopPropagation = lua_toboolean(L, -1); |
| } |
| // Stack: [self, result] |
| rive_lua_pop(L, 1); |
| } |
| // Stack: [self] |
| rive_lua_pop(L, 1); |
| return shouldStopPropagation; |
| } |
| |
| bool ScriptedDrawable::textInput(const std::string& text) |
| { |
| if (!wantsTextInput()) |
| { |
| return false; |
| } |
| bool shouldStopPropagation = false; |
| auto L = state(); |
| if (L == nullptr) |
| { |
| return shouldStopPropagation; |
| } |
| // Stack: [] |
| rive_lua_pushRef(L, self()); |
| // Stack: [self] |
| if (static_cast<lua_Type>(lua_getfield(L, -1, "textEvent")) != |
| LUA_TFUNCTION) |
| { |
| // Assumed for legacy files but not implemented; no-op. |
| rive_lua_pop(L, 2); // non-function field + self |
| return shouldStopPropagation; |
| } |
| // Stack: [self, field] |
| lua_pushvalue(L, -2); |
| // Stack: [self, field, self] |
| lua_newrive<ScriptedTextInputInvocation>(L, text); |
| |
| // Stack: [self, field, self, textInvocation] |
| if (static_cast<lua_Status>(rive_lua_pcall_with_context(L, this, 2, 1)) != |
| LUA_OK) |
| { |
| fprintf(stderr, "%s failed\n", "textEvent"); |
| // Stack: [self, status] |
| rive_lua_pop(L, 1); |
| } |
| else |
| { |
| if (lua_isboolean(L, -1)) |
| { |
| shouldStopPropagation = lua_toboolean(L, -1); |
| } |
| // Stack: [self, result] |
| rive_lua_pop(L, 1); |
| } |
| // Stack: [self] |
| rive_lua_pop(L, 1); |
| return shouldStopPropagation; |
| } |
| |
| bool ScriptedDrawable::willDraw() |
| { |
| return Super::willDraw() && m_vm != nullptr && draws(); |
| } |
| |
| #else |
| void ScriptedDrawable::draw(Renderer* renderer) {} |
| |
| std::vector<HitComponent*> ScriptedDrawable::hitComponents( |
| StateMachineInstance* sm) |
| { |
| return {}; |
| } |
| |
| HitResult HitScriptedDrawable::processEvent(Vec2D position, |
| ListenerType hitType, |
| bool canHit, |
| float timeStamp, |
| int pointerId) |
| { |
| return HitResult::none; |
| } |
| |
| HitResult HitScriptedDrawable::processGamepadInvocation( |
| const ListenerInvocation& invocation, |
| ScriptedDrawable* alreadyDispatched) |
| { |
| // TODO: implement |
| return HitResult::none; |
| } |
| |
| bool ScriptedDrawable::willDraw() { return Super::willDraw() && draws(); } |
| |
| #endif |
| |
| void ScriptedDrawable::update(ComponentDirt value) |
| { |
| Super::update(value); |
| if ((value & ComponentDirt::ScriptUpdate) == ComponentDirt::ScriptUpdate) |
| { |
| scriptUpdate(); |
| m_isAdvanceActive = true; |
| } |
| } |
| |
| Core* ScriptedDrawable::hitTest(HitInfo*, const Mat2D&) { return nullptr; } |
| |
| StatusCode ScriptedDrawable::onAddedDirty(CoreContext* context) |
| { |
| auto code = Super::onAddedDirty(context); |
| if (code != StatusCode::Ok) |
| { |
| return code; |
| } |
| artboard()->addScriptedObject(this); |
| return StatusCode::Ok; |
| } |
| |
| bool ScriptedDrawable::advanceComponent(float elapsedSeconds, |
| AdvanceFlags flags) |
| { |
| if (elapsedSeconds == 0) |
| { |
| return false; |
| } |
| if (!m_isAdvanceActive) |
| { |
| return false; |
| } |
| m_isAdvanceActive = false; |
| if (!enums::is_flag_set(flags, AdvanceFlags::AdvanceNested)) |
| { |
| elapsedSeconds = 0; |
| } |
| auto advanced = scriptAdvance(elapsedSeconds); |
| if (advanced) |
| { |
| m_isAdvanceActive = true; |
| addScriptedDirt(ComponentDirt::Paint); |
| } |
| return advanced; |
| } |
| |
| bool ScriptedDrawable::addScriptedDirt(ComponentDirt value, bool recurse) |
| { |
| return Drawable::addDirt(value, recurse); |
| } |
| |
| void ScriptedDrawable::addProperty(CustomProperty* prop) |
| { |
| auto scriptInput = ScriptInput::from(prop); |
| if (scriptInput != nullptr) |
| { |
| scriptInput->scriptedObject(this); |
| } |
| CustomPropertyContainer::addProperty(prop); |
| } |
| |
| StatusCode ScriptedDrawable::import(ImportStack& importStack) |
| { |
| auto result = registerReferencer(importStack); |
| if (result != StatusCode::Ok) |
| { |
| return result; |
| } |
| return Super::import(importStack); |
| } |
| |
| Core* ScriptedDrawable::clone() const |
| { |
| ScriptedDrawable* twin = |
| ScriptedDrawableBase::clone()->as<ScriptedDrawable>(); |
| if (m_fileAsset != nullptr) |
| { |
| twin->setAsset(m_fileAsset); |
| } |
| return twin; |
| } |
| |
| void ScriptedDrawable::markNeedsUpdate() |
| { |
| if (inUpdatePhase()) |
| { |
| return; |
| } |
| addScriptedDirt(ComponentDirt::ScriptUpdate); |
| } |
| |
| bool ScriptedDrawable::worldToLocal(Vec2D world, Vec2D* local) |
| { |
| Mat2D toMountedArtboard; |
| if (!worldTransform().invert(&toMountedArtboard)) |
| { |
| return false; |
| } |
| |
| *local = toMountedArtboard * world; |
| |
| return true; |
| } |
| |
| bool HitScriptedDrawable::handlesEvent(bool canHit, ListenerType hitEvent) |
| { |
| if (canHit) |
| { |
| switch (hitEvent) |
| { |
| case ListenerType::down: |
| return m_drawable->wantsPointerDown(); |
| case ListenerType::up: |
| return m_drawable->wantsPointerUp(); |
| case ListenerType::dragStart: |
| return false; |
| case ListenerType::dragEnd: |
| return false; |
| default: |
| return m_drawable->wantsPointerMove(); |
| } |
| } |
| return m_drawable->wantsPointerExit(); |
| } |
| |
| std::string HitScriptedDrawable::methodName(bool canHit, ListenerType hitEvent) |
| { |
| if (canHit) |
| { |
| switch (hitEvent) |
| { |
| case ListenerType::down: |
| return "pointerDown"; |
| case ListenerType::up: |
| return "pointerUp"; |
| case ListenerType::dragStart: |
| case ListenerType::dragEnd: |
| return ""; |
| default: |
| return "pointerMove"; |
| } |
| } |
| return "pointerExit"; |
| } |