feat(core): mouse button options (#14518) 60eb10b83d added mouse button options Co-authored-by: Jonathon Copeland <jcopela4@gmail.com>
diff --git a/.rive_head b/.rive_head index 1b52e3e..0fbbc87 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -30c30acec9ddcb98f7a85cd53cf7065fa4bcc189 +60eb10b83d05147324564606dea22d69e358a6c6
diff --git a/dev/core_generator/lib/main.dart b/dev/core_generator/lib/main.dart index 1d433d9..a550cfc 100644 --- a/dev/core_generator/lib/main.dart +++ b/dev/core_generator/lib/main.dart
@@ -9,7 +9,8 @@ Directory(defsPath).list(recursive: true).listen( (entity) { if (entity is File && entity.path.toLowerCase().endsWith('.json')) { - Definition.make(entity.path.substring(defsPath.length)); + Definition.make( + entity.path.substring(defsPath.length).replaceAll('\\', '/')); } }, onDone: Definition.generate,
diff --git a/include/rive/animation/listener_invocation.hpp b/include/rive/animation/listener_invocation.hpp index 176c0fe..3029a84 100644 --- a/include/rive/animation/listener_invocation.hpp +++ b/include/rive/animation/listener_invocation.hpp
@@ -7,6 +7,7 @@ #include "rive/input/standard_gamepad.hpp" #include "rive/listener_type.hpp" #include "rive/math/vec2d.hpp" +#include "rive/pointer_button.hpp" #include <string> #include <type_traits> @@ -44,6 +45,7 @@ int pointerId = 0; ListenerType hitEvent = ListenerType::move; float timeStamp = 0.f; + PointerButton button = PointerButton::primary; }; struct KeyboardInvocation @@ -130,11 +132,13 @@ class ListenerInvocation { public: - static ListenerInvocation pointer(Vec2D position, - Vec2D previousPosition, - int pointerId, - ListenerType hitEvent, - float timeStamp); + static ListenerInvocation pointer( + Vec2D position, + Vec2D previousPosition, + int pointerId, + ListenerType hitEvent, + float timeStamp, + PointerButton button = PointerButton::primary); static ListenerInvocation keyboard(Key key, KeyModifiers modifiers, bool isPressed,
diff --git a/include/rive/animation/listener_types/listener_input_type.hpp b/include/rive/animation/listener_types/listener_input_type.hpp index 1d616f4..84f79ba 100644 --- a/include/rive/animation/listener_types/listener_input_type.hpp +++ b/include/rive/animation/listener_types/listener_input_type.hpp
@@ -1,6 +1,7 @@ #ifndef _RIVE_LISTENER_INPUT_TYPE_HPP_ #define _RIVE_LISTENER_INPUT_TYPE_HPP_ #include "rive/generated/animation/listener_types/listener_input_type_base.hpp" +#include "rive/pointer_button.hpp" #include <stdio.h> namespace rive { @@ -8,6 +9,10 @@ { public: StatusCode import(ImportStack& importStack) override; + virtual PointerButton pointerButton() const + { + return PointerButton::primary; + } }; } // namespace rive
diff --git a/include/rive/animation/listener_types/listener_input_type_pointer_button.hpp b/include/rive/animation/listener_types/listener_input_type_pointer_button.hpp new file mode 100644 index 0000000..62ba44c --- /dev/null +++ b/include/rive/animation/listener_types/listener_input_type_pointer_button.hpp
@@ -0,0 +1,16 @@ +#ifndef _RIVE_LISTENER_INPUT_TYPE_POINTER_BUTTON_HPP_ +#define _RIVE_LISTENER_INPUT_TYPE_POINTER_BUTTON_HPP_ +#include "rive/generated/animation/listener_types/listener_input_type_pointer_button_base.hpp" +namespace rive +{ +class ListenerInputTypePointerButton : public ListenerInputTypePointerButtonBase +{ +public: + PointerButton pointerButton() const override + { + return (PointerButton)pointerButtonValue(); + } +}; +} // namespace rive + +#endif
diff --git a/include/rive/animation/nested_state_machine.hpp b/include/rive/animation/nested_state_machine.hpp index 48dca38..474a898 100644 --- a/include/rive/animation/nested_state_machine.hpp +++ b/include/rive/animation/nested_state_machine.hpp
@@ -27,8 +27,12 @@ HitResult pointerMove(Vec2D position, float timeStamp = 0, int pointerId = 0); - HitResult pointerDown(Vec2D position, int pointerId = 0); - HitResult pointerUp(Vec2D position, int pointerId = 0); + HitResult pointerDown(Vec2D position, + int pointerId = 0, + PointerButton button = PointerButton::primary); + HitResult pointerUp(Vec2D position, + int pointerId = 0, + PointerButton button = PointerButton::primary); HitResult pointerExit(Vec2D position, int pointerId = 0); HitResult pointerScroll(Vec2D position, const ScrollEvent& event, @@ -39,8 +43,15 @@ bool hasScrollTargetAt(Vec2D position); bool scrollOccludedAt(Vec2D position); void cancelScroll(); - HitResult dragStart(Vec2D position, float timeStamp = 0, int pointerId = 0); - HitResult dragEnd(Vec2D position, float timeStamp = 0, int pointerId = 0); + bool listensToButtonAt(Vec2D position, PointerButton button); + HitResult dragStart(Vec2D position, + float timeStamp = 0, + int pointerId = 0, + PointerButton button = PointerButton::primary); + HitResult dragEnd(Vec2D position, + float timeStamp = 0, + int pointerId = 0, + PointerButton button = PointerButton::primary); bool tryChangeState(); bool hitTest(Vec2D position) const; bool hitTestBounded(Vec2D position) const;
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp index cf905db..4208391 100644 --- a/include/rive/animation/state_machine_instance.hpp +++ b/include/rive/animation/state_machine_instance.hpp
@@ -85,7 +85,8 @@ HitResult updateListeners(Vec2D position, ListenerType hitListener, int pointerId = 0, - float timeStamp = 0); + float timeStamp = 0, + PointerButton button = PointerButton::primary); template <typename SMType, typename InstType> InstType* getNamedInput(const std::string& name) const; @@ -187,14 +188,23 @@ HitResult pointerMove(Vec2D position, float timeStamp = 0, int pointerId = 0) override; - HitResult pointerDown(Vec2D position, int pointerId = 0) override; - HitResult pointerUp(Vec2D position, 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); - HitResult dragEnd(Vec2D position, float timeStamp = 0, int pointerId = 0); + 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, @@ -213,6 +223,10 @@ 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; @@ -530,11 +544,13 @@ m_component(component), m_stateMachineInstance(stateMachineInstance) {} virtual ~HitComponent() {} - virtual HitResult processEvent(Vec2D position, - ListenerType hitType, - bool canHit, - float timeStamp = 0, - int pointerId = 0) = 0; + 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; @@ -565,6 +581,14 @@ /// 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)
diff --git a/include/rive/animation/state_machine_listener.hpp b/include/rive/animation/state_machine_listener.hpp index 7123bc7..86820e9 100644 --- a/include/rive/animation/state_machine_listener.hpp +++ b/include/rive/animation/state_machine_listener.hpp
@@ -3,6 +3,7 @@ #include "rive/animation/listener_invocation.hpp" #include "rive/generated/animation/state_machine_listener_base.hpp" #include "rive/listener_type.hpp" +#include "rive/pointer_button.hpp" #include "rive/span.hpp" #include "rive/animation/listener_types/listener_input_type.hpp" @@ -25,6 +26,9 @@ // return (ListenerType)listenerTypeValue(); // } virtual bool hasListener(ListenerType) const; + virtual bool hasListener(ListenerType, PointerButton) const; + // True if any pointer press listener type is bound to button. + virtual bool listensToButton(PointerButton) const; bool hasListeners(Span<const ListenerType> listenerTypes) const; // True if any listener type hit-tests a pointer against the target. bool hasPointerListeners() const; @@ -76,6 +80,20 @@ } #endif +#ifdef TESTING + void replaceListenerInputTypeForTesting( + size_t index, + std::unique_ptr<ListenerInputType> inputType) + { + m_listenerInputTypes[index] = std::move(inputType); + } + void addListenerInputTypeForTesting( + std::unique_ptr<ListenerInputType> inputType) + { + m_listenerInputTypes.push_back(std::move(inputType)); + } +#endif + private: void addAction(std::unique_ptr<ListenerAction>); void addListenerInputType(std::unique_ptr<ListenerInputType>);
diff --git a/include/rive/animation/state_machine_listener_single.hpp b/include/rive/animation/state_machine_listener_single.hpp index f972e02..8cc1a1e 100644 --- a/include/rive/animation/state_machine_listener_single.hpp +++ b/include/rive/animation/state_machine_listener_single.hpp
@@ -13,6 +13,15 @@ { return listenerTypeValue() == (int)listenerType; } + bool hasListener(ListenerType listenerType, + PointerButton button) const override + { + return button == PointerButton::primary && hasListener(listenerType); + } + bool listensToButton(PointerButton button) const override + { + return button == PointerButton::primary && hasPointerListeners(); + } void decodeViewModelPathIds(Span<const uint8_t> value) override; void copyViewModelPathIds( const StateMachineListenerSingleBase& object) override;
diff --git a/include/rive/animation/text_input_listener_group.hpp b/include/rive/animation/text_input_listener_group.hpp index 6a89c8d..cfa73b0 100644 --- a/include/rive/animation/text_input_listener_group.hpp +++ b/include/rive/animation/text_input_listener_group.hpp
@@ -27,6 +27,7 @@ Vec2D position, int pointerId, ListenerType hitEvent, + PointerButton button, bool canHit, float timeStamp, StateMachineInstance* stateMachineInstance) override;
diff --git a/include/rive/command_queue.hpp b/include/rive/command_queue.hpp index 73d84d1..96e4784 100644 --- a/include/rive/command_queue.hpp +++ b/include/rive/command_queue.hpp
@@ -8,6 +8,7 @@ #include "rive/input/focusable.hpp" #include "rive/refcnt.hpp" #include "rive/math/vec2d.hpp" +#include "rive/pointer_button.hpp" #include "rive/viewmodel/runtime/viewmodel_runtime.hpp" #include "rive/animation/semantic_listener_group.hpp" #include "rive/semantic/semantic_snapshot.hpp" @@ -968,6 +969,8 @@ Vec2D position; // the cursor position float scaleFactor = 1.0f; // scale factor for things like retina display int pointerId = 0; // stable pointer identifier for multitouch + PointerButton button = PointerButton::primary; // mouse button for + // down/up events }; // All pointer events will automatically convert between artboard and screen
diff --git a/include/rive/constraints/draggable_constraint.hpp b/include/rive/constraints/draggable_constraint.hpp index 6ce251f..54aab73 100644 --- a/include/rive/constraints/draggable_constraint.hpp +++ b/include/rive/constraints/draggable_constraint.hpp
@@ -112,6 +112,7 @@ Vec2D position, int pointerId, ListenerType hitEvent, + PointerButton button, bool canHit, float timeStamp, StateMachineInstance* stateMachineInstance) override;
diff --git a/include/rive/generated/animation/listener_types/listener_input_type_pointer_button_base.hpp b/include/rive/generated/animation/listener_types/listener_input_type_pointer_button_base.hpp new file mode 100644 index 0000000..aaab4c6 --- /dev/null +++ b/include/rive/generated/animation/listener_types/listener_input_type_pointer_button_base.hpp
@@ -0,0 +1,78 @@ +#ifndef _RIVE_LISTENER_INPUT_TYPE_POINTER_BUTTON_BASE_HPP_ +#define _RIVE_LISTENER_INPUT_TYPE_POINTER_BUTTON_BASE_HPP_ +#include "rive/animation/listener_types/listener_input_type.hpp" +#include "rive/core/field_types/core_uint_type.hpp" +namespace rive +{ +class ListenerInputTypePointerButtonBase : public ListenerInputType +{ +protected: + typedef ListenerInputType Super; + +public: + static const uint16_t typeKey = 155; + + /// Helper to quickly determine if a core object extends another without + /// RTTI at runtime. + bool isTypeOf(uint16_t typeKey) const override + { + switch (typeKey) + { + case ListenerInputTypePointerButtonBase::typeKey: + case ListenerInputTypeBase::typeKey: + return true; + default: + return false; + } + } + + uint16_t coreType() const override { return typeKey; } + + static const uint16_t pointerButtonValuePropertyKey = 468; + +protected: + uint8_t m_PointerButtonValue = 0; + +public: + inline uint8_t pointerButtonValue() const { return m_PointerButtonValue; } + void pointerButtonValue(uint8_t value) + { + if (m_PointerButtonValue == value) + { + return; + } + RIVE_EDITOR_CHANGING(pointerButtonValuePropertyKey, + &m_PointerButtonValue, + &value); + m_PointerButtonValue = value; + RIVE_EDITOR_CHANGED(pointerButtonValueChanged()); + notifyPropertyChanged(pointerButtonValuePropertyKey); + } + + Core* clone() const override; + void copy(const ListenerInputTypePointerButtonBase& object) + { + m_PointerButtonValue = object.m_PointerButtonValue; + ListenerInputType::copy(object); + } + + bool deserialize(uint16_t propertyKey, BinaryReader& reader) override + { + switch (propertyKey) + { + case pointerButtonValuePropertyKey: + m_PointerButtonValue = CoreUintType::deserialize(reader); + return true; + } + return ListenerInputType::deserialize(propertyKey, reader); + } + +protected: + virtual void pointerButtonValueChanged() {} +#ifdef WITH_RIVE_EDITOR +#include "editor_native/generated/animation/listener_types/listener_input_type_pointer_button_ext.inl" +#endif +}; +} // namespace rive + +#endif \ No newline at end of file
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp index 89eb73b..be03434 100644 --- a/include/rive/generated/core_registry.hpp +++ b/include/rive/generated/core_registry.hpp
@@ -50,6 +50,7 @@ #include "rive/animation/listener_types/listener_input_type_event.hpp" #include "rive/animation/listener_types/listener_input_type_gamepad.hpp" #include "rive/animation/listener_types/listener_input_type_keyboard.hpp" +#include "rive/animation/listener_types/listener_input_type_pointer_button.hpp" #include "rive/animation/listener_types/listener_input_type_semantic.hpp" #include "rive/animation/listener_types/listener_input_type_text.hpp" #include "rive/animation/listener_types/listener_input_type_viewmodel.hpp" @@ -659,6 +660,8 @@ return new ListenerInputTypeGamepad(); case ListenerInputTypeKeyboardBase::typeKey: return new ListenerInputTypeKeyboard(); + case ListenerInputTypePointerButtonBase::typeKey: + return new ListenerInputTypePointerButton(); case ListenerInputTypeTextBase::typeKey: return new ListenerInputTypeText(); case ListenerInputTypeSemanticBase::typeKey: @@ -1692,6 +1695,11 @@ case ListenerInputTypeBase::listenerTypeValuePropertyKey: object->as<ListenerInputTypeBase>()->listenerTypeValue(value); break; + case ListenerInputTypePointerButtonBase:: + pointerButtonValuePropertyKey: + object->as<ListenerInputTypePointerButtonBase>() + ->pointerButtonValue(value); + break; case ShapePaintBase::blendModeValuePropertyKey: object->as<ShapePaintBase>()->blendModeValue(value); break; @@ -3861,6 +3869,10 @@ return object->as<ElasticInterpolatorBase>()->easingValue(); case ListenerInputTypeBase::listenerTypeValuePropertyKey: return object->as<ListenerInputTypeBase>()->listenerTypeValue(); + case ListenerInputTypePointerButtonBase:: + pointerButtonValuePropertyKey: + return object->as<ListenerInputTypePointerButtonBase>() + ->pointerButtonValue(); case ShapePaintBase::blendModeValuePropertyKey: return object->as<ShapePaintBase>()->blendModeValue(); case ShapePaintBase::additiveAmountPropertyKey: @@ -5219,6 +5231,8 @@ propertyKeyPropertyKey: case ElasticInterpolatorBase::easingValuePropertyKey: case ListenerInputTypeBase::listenerTypeValuePropertyKey: + case ListenerInputTypePointerButtonBase:: + pointerButtonValuePropertyKey: case ShapePaintBase::blendModeValuePropertyKey: case ShapePaintBase::additiveAmountPropertyKey: case ColorChannelsBase::colorRedPropertyKey: @@ -6157,6 +6171,9 @@ return object->is<ElasticInterpolatorBase>(); case ListenerInputTypeBase::listenerTypeValuePropertyKey: return object->is<ListenerInputTypeBase>(); + case ListenerInputTypePointerButtonBase:: + pointerButtonValuePropertyKey: + return object->is<ListenerInputTypePointerButtonBase>(); case ShapePaintBase::blendModeValuePropertyKey: return object->is<ShapePaintBase>(); case ShapePaintBase::additiveAmountPropertyKey:
diff --git a/include/rive/listener_group.hpp b/include/rive/listener_group.hpp index e91f300..f44d724 100644 --- a/include/rive/listener_group.hpp +++ b/include/rive/listener_group.hpp
@@ -8,6 +8,7 @@ #include "rive/gesture_click_phase.hpp" #include "rive/listener_type.hpp" #include "rive/math/vec2d.hpp" +#include "rive/pointer_button.hpp" #include "rive/process_event_result.hpp" namespace rive @@ -27,6 +28,7 @@ // so a group-wide flag lets one pointer end another's drag. bool hasDragged = false; GestureClickPhase phase = GestureClickPhase::out; + PointerButton button = PointerButton::primary; Vec2D* previousPosition() { return &m_previousPosition; } private: @@ -50,10 +52,10 @@ /// once every group has been cancelled. virtual bool cancelPointer(int pointerId, Vec2D position, float timeStamp); /// Cancels every pointer this group is tracking, appending to dragEnded the - /// ids that still owe a dragEnd. + /// ids that still owe a dragEnd along with the button that dragged. void cancelPointers(Vec2D position, float timeStamp, - std::vector<int>& dragEnded); + std::vector<std::pair<int, PointerButton>>& dragEnded); virtual void enable(int pointerId = 0); virtual void disable(int pointerId = 0); bool isConsumed() { return m_isConsumed; } @@ -71,6 +73,7 @@ Vec2D position, int pointerId, ListenerType hitEvent, + PointerButton button, bool canHit, float timeStamp, StateMachineInstance* stateMachineInstance);
diff --git a/include/rive/pointer_button.hpp b/include/rive/pointer_button.hpp new file mode 100644 index 0000000..62922e0 --- /dev/null +++ b/include/rive/pointer_button.hpp
@@ -0,0 +1,12 @@ +#ifndef _RIVE_POINTER_BUTTON_HPP_ +#define _RIVE_POINTER_BUTTON_HPP_ +namespace rive +{ +enum class PointerButton : int +{ + primary = 0, + secondary = 1, + middle = 2, +}; +} +#endif
diff --git a/include/rive/scene.hpp b/include/rive/scene.hpp index 0b96c40..885b6d5 100644 --- a/include/rive/scene.hpp +++ b/include/rive/scene.hpp
@@ -7,6 +7,7 @@ #include "rive/animation/keyed_callback_reporter.hpp" #include "rive/core/field_types/core_callback_type.hpp" #include "rive/hit_result.hpp" +#include "rive/pointer_button.hpp" #include "rive/scroll_event.hpp" #include "rive/refcnt.hpp" #include <string> @@ -53,11 +54,16 @@ virtual void bindViewModelInstance( rcp<ViewModelInstance> viewModelInstance); - virtual HitResult pointerDown(Vec2D, int pointerId = 0); + virtual HitResult pointerDown( + Vec2D, + int pointerId = 0, + PointerButton button = PointerButton::primary); virtual HitResult pointerMove(Vec2D position, float timeStamp = 0, int pointerId = 0); - virtual HitResult pointerUp(Vec2D, int pointerId = 0); + virtual HitResult pointerUp(Vec2D, + int pointerId = 0, + PointerButton button = PointerButton::primary); virtual HitResult pointerExit(Vec2D, int pointerId = 0); /// Indirect scroll input -- a wheel, or a trackpad gesture the platform /// already classified as a scroll. Direct touch scrolling stays on the
diff --git a/include/rive/scripted/scripted_drawable.hpp b/include/rive/scripted/scripted_drawable.hpp index f593c62..ab99757 100644 --- a/include/rive/scripted/scripted_drawable.hpp +++ b/include/rive/scripted/scripted_drawable.hpp
@@ -105,7 +105,8 @@ ListenerType hitType, bool canHit, float timeStamp, - int pointerId) override; + int pointerId, + PointerButton button) override; HitResult processGamepadInvocation( const ListenerInvocation& invocation, ScriptedDrawable* alreadyDispatched) override;
diff --git a/src/animation/listener_invocation.cpp b/src/animation/listener_invocation.cpp index 129d221..c49a7fe 100644 --- a/src/animation/listener_invocation.cpp +++ b/src/animation/listener_invocation.cpp
@@ -10,7 +10,8 @@ Vec2D previousPosition, int pointerId, ListenerType hitEvent, - float timeStamp) + float timeStamp, + PointerButton button) { PointerInvocation p; p.position = position; @@ -18,6 +19,7 @@ p.pointerId = pointerId; p.hitEvent = hitEvent; p.timeStamp = timeStamp; + p.button = button; return ListenerInvocation(ListenerInvocationStorage(std::move(p))); }
diff --git a/src/animation/nested_state_machine.cpp b/src/animation/nested_state_machine.cpp index b4dd5b8..efb1b38 100644 --- a/src/animation/nested_state_machine.cpp +++ b/src/animation/nested_state_machine.cpp
@@ -93,20 +93,24 @@ return HitResult::none; } -HitResult NestedStateMachine::pointerDown(Vec2D position, int pointerId) +HitResult NestedStateMachine::pointerDown(Vec2D position, + int pointerId, + PointerButton button) { if (m_StateMachineInstance != nullptr) { - return m_StateMachineInstance->pointerDown(position, pointerId); + return m_StateMachineInstance->pointerDown(position, pointerId, button); } return HitResult::none; } -HitResult NestedStateMachine::pointerUp(Vec2D position, int pointerId) +HitResult NestedStateMachine::pointerUp(Vec2D position, + int pointerId, + PointerButton button) { if (m_StateMachineInstance != nullptr) { - return m_StateMachineInstance->pointerUp(position, pointerId); + return m_StateMachineInstance->pointerUp(position, pointerId, button); } return HitResult::none; } @@ -137,6 +141,12 @@ m_StateMachineInstance->hasScrollTargetAt(position); } +bool NestedStateMachine::listensToButtonAt(Vec2D position, PointerButton button) +{ + return m_StateMachineInstance != nullptr && + m_StateMachineInstance->listensToButtonAt(position, button); +} + bool NestedStateMachine::scrollOccludedAt(Vec2D position) { return m_StateMachineInstance != nullptr && @@ -168,25 +178,31 @@ HitResult NestedStateMachine::dragStart(Vec2D position, float timeStamp, - int pointerId) + int pointerId, + PointerButton button) { if (m_StateMachineInstance != nullptr) { return m_StateMachineInstance->dragStart(position, timeStamp, true, - pointerId); + pointerId, + button); } return HitResult::none; } HitResult NestedStateMachine::dragEnd(Vec2D position, float timeStamp, - int pointerId) + int pointerId, + PointerButton button) { if (m_StateMachineInstance != nullptr) { - return m_StateMachineInstance->dragEnd(position, timeStamp, pointerId); + return m_StateMachineInstance->dragEnd(position, + timeStamp, + pointerId, + button); } return HitResult::none; }
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp index a81c5c4..0e53403 100644 --- a/src/animation/state_machine_instance.cpp +++ b/src/animation/state_machine_instance.cpp
@@ -943,6 +943,28 @@ 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 @@ -1032,7 +1054,8 @@ ListenerType hitType, bool canHit, float timeStamp, - int pointerId) override + 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. @@ -1056,6 +1079,7 @@ position, pointerId, hitType, + button, canHit, timeStamp, m_stateMachineInstance) == @@ -1294,6 +1318,32 @@ 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 (!nestedArtboard->worldToLocal(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>(); @@ -1419,7 +1469,8 @@ ListenerType hitType, bool canHit, float timeStamp, - int pointerId) override + int pointerId, + PointerButton button) override { auto nestedArtboard = m_component->as<NestedArtboard>(); HitResult hitResult = HitResult::none; @@ -1448,12 +1499,14 @@ case ListenerType::down: hitResult = nestedStateMachine->pointerDown(nestedPosition, - pointerId); + pointerId, + button); break; case ListenerType::up: hitResult = nestedStateMachine->pointerUp(nestedPosition, - pointerId); + pointerId, + button); break; case ListenerType::move: hitResult = @@ -1464,12 +1517,14 @@ case ListenerType::dragStart: nestedStateMachine->dragStart(nestedPosition, timeStamp, - pointerId); + pointerId, + button); break; case ListenerType::dragEnd: nestedStateMachine->dragEnd(nestedPosition, timeStamp, - pointerId); + pointerId, + button); break; case ListenerType::exit: hitResult = @@ -1685,6 +1740,32 @@ 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>(); @@ -1810,7 +1891,8 @@ ListenerType hitType, bool canHit, float timeStamp, - int pointerId) override + int pointerId, + PointerButton button) override { auto componentList = m_component->as<ArtboardComponentList>(); HitResult hitResult = HitResult::none; @@ -1840,12 +1922,14 @@ case ListenerType::down: itemHitResult = stateMachine->pointerDown(listPosition, - pointerId); + pointerId, + button); break; case ListenerType::up: itemHitResult = stateMachine->pointerUp(listPosition, - pointerId); + pointerId, + button); break; case ListenerType::move: itemHitResult = @@ -1862,10 +1946,14 @@ stateMachine->dragStart(listPosition, 0, true, - pointerId); + pointerId, + button); break; case ListenerType::dragEnd: - stateMachine->dragEnd(listPosition, 0, pointerId); + stateMachine->dragEnd(listPosition, + 0, + pointerId, + button); break; case ListenerType::enter: case ListenerType::event: @@ -2207,7 +2295,8 @@ HitResult StateMachineInstance::updateListeners(Vec2D position, ListenerType hitType, int pointerId, - float timeStamp) + float timeStamp, + PointerButton button) { // Listener hit-testing runs in content space, so undo the frame origin and // the artboard's own rotation/scale first. @@ -2226,7 +2315,7 @@ } // Drag ends owed by cancellation, dispatched once the pass below has had a // chance to emit its hover exits. - std::vector<int> dragEnded; + std::vector<std::pair<int, PointerButton>> dragEnded; if (contentsCollapsed) { // canHit alone won't do this: it marks a target as occluded, and an @@ -2266,7 +2355,8 @@ hitType, !hitOpaque && !contentsCollapsed, timeStamp, - pointerId); + pointerId, + button); if (hitResult != HitResult::none) { hitSomething = true; @@ -2277,9 +2367,9 @@ } } // Hover exits have been emitted, so it's safe to let dragEnd re-enter now. - for (auto endedPointerId : dragEnded) + for (auto& ended : dragEnded) { - dragEnd(position, timeStamp, endedPointerId); + dragEnd(position, timeStamp, ended.first, ended.second); } // Finally release events that are complete if (hitType == ListenerType::exit) @@ -2346,13 +2436,17 @@ { return updateListeners(position, ListenerType::move, id, timeStamp); } -HitResult StateMachineInstance::pointerDown(Vec2D position, int id) +HitResult StateMachineInstance::pointerDown(Vec2D position, + int id, + PointerButton button) { - return updateListeners(position, ListenerType::down, id); + return updateListeners(position, ListenerType::down, id, 0, button); } -HitResult StateMachineInstance::pointerUp(Vec2D position, int id) +HitResult StateMachineInstance::pointerUp(Vec2D position, + int id, + PointerButton button) { - return updateListeners(position, ListenerType::up, id); + return updateListeners(position, ListenerType::up, id, 0, button); } HitResult StateMachineInstance::pointerExit(Vec2D position, int id) { @@ -2361,21 +2455,28 @@ HitResult StateMachineInstance::dragStart(Vec2D position, float timeStamp, bool disablePointer, - int pointerId) + int pointerId, + PointerButton button) { if (disablePointer) { disablePointerEvents(pointerId); } - auto hit = updateListeners(position, ListenerType::dragStart, pointerId); + auto hit = updateListeners(position, + ListenerType::dragStart, + pointerId, + 0, + button); return hit; } HitResult StateMachineInstance::dragEnd(Vec2D position, float timeStamp, - int pointerId) + int pointerId, + PointerButton button) { enablePointerEvents(pointerId); - auto hit = updateListeners(position, ListenerType::dragEnd, pointerId); + auto hit = + updateListeners(position, ListenerType::dragEnd, pointerId, 0, button); pointerMove(position, timeStamp, pointerId); return hit; } @@ -2499,6 +2600,27 @@ return false; } +bool StateMachineInstance::listensToButtonAt(Vec2D position, + PointerButton button) +{ + if (!mapToContentSpace(position)) + { + return false; + } + 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))
diff --git a/src/animation/state_machine_listener.cpp b/src/animation/state_machine_listener.cpp index 5fddcdc..23a3622 100644 --- a/src/animation/state_machine_listener.cpp +++ b/src/animation/state_machine_listener.cpp
@@ -52,6 +52,77 @@ return false; } +static bool listenerTypeHasButton(ListenerType listenerType) +{ + switch (listenerType) + { + case ListenerType::down: + case ListenerType::up: + case ListenerType::click: + case ListenerType::drag: + case ListenerType::dragStart: + case ListenerType::dragEnd: + return true; + default: + return false; + } +} + +bool StateMachineListener::hasListener(ListenerType listenerType, + PointerButton button) const +{ + if (!listenerTypeHasButton(listenerType)) + { + return hasListener(listenerType); + } + for (auto& listenerInputType : m_listenerInputTypes) + { + if (listenerInputType->listenerTypeValue() == (int)listenerType && + listenerInputType->pointerButton() == button) + { + return true; + } + } +#ifdef WITH_RIVE_EDITOR + for (auto* listenerInputType : m_editorListenerInputTypes) + { + if (listenerInputType != nullptr && + listenerInputType->listenerTypeValue() == (int)listenerType && + listenerInputType->pointerButton() == button) + { + return true; + } + } +#endif + return false; +} + +bool StateMachineListener::listensToButton(PointerButton button) const +{ + for (auto& listenerInputType : m_listenerInputTypes) + { + if (listenerTypeHasButton( + (ListenerType)listenerInputType->listenerTypeValue()) && + listenerInputType->pointerButton() == button) + { + return true; + } + } +#ifdef WITH_RIVE_EDITOR + for (auto* listenerInputType : m_editorListenerInputTypes) + { + if (listenerInputType != nullptr && + listenerTypeHasButton( + (ListenerType)listenerInputType->listenerTypeValue()) && + listenerInputType->pointerButton() == button) + { + return true; + } + } +#endif + return false; +} + bool StateMachineListener::hasListeners( Span<const ListenerType> listenerTypes) const {
diff --git a/src/animation/text_input_listener_group.cpp b/src/animation/text_input_listener_group.cpp index a8cc8e7..9f1876e 100644 --- a/src/animation/text_input_listener_group.cpp +++ b/src/animation/text_input_listener_group.cpp
@@ -50,10 +50,16 @@ Vec2D position, int pointerId, ListenerType hitEvent, + PointerButton button, bool canHit, float timeStamp, StateMachineInstance* stateMachineInstance) { + if ((hitEvent == ListenerType::down || hitEvent == ListenerType::up) && + button != PointerButton::primary) + { + return ProcessEventResult::none; + } // We implement our own click phase tracking instead of calling base class // because base class processEvent accesses m_listener which is nullptr auto* pData = pointerData(pointerId);
diff --git a/src/command_server.cpp b/src/command_server.cpp index 8284eea..169f65c 100644 --- a/src/command_server.cpp +++ b/src/command_server.cpp
@@ -4280,7 +4280,8 @@ stateMachineWrapper->m_mutex); auto hitResult = stateMachineWrapper->instance->pointerDown( position, - pointerEvent.pointerId); + pointerEvent.pointerId, + pointerEvent.button); // Process down-driven state changes before a following // pointer up. if (hitResult != HitResult::none) @@ -4319,7 +4320,8 @@ stateMachineWrapper->m_mutex); auto hitResult = stateMachineWrapper->instance->pointerUp( position, - pointerEvent.pointerId); + pointerEvent.pointerId, + pointerEvent.button); // Process up-driven state changes before any following // pointer exit. if (hitResult != HitResult::none)
diff --git a/src/constraints/draggable_constraint.cpp b/src/constraints/draggable_constraint.cpp index 8142df3..21b7e1b 100644 --- a/src/constraints/draggable_constraint.cpp +++ b/src/constraints/draggable_constraint.cpp
@@ -57,16 +57,23 @@ Vec2D position, int pointerId, ListenerType hitEvent, + PointerButton button, bool canHit, float timeStamp, StateMachineInstance* stateMachineInstance) { + if ((hitEvent == ListenerType::down || hitEvent == ListenerType::up) && + button != PointerButton::primary) + { + return ProcessEventResult::none; + } auto pointer = pointerData(pointerId); auto prevPhase = pointer->phase; ListenerGroup::processEvent(component, position, pointerId, hitEvent, + button, canHit, timeStamp, stateMachineInstance);
diff --git a/src/generated/animation/listener_types/listener_input_type_pointer_button_base.cpp b/src/generated/animation/listener_types/listener_input_type_pointer_button_base.cpp new file mode 100644 index 0000000..81a3d88 --- /dev/null +++ b/src/generated/animation/listener_types/listener_input_type_pointer_button_base.cpp
@@ -0,0 +1,11 @@ +#include "rive/generated/animation/listener_types/listener_input_type_pointer_button_base.hpp" +#include "rive/animation/listener_types/listener_input_type_pointer_button.hpp" + +using namespace rive; + +Core* ListenerInputTypePointerButtonBase::clone() const +{ + auto cloned = new ListenerInputTypePointerButton(); + cloned->copy(*this); + return cloned; +}
diff --git a/src/listener_group.cpp b/src/listener_group.cpp index 7b26b9d..6daa25e 100644 --- a/src/listener_group.cpp +++ b/src/listener_group.cpp
@@ -97,20 +97,22 @@ return wasDragging; } -void ListenerGroup::cancelPointers(Vec2D position, - float timeStamp, - std::vector<int>& dragEnded) +void ListenerGroup::cancelPointers( + Vec2D position, + float timeStamp, + std::vector<std::pair<int, PointerButton>>& dragEnded) { for (auto& entry : m_pointers) { + auto button = entry.second->button; if (!cancelPointer(entry.first, position, timeStamp)) { continue; } bool alreadyEnded = false; - for (auto id : dragEnded) + for (auto& ended : dragEnded) { - if (id == entry.first) + if (ended.first == entry.first) { alreadyEnded = true; break; @@ -118,7 +120,7 @@ } if (!alreadyEnded) { - dragEnded.push_back(entry.first); + dragEnded.emplace_back(entry.first, button); } } } @@ -182,6 +184,7 @@ Vec2D position, int pointerId, ListenerType hitEvent, + PointerButton button, bool canHit, float timeStamp, StateMachineInstance* stateMachineInstance) @@ -219,17 +222,24 @@ { if (hitEvent == ListenerType::down) { - pointer->phase = GestureClickPhase::down; + if (pointer->phase != GestureClickPhase::down) + { + pointer->phase = GestureClickPhase::down; + pointer->button = button; + } } else if (hitEvent == ListenerType::up && - pointer->phase == GestureClickPhase::down) + pointer->phase == GestureClickPhase::down && + pointer->button == button) { pointer->phase = GestureClickPhase::clicked; } } else { - if (hitEvent == ListenerType::down || hitEvent == ListenerType::up) + if ((hitEvent == ListenerType::down || hitEvent == ListenerType::up) && + (pointer->phase != GestureClickPhase::down || + pointer->button == button)) { pointer->phase = GestureClickPhase::out; } @@ -239,12 +249,16 @@ pointer->phase == GestureClickPhase::out) && pointer->hasDragged) { - stateMachineInstance->dragEnd(position, timeStamp, pointerId); + stateMachineInstance->dragEnd(position, + timeStamp, + pointerId, + pointer->button); pointer->hasDragged = false; } auto _listener = listener(); bool shouldPerformChanges = false; auto listenerTypeMatched = hitEvent; + auto buttonMatched = button; // Always update hover states regardless of which specific listener type // we're trying to trigger. // If hover has changed and: @@ -269,12 +283,13 @@ // - the event type matches the listener type and it is hovering the // group if (pointer->phase == GestureClickPhase::clicked && - _listener->hasListener(ListenerType::click)) + _listener->hasListener(ListenerType::click, pointer->button)) { shouldPerformChanges = true; listenerTypeMatched = ListenerType::click; + buttonMatched = pointer->button; } - else if (isGroupHovered && _listener->hasListener(hitEvent)) + else if (isGroupHovered && _listener->hasListener(hitEvent, button)) { shouldPerformChanges = true; } @@ -283,17 +298,19 @@ // - the clickPhase is down // - the pointer type is move if (pointer->phase == GestureClickPhase::down && - _listener->hasListener(ListenerType::drag) && + _listener->hasListener(ListenerType::drag, pointer->button) && hitEvent == ListenerType::move) { shouldPerformChanges = true; listenerTypeMatched = ListenerType::drag; + buttonMatched = pointer->button; if (!pointer->hasDragged) { stateMachineInstance->dragStart(position, timeStamp, false, - pointerId); + pointerId, + pointer->button); pointer->hasDragged = true; } } @@ -306,7 +323,8 @@ Vec2D(previousPosition->x, previousPosition->y), pointerId, listenerTypeMatched, - timeStamp)); + timeStamp, + buttonMatched)); stateMachineInstance->markNeedsAdvance(); consume();
diff --git a/src/scene.cpp b/src/scene.cpp index 6f664ca..385619d 100644 --- a/src/scene.cpp +++ b/src/scene.cpp
@@ -15,12 +15,18 @@ void Scene::draw(Renderer* renderer) { m_artboardInstance->draw(renderer); } -HitResult Scene::pointerDown(Vec2D, int) { return HitResult::none; } +HitResult Scene::pointerDown(Vec2D, int, PointerButton) +{ + return HitResult::none; +} HitResult Scene::pointerMove(Vec2D, float timeStamp, int) { return HitResult::none; } -HitResult Scene::pointerUp(Vec2D, int) { return HitResult::none; } +HitResult Scene::pointerUp(Vec2D, int, PointerButton) +{ + return HitResult::none; +} HitResult Scene::pointerExit(Vec2D, int) { return HitResult::none; } HitResult Scene::pointerScroll(Vec2D, const ScrollEvent&, float, int) {
diff --git a/src/scripted/scripted_drawable.cpp b/src/scripted/scripted_drawable.cpp index a491259..df5171f 100644 --- a/src/scripted/scripted_drawable.cpp +++ b/src/scripted/scripted_drawable.cpp
@@ -96,7 +96,8 @@ ListenerType hitType, bool canHit, float timeStamp, - int pointerId) + int pointerId, + PointerButton button) { HitResult hitResult = HitResult::none; auto scriptAsset = m_drawable->scriptAsset(); @@ -196,7 +197,8 @@ ListenerType hitType, bool canHit, float timeStamp, - int pointerId) + int pointerId, + PointerButton button) { return HitResult::none; }
diff --git a/tests/unit_tests/assets/pointer_button_nested.riv b/tests/unit_tests/assets/pointer_button_nested.riv new file mode 100644 index 0000000..9f2fd57 --- /dev/null +++ b/tests/unit_tests/assets/pointer_button_nested.riv Binary files differ
diff --git a/tests/unit_tests/assets/pointer_button_secondary.riv b/tests/unit_tests/assets/pointer_button_secondary.riv new file mode 100644 index 0000000..9446d58 --- /dev/null +++ b/tests/unit_tests/assets/pointer_button_secondary.riv Binary files differ
diff --git a/tests/unit_tests/runtime/command_queue_test.cpp b/tests/unit_tests/runtime/command_queue_test.cpp index feedb12..73d7312 100644 --- a/tests/unit_tests/runtime/command_queue_test.cpp +++ b/tests/unit_tests/runtime/command_queue_test.cpp
@@ -5714,6 +5714,43 @@ serverThread.join(); } +TEST_CASE("pointer buttons", "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + // A full-artboard shape with a secondary click listener that sets the + // "clicked" bool, exported by rive_core's pointer_button_export_test.dart. + std::ifstream stream("assets/pointer_button_secondary.riv", + std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {})); + ArtboardHandle artboardHandle = + commandQueue->instantiateDefaultArtboard(fileHandle); + StateMachineHandle smHandle = + commandQueue->instantiateDefaultStateMachine(artboardHandle); + commandQueue->advanceStateMachine(smHandle, 0.0f); + + Vec2D center(250.0f, 250.0f); + checkStateMachineBool(commandQueue, smHandle, "clicked", false); + + // The default button is primary, which this listener ignores. + commandQueue->pointerDown(smHandle, {.position = center}); + commandQueue->pointerUp(smHandle, {.position = center}); + checkStateMachineBool(commandQueue, smHandle, "clicked", false); + + commandQueue->pointerDown( + smHandle, + {.position = center, .button = PointerButton::secondary}); + commandQueue->pointerUp( + smHandle, + {.position = center, .button = PointerButton::secondary}); + checkStateMachineBool(commandQueue, smHandle, "clicked", true); + + commandQueue->disconnect(); + serverThread.join(); +} + // Captures the view model update emitted by rapid_pointer_events.riv when its // state machine reaches the down-driven state. class RapidPointerViewModelListener
diff --git a/tests/unit_tests/runtime/pointer_button_test.cpp b/tests/unit_tests/runtime/pointer_button_test.cpp new file mode 100644 index 0000000..978cc50 --- /dev/null +++ b/tests/unit_tests/runtime/pointer_button_test.cpp
@@ -0,0 +1,546 @@ +#include <rive/animation/listener_types/listener_input_type.hpp> +#include <rive/animation/listener_types/listener_input_type_pointer_button.hpp> +#include <rive/animation/state_machine_instance.hpp> +#include <rive/animation/state_machine_listener.hpp> +#include <rive/animation/linear_animation_instance.hpp> +#include <rive/animation/nested_state_machine.hpp> +#include <rive/animation/state_machine_input_instance.hpp> +#include <rive/constraints/scrolling/scroll_constraint.hpp> +#include <rive/nested_artboard.hpp> +#include <rive/pointer_button.hpp> +#include <rive/text/text_input.hpp> +#include <rive/viewmodel/viewmodel_instance_number.hpp> +#include "rive_file_reader.hpp" + +#include <catch.hpp> + +using namespace rive; + +namespace +{ +// click_event's "art-1" has two 200x200 rects under one group with a single +// legacy (StateMachineListenerSingle) click listener that reports an event. +struct ClickScene +{ + rcp<File> file; + std::unique_ptr<ArtboardInstance> artboard; + std::unique_ptr<StateMachineInstance> stateMachine; + + ClickScene() + { + file = ReadRiveFile("assets/click_event.riv"); + auto sourceArtboard = file->artboard("art-1"); + auto machine = sourceArtboard->stateMachine("sm-1"); + REQUIRE(machine != nullptr); + artboard = sourceArtboard->instance(); + stateMachine = + std::make_unique<StateMachineInstance>(machine, artboard.get()); + stateMachine->advance(0.0f); + artboard->advance(0.0f); + stateMachine->advance(0.0f); + REQUIRE(stateMachine->hitComponentsCount() == 2); + REQUIRE(stateMachine->reportedEventCount() == 0); + } + + void down(PointerButton button, Vec2D position = Vec2D(75.0f, 75.0f)) + { + stateMachine->pointerDown(position, 0, button); + } + + void up(PointerButton button, Vec2D position = Vec2D(75.0f, 75.0f)) + { + stateMachine->pointerUp(position, 0, button); + } + + size_t events() const { return stateMachine->reportedEventCount(); } +}; + +// listener_input_values (see assets/rml/listener_input_values.rml) has a +// full-artboard shape whose first listener is a ListenerInputType "down" that +// writes the pointer position into the px/py view model numbers. Passing a +// button swaps that input for a ListenerInputTypePointerButton bound to it. +struct DownScene +{ + rcp<File> file; + std::unique_ptr<ArtboardInstance> artboard; + std::unique_ptr<StateMachineInstance> stateMachine; + rcp<ViewModelInstance> vmi; + + using Input = std::pair<ListenerType, PointerButton>; + + DownScene(std::vector<Input> inputs = {}) + { + file = ReadRiveFile("assets/listener_input_values.riv"); + auto machine = file->artboard()->stateMachine(0); + REQUIRE(machine != nullptr); + REQUIRE(machine->listenerCount() > 0); + auto listener = machine->listener(0); + REQUIRE(listener->listenerInputTypeCount() == 1); + auto inputType = listener->listenerInputType(0); + REQUIRE(inputType->listenerTypeValue() == (uint32_t)ListenerType::down); + REQUIRE(inputType->pointerButton() == PointerButton::primary); + for (size_t i = 0; i < inputs.size(); i++) + { + auto buttonInput = + std::make_unique<ListenerInputTypePointerButton>(); + buttonInput->listenerTypeValue((uint32_t)inputs[i].first); + buttonInput->pointerButtonValue((uint8_t)inputs[i].second); + auto mutableListener = const_cast<StateMachineListener*>(listener); + if (i == 0) + { + mutableListener->replaceListenerInputTypeForTesting( + 0, + std::move(buttonInput)); + } + else + { + mutableListener->addListenerInputTypeForTesting( + std::move(buttonInput)); + } + REQUIRE(listener->listenerInputType(i)->pointerButton() == + inputs[i].second); + } + + artboard = file->artboardDefault(); + stateMachine = artboard->stateMachineAt(0); + vmi = file->createDefaultViewModelInstance(artboard.get()); + REQUIRE(vmi != nullptr); + stateMachine->bindViewModelInstance(vmi); + advance(); + } + + void advance() { stateMachine->advanceAndApply(0.016f); } + + void down(PointerButton button, Vec2D position) + { + stateMachine->pointerDown(position, 0, button); + advance(); + } + + void move(Vec2D position) + { + stateMachine->pointerMove(position); + advance(); + } + + void up(PointerButton button, Vec2D position) + { + stateMachine->pointerUp(position, 0, button); + advance(); + } + + float px() + { + auto value = vmi->propertyValue("px"); + REQUIRE(value != nullptr); + return value->as<ViewModelInstanceNumber>()->propertyValue(); + } +}; +} // namespace + +TEST_CASE("plain listener input types are primary button listeners", + "[pointer_button]") +{ + auto file = ReadRiveFile("assets/listener_input_values.riv"); + auto machine = file->artboard()->stateMachine(0); + auto listener = machine->listener(0); + REQUIRE(listener->listenerInputTypeCount() == 1); + REQUIRE(listener->listenerInputType(0)->pointerButton() == + PointerButton::primary); + REQUIRE(listener->hasListener(ListenerType::down)); + REQUIRE(listener->hasListener(ListenerType::down, PointerButton::primary)); + REQUIRE_FALSE( + listener->hasListener(ListenerType::down, PointerButton::secondary)); + REQUIRE_FALSE( + listener->hasListener(ListenerType::down, PointerButton::middle)); +} + +TEST_CASE("pointer button input types listen to their button only", + "[pointer_button]") +{ + ListenerInputTypePointerButton input; + input.listenerTypeValue((uint32_t)ListenerType::click); + REQUIRE(input.is<ListenerInputType>()); + REQUIRE(input.pointerButton() == PointerButton::primary); + input.pointerButtonValue((uint8_t)PointerButton::secondary); + REQUIRE(input.pointerButton() == PointerButton::secondary); + input.pointerButtonValue((uint8_t)PointerButton::middle); + REQUIRE(input.pointerButton() == PointerButton::middle); +} + +TEST_CASE("legacy single listeners only respond to the primary button", + "[pointer_button]") +{ + auto file = ReadRiveFile("assets/click_event.riv"); + auto machine = file->artboard("art-1")->stateMachine("sm-1"); + auto listener = machine->listener(0); + REQUIRE(listener->listenerInputTypeCount() == 0); + REQUIRE(listener->hasListener(ListenerType::click)); + REQUIRE(listener->hasListener(ListenerType::click, PointerButton::primary)); + REQUIRE_FALSE( + listener->hasListener(ListenerType::click, PointerButton::secondary)); +} + +TEST_CASE("primary down listener ignores other buttons", "[pointer_button]") +{ + DownScene scene; + REQUIRE(scene.px() == 0.0f); + + scene.down(PointerButton::secondary, Vec2D(120.0f, 310.0f)); + CHECK(scene.px() == 0.0f); + + scene.down(PointerButton::middle, Vec2D(120.0f, 310.0f)); + CHECK(scene.px() == 0.0f); + + scene.down(PointerButton::primary, Vec2D(120.0f, 310.0f)); + CHECK(scene.px() == 120.0f); +} + +TEST_CASE("secondary down listener fires only on the secondary button", + "[pointer_button]") +{ + DownScene scene({{ListenerType::down, PointerButton::secondary}}); + REQUIRE(scene.px() == 0.0f); + + scene.down(PointerButton::primary, Vec2D(120.0f, 310.0f)); + CHECK(scene.px() == 0.0f); + + scene.down(PointerButton::middle, Vec2D(120.0f, 310.0f)); + CHECK(scene.px() == 0.0f); + + scene.down(PointerButton::secondary, Vec2D(120.0f, 310.0f)); + CHECK(scene.px() == 120.0f); + + scene.down(PointerButton::secondary, Vec2D(200.0f, 310.0f)); + CHECK(scene.px() == 200.0f); +} + +TEST_CASE("listensToButtonAt reports the buttons wanted under a point", + "[pointer_button]") +{ + Vec2D inside(120.0f, 310.0f); + Vec2D outside(-50.0f, -50.0f); + + DownScene primary; + CHECK(primary.stateMachine->listensToButtonAt(inside, + PointerButton::primary)); + CHECK_FALSE( + primary.stateMachine->listensToButtonAt(inside, + PointerButton::secondary)); + CHECK_FALSE( + primary.stateMachine->listensToButtonAt(outside, + PointerButton::primary)); + + DownScene scene({{ListenerType::down, PointerButton::secondary}}); + CHECK(scene.stateMachine->listensToButtonAt(inside, + PointerButton::secondary)); + CHECK_FALSE( + scene.stateMachine->listensToButtonAt(inside, PointerButton::primary)); + CHECK_FALSE( + scene.stateMachine->listensToButtonAt(inside, PointerButton::middle)); + CHECK_FALSE( + scene.stateMachine->listensToButtonAt(outside, + PointerButton::secondary)); + + ClickScene legacy; + CHECK(legacy.stateMachine->listensToButtonAt(Vec2D(75.0f, 75.0f), + PointerButton::primary)); + CHECK_FALSE( + legacy.stateMachine->listensToButtonAt(Vec2D(75.0f, 75.0f), + PointerButton::secondary)); + CHECK_FALSE(legacy.stateMachine->listensToButtonAt(Vec2D(300.0f, 75.0f), + PointerButton::primary)); +} + +TEST_CASE("synthetic drag events carry the dragging button", "[pointer_button]") +{ + Vec2D start(100.0f, 100.0f); + Vec2D dragged(150.0f, 150.0f); + Vec2D released(200.0f, 200.0f); + + SECTION("a secondary drag end reaches a secondary drag end listener") + { + DownScene scene({{ListenerType::drag, PointerButton::secondary}, + {ListenerType::dragEnd, PointerButton::secondary}}); + scene.down(PointerButton::secondary, start); + CHECK(scene.px() == 0.0f); + scene.move(dragged); + CHECK(scene.px() == 150.0f); + scene.up(PointerButton::secondary, released); + CHECK(scene.px() == 200.0f); + } + + SECTION("a secondary drag end skips a primary drag end listener") + { + DownScene scene({{ListenerType::drag, PointerButton::secondary}, + {ListenerType::dragEnd, PointerButton::primary}}); + scene.down(PointerButton::secondary, start); + scene.move(dragged); + CHECK(scene.px() == 150.0f); + scene.up(PointerButton::secondary, released); + CHECK(scene.px() == 150.0f); + } + + SECTION("a secondary drag start reaches a secondary drag start listener") + { + DownScene scene({{ListenerType::drag, PointerButton::secondary}, + {ListenerType::dragStart, PointerButton::secondary}}); + scene.down(PointerButton::secondary, start); + scene.move(dragged); + CHECK(scene.px() == 150.0f); + scene.up(PointerButton::secondary, released); + CHECK(scene.px() == 150.0f); + } + + SECTION("a primary drag still drives primary drag listeners") + { + DownScene scene({{ListenerType::drag, PointerButton::primary}, + {ListenerType::dragEnd, PointerButton::primary}}); + scene.down(PointerButton::primary, start); + scene.move(dragged); + CHECK(scene.px() == 150.0f); + scene.up(PointerButton::primary, released); + CHECK(scene.px() == 200.0f); + } + + SECTION("a primary drag ignores secondary drag listeners") + { + DownScene scene({{ListenerType::drag, PointerButton::secondary}, + {ListenerType::dragEnd, PointerButton::secondary}}); + scene.down(PointerButton::primary, start); + scene.move(dragged); + scene.up(PointerButton::primary, released); + CHECK(scene.px() == 0.0f); + } +} + +TEST_CASE("primary click listener ignores other buttons", "[pointer_button]") +{ + ClickScene scene; + + scene.down(PointerButton::primary); + scene.up(PointerButton::primary); + REQUIRE(scene.events() == 1); + + scene.down(PointerButton::secondary); + scene.up(PointerButton::secondary); + REQUIRE(scene.events() == 1); + + scene.down(PointerButton::middle); + scene.up(PointerButton::middle); + REQUIRE(scene.events() == 1); + + scene.down(PointerButton::primary); + scene.up(PointerButton::primary); + REQUIRE(scene.events() == 2); +} + +TEST_CASE("a click needs the same button down and up", "[pointer_button]") +{ + ClickScene scene; + + scene.down(PointerButton::primary); + scene.up(PointerButton::secondary); + REQUIRE(scene.events() == 0); + + scene.up(PointerButton::primary); + REQUIRE(scene.events() == 1); + + scene.down(PointerButton::secondary); + scene.up(PointerButton::primary); + REQUIRE(scene.events() == 1); +} + +TEST_CASE("a held press survives another button pressed over it", + "[pointer_button]") +{ + ClickScene scene; + + scene.down(PointerButton::primary); + scene.down(PointerButton::secondary); + scene.up(PointerButton::secondary); + REQUIRE(scene.events() == 0); + + scene.up(PointerButton::primary); + REQUIRE(scene.events() == 1); +} + +TEST_CASE("releasing another button outside keeps the held press", + "[pointer_button]") +{ + ClickScene scene; + + scene.down(PointerButton::primary); + scene.up(PointerButton::secondary, Vec2D(300.0f, 75.0f)); + scene.up(PointerButton::primary); + REQUIRE(scene.events() == 1); + + scene.down(PointerButton::primary); + scene.up(PointerButton::primary, Vec2D(300.0f, 75.0f)); + scene.up(PointerButton::primary); + REQUIRE(scene.events() == 1); +} + +// pointer_button_secondary.riv is exported by rive_core's +// pointer_button_export_test.dart: a full-artboard shape with a secondary click +// listener that reports "Boom" and sets the "clicked" bool input. +TEST_CASE("an exported secondary click fixture loads as a pointer button input", + "[pointer_button]") +{ + auto file = ReadRiveFile("assets/pointer_button_secondary.riv"); + auto machine = file->artboard()->stateMachine(0); + REQUIRE(machine != nullptr); + REQUIRE(machine->listenerCount() == 1); + auto listener = machine->listener(0); + REQUIRE(listener->listenerInputTypeCount() == 1); + auto input = listener->listenerInputType(0); + REQUIRE(input->is<ListenerInputTypePointerButton>()); + REQUIRE(input->pointerButton() == PointerButton::secondary); + REQUIRE(input->listenerTypeValue() == (uint32_t)ListenerType::click); + + auto artboard = file->artboardDefault(); + auto stateMachine = artboard->stateMachineAt(0); + stateMachine->advanceAndApply(0.0f); + auto clicked = stateMachine->getBool("clicked"); + REQUIRE(clicked != nullptr); + REQUIRE(clicked->value() == false); + + Vec2D center(250.0f, 250.0f); + stateMachine->pointerDown(center, 0, PointerButton::primary); + stateMachine->pointerUp(center, 0, PointerButton::primary); + CHECK(stateMachine->reportedEventCount() == 0); + CHECK(clicked->value() == false); + + stateMachine->pointerDown(center, 0, PointerButton::secondary); + stateMachine->pointerUp(center, 0, PointerButton::secondary); + CHECK(stateMachine->reportedEventCount() == 1); + CHECK(clicked->value() == true); +} + +// pointer_button_nested.riv is exported by the same Dart test: a host artboard +// holding "Inner" at (100, 100), whose full shape has a secondary click +// listener ("Boom") and a secondary drag + dragEnd listener ("Drag"). +TEST_CASE("nested pointer button listeners dispatch through the host", + "[pointer_button]") +{ + auto file = ReadRiveFile("assets/pointer_button_nested.riv"); + auto artboard = file->artboardDefault(); + auto stateMachine = artboard->stateMachineAt(0); + stateMachine->advanceAndApply(0.0f); + + auto nestedArtboards = artboard->find<NestedArtboard>(); + REQUIRE(nestedArtboards.size() == 1); + auto nestedArtboard = nestedArtboards[0]; + REQUIRE(nestedArtboard->nestedAnimations().size() == 1); + auto inner = nestedArtboard->nestedAnimations()[0] + ->as<NestedStateMachine>() + ->stateMachineInstance(); + REQUIRE(inner != nullptr); + + Vec2D overInner(200.0f, 200.0f); + Vec2D besideInner(20.0f, 20.0f); + CHECK(stateMachine->listensToButtonAt(overInner, PointerButton::secondary)); + CHECK_FALSE( + stateMachine->listensToButtonAt(overInner, PointerButton::primary)); + CHECK_FALSE( + stateMachine->listensToButtonAt(besideInner, PointerButton::secondary)); + + stateMachine->pointerDown(overInner, 0, PointerButton::primary); + stateMachine->pointerUp(overInner, 0, PointerButton::primary); + CHECK(inner->reportedEventCount() == 0); + + stateMachine->pointerDown(overInner, 0, PointerButton::secondary); + stateMachine->pointerUp(overInner, 0, PointerButton::secondary); + CHECK(inner->reportedEventCount() == 1); + stateMachine->advanceAndApply(0.016f); + + SECTION("a secondary drag reaches the nested drag listeners") + { + stateMachine->pointerDown(Vec2D(150.0f, 150.0f), + 0, + PointerButton::secondary); + stateMachine->pointerMove(Vec2D(170.0f, 170.0f)); + auto afterMove = inner->reportedEventCount(); + CHECK(afterMove >= 1); + stateMachine->pointerUp(Vec2D(180.0f, 180.0f), + 0, + PointerButton::secondary); + CHECK(inner->reportedEventCount() > afterMove); + } + + SECTION("a primary drag leaves the nested secondary drag listeners alone") + { + stateMachine->pointerDown(Vec2D(150.0f, 150.0f), + 0, + PointerButton::primary); + stateMachine->pointerMove(Vec2D(170.0f, 170.0f)); + stateMachine->pointerUp(Vec2D(180.0f, 180.0f), + 0, + PointerButton::primary); + CHECK(inner->reportedEventCount() == 0); + } +} + +TEST_CASE("scroll views only drag with the primary button", "[pointer_button]") +{ + auto file = ReadRiveFile("assets/layout/layout_scroll_vertical.riv"); + auto artboard = file->artboard(); + auto artboardInstance = artboard->instance(); + auto machine = artboard->stateMachine("State Machine 1"); + REQUIRE(machine != nullptr); + StateMachineInstance smi(machine, artboardInstance.get()); + auto scroll = artboardInstance->find<ScrollConstraint>()[0]; + smi.advanceAndApply(0.0f); + + Vec2D over(artboardInstance->width() / 2.0f, + artboardInstance->height() / 2.0f); + Vec2D dragged(over.x, over.y - 50.0f); + + smi.pointerDown(over, 0, PointerButton::secondary); + smi.pointerMove(dragged); + smi.pointerUp(dragged, 0, PointerButton::secondary); + smi.advanceAndApply(0.0f); + CHECK(scroll->offsetY() == 0.0f); + + smi.pointerDown(over, 0, PointerButton::primary); + smi.pointerMove(dragged); + smi.advanceAndApply(0.0f); + CHECK(scroll->offsetY() != 0.0f); + smi.pointerUp(dragged, 0, PointerButton::primary); +} + +TEST_CASE("text inputs only select with the primary button", "[pointer_button]") +{ + auto file = ReadRiveFile("assets/text_input.riv"); + auto artboard = file->artboardNamed("Text Input - Multiline"); + REQUIRE(artboard != nullptr); + auto stateMachine = artboard->stateMachineAt(0); + REQUIRE(stateMachine != nullptr); + stateMachine->advanceAndApply(0.0f); + auto textInput = artboard->objects<TextInput>().first(); + REQUIRE(textInput != nullptr); + textInput->rawTextInput()->text("hello world"); + stateMachine->advanceAndApply(0.0f); + + Vec2D press(8.0f, 8.0f); + stateMachine->pointerDown(press, 0, PointerButton::secondary); + CHECK(textInput->isDragging() == false); + stateMachine->pointerUp(press, 0, PointerButton::secondary); + + stateMachine->pointerDown(press, 0, PointerButton::primary); + CHECK(textInput->isDragging() == true); + stateMachine->pointerUp(press, 0, PointerButton::primary); +} + +TEST_CASE("scenes without listeners ignore pointer buttons", "[pointer_button]") +{ + auto file = ReadRiveFile("assets/listener_input_values.riv"); + auto artboard = file->artboardDefault(); + REQUIRE(artboard->animationCount() > 0); + auto animation = artboard->animationAt(0); + Scene* scene = animation.get(); + CHECK(scene->pointerDown(Vec2D(1.0f, 1.0f), 0, PointerButton::secondary) == + HitResult::none); + CHECK(scene->pointerUp(Vec2D(1.0f, 1.0f), 0, PointerButton::secondary) == + HitResult::none); +}
diff --git a/tests/unit_tests/runtime/scripting/scripting_drawable_pointer_type_test.cpp b/tests/unit_tests/runtime/scripting/scripting_drawable_pointer_type_test.cpp index c8d3d69..56abdb1 100644 --- a/tests/unit_tests/runtime/scripting/scripting_drawable_pointer_type_test.cpp +++ b/tests/unit_tests/runtime/scripting/scripting_drawable_pointer_type_test.cpp
@@ -115,24 +115,44 @@ HitScriptedDrawable hit(&drawable, nullptr); - hit.processEvent(Vec2D(1.0f, 1.0f), ListenerType::down, true, 1.5f, 0); + hit.processEvent(Vec2D(1.0f, 1.0f), + ListenerType::down, + true, + 1.5f, + 0, + PointerButton::primary); CHECK(readString(L, "getLastHandler") == "pointerDown"); CHECK(readString(L, "getLastType") == "pointerDown"); CHECK(readNumber(L, "getLastTimeStamp") == 1.5f); - hit.processEvent(Vec2D(2.0f, 1.0f), ListenerType::move, true, 2.25f, 0); + hit.processEvent(Vec2D(2.0f, 1.0f), + ListenerType::move, + true, + 2.25f, + 0, + PointerButton::primary); CHECK(readString(L, "getLastHandler") == "pointerMove"); CHECK(readString(L, "getLastType") == "pointerMove"); CHECK(readNumber(L, "getLastTimeStamp") == 2.25f); - hit.processEvent(Vec2D(2.0f, 1.0f), ListenerType::up, true, 3.0f, 0); + hit.processEvent(Vec2D(2.0f, 1.0f), + ListenerType::up, + true, + 3.0f, + 0, + PointerButton::primary); CHECK(readString(L, "getLastHandler") == "pointerUp"); CHECK(readString(L, "getLastType") == "pointerUp"); CHECK(readNumber(L, "getLastTimeStamp") == 3.0f); // An occluded target is dispatched to pointerExit whatever the incoming // event was, and the type follows the handler. - hit.processEvent(Vec2D(2.0f, 1.0f), ListenerType::move, false, 4.0f, 0); + hit.processEvent(Vec2D(2.0f, 1.0f), + ListenerType::move, + false, + 4.0f, + 0, + PointerButton::primary); CHECK(readString(L, "getLastHandler") == "pointerExit"); CHECK(readString(L, "getLastType") == "pointerExit"); CHECK(readNumber(L, "getLastTimeStamp") == 4.0f);
diff --git a/tests/unit_tests/runtime/scripting/scripting_wake_advance_test.cpp b/tests/unit_tests/runtime/scripting/scripting_wake_advance_test.cpp index dd76c90..e96dc8b 100644 --- a/tests/unit_tests/runtime/scripting/scripting_wake_advance_test.cpp +++ b/tests/unit_tests/runtime/scripting/scripting_wake_advance_test.cpp
@@ -114,7 +114,12 @@ // Dispatch a pointer event through the hit component. HitScriptedDrawable hit(&drawable, nullptr); - hit.processEvent(Vec2D(1.0f, 1.0f), ListenerType::down, true, 0.0f, 0); + hit.processEvent(Vec2D(1.0f, 1.0f), + ListenerType::down, + true, + 0.0f, + 0, + PointerButton::primary); CHECK(readCounter(L, "getPointerDownCount") == 1); // The event re-armed the loop: the next frame advances the script again.