| #include "rive/animation/listener_invocation.hpp" | |
| #include "rive/component_dirt.hpp" | |
| #include "rive/assets/script_asset.hpp" | |
| #include "rive/scripted/scripted_drawable.hpp" | |
| #include "rive/scripted/scripted_layout.hpp" | |
| using namespace rive; | |
| #ifdef WITH_RIVE_SCRIPTING | |
| void ScriptedDrawable::didHydrateScriptInputs() | |
| { | |
| m_isAdvanceActive = true; | |
| addDirt(ComponentDirt::Paint); | |
| } | |
| void ScriptedDrawable::didReinit() | |
| { | |
| // A paused editor never ticks scripts, so advance and update driven | |
| // content, like gpu canvas fills, would stay blank until play; force one | |
| // zero step and an update to re-record it. | |
| m_isAdvanceActive = true; | |
| m_forceAdvance = true; | |
| addDirt(ComponentDirt::Paint | ComponentDirt::ScriptUpdate); | |
| } | |
| void ScriptedDrawable::draw(Renderer* renderer) | |
| { | |
| if (!draws() || m_vm == nullptr || !m_vm->valid()) | |
| { | |
| return; | |
| } | |
| float opacity = renderOpacity(); | |
| bool needsOpacitySave = (opacity != 1.0f); | |
| if (needsSaveOperation() || needsOpacitySave) | |
| { | |
| renderer->save(); | |
| } | |
| if (needsOpacitySave) | |
| { | |
| renderer->modulateOpacity(opacity); | |
| } | |
| renderer->transform(worldTransform()); | |
| m_vm->callDraw(this, m_self, renderer); | |
| if (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 || !m_vm->valid()) | |
| { | |
| return false; | |
| } | |
| 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; | |
| } | |
| if (!m_vm->callGamepadEvent(this, m_self, method, inv)) | |
| { | |
| return false; | |
| } | |
| wakeAdvance(); | |
| return true; | |
| } | |
| HitResult HitScriptedDrawable::processEvent(Vec2D position, | |
| ListenerType hitType, | |
| bool canHit, | |
| float timeStamp, | |
| int pointerId, | |
| PointerButton button) | |
| { | |
| HitResult hitResult = HitResult::none; | |
| auto scriptAsset = m_drawable->scriptAsset(); | |
| auto backend = m_drawable->backend(); | |
| if (backend == nullptr || !backend->valid() || scriptAsset == nullptr || | |
| !handlesEvent(canHit, hitType)) | |
| { | |
| return HitResult::none; | |
| } | |
| Vec2D localPos; | |
| auto hasLocalPos = m_drawable->worldToLocal(position, &localPos); | |
| if (!hasLocalPos) | |
| { | |
| return hitResult; | |
| } | |
| auto mName = methodName(canHit, hitType); | |
| if (backend->callPointerEvent(m_drawable, | |
| m_drawable->self(), | |
| mName.c_str(), | |
| pointerId, | |
| localPos, | |
| canHit ? hitType : ListenerType::exit, | |
| timeStamp, | |
| &hitResult)) | |
| { | |
| m_drawable->wakeAdvance(); | |
| } | |
| 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; | |
| } | |
| if (m_vm == nullptr || !m_vm->valid()) | |
| { | |
| return false; | |
| } | |
| bool shouldStopPropagation = m_vm->callKeyboardEvent(this, | |
| self(), | |
| key, | |
| modifiers, | |
| isPressed, | |
| isRepeat); | |
| wakeAdvance(); | |
| return shouldStopPropagation; | |
| } | |
| bool ScriptedDrawable::textInput(const std::string& text) | |
| { | |
| if (!wantsTextInput()) | |
| { | |
| return false; | |
| } | |
| if (m_vm == nullptr || !m_vm->valid()) | |
| { | |
| return false; | |
| } | |
| bool shouldStopPropagation = m_vm->callTextEvent(this, self(), text); | |
| wakeAdvance(); | |
| 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, | |
| PointerButton button) | |
| { | |
| 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; | |
| } | |
| } | |
| void ScriptedDrawable::wakeAdvance() | |
| { | |
| m_isAdvanceActive = true; | |
| addScriptedDirt(ComponentDirt::Paint); | |
| } | |
| 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) | |
| { | |
| bool forced = m_forceAdvance; | |
| m_forceAdvance = false; | |
| if (elapsedSeconds == 0 && !forced) | |
| { | |
| return false; | |
| } | |
| if (!m_isAdvanceActive || isCollapsed()) | |
| { | |
| 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 HitScriptedDrawable::hitTestBounded(Vec2D position) const | |
| { | |
| if (!m_drawable->is<ScriptedLayout>()) | |
| { | |
| // A scripted node has no box of its own; everywhere, like hitTest. | |
| return true; | |
| } | |
| Vec2D size; | |
| if (!m_drawable->as<ScriptedLayout>()->layoutSize(size)) | |
| { | |
| return false; | |
| } | |
| Vec2D local; | |
| if (!m_drawable->worldToLocal(position, &local)) | |
| { | |
| return false; | |
| } | |
| return local.x >= 0.0f && local.y >= 0.0f && local.x <= size.x && | |
| local.y <= size.y; | |
| } | |
| 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"; | |
| } |