feature: add support for sending keyboard inputs to focused elements (#11924) 19486d13d0 Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head index b3b46bf..1d2e4af 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -46a089fc12117f1be6437b58d90d9ce3efd86325 +19486d13d0cfb9f7d1866a1371f71f1ec4e77dfe
diff --git a/dev/defs/animation/listener_types/listener_input_type_keyboard.json b/dev/defs/animation/listener_types/listener_input_type_keyboard.json new file mode 100644 index 0000000..dea7d7a --- /dev/null +++ b/dev/defs/animation/listener_types/listener_input_type_keyboard.json
@@ -0,0 +1,8 @@ +{ + "name": "ListenerInputTypeKeyboard", + "key": { + "int": 665, + "string": "listenerinputtypekeyboard" + }, + "extends": "animation/listener_types/listener_input_type.json" +} \ No newline at end of file
diff --git a/dev/defs/inputs/keyboard_input.json b/dev/defs/inputs/keyboard_input.json new file mode 100644 index 0000000..8eec54f --- /dev/null +++ b/dev/defs/inputs/keyboard_input.json
@@ -0,0 +1,37 @@ +{ + "name": "KeyboardInput", + "key": { + "int": 664, + "string": "keyboardinput" + }, + "extends": "inputs/user_input.json", + "properties": { + "keyType": { + "type": "uint", + "initialValue": "-1", + "key": { + "int": 971, + "string": "keytype" + }, + "description": "Key type for this keyboard input" + }, + "keyPhase": { + "type": "uint", + "initialValue": "0", + "key": { + "int": 972, + "string": "keyphase" + }, + "description": "Key phase to listen to (keyDown, keyUp, keyRepeat)" + }, + "modifiers": { + "type": "uint", + "initialValue": "0", + "key": { + "int": 973, + "string": "modifiers" + }, + "description": "bit flag for none, shift, control, alt, meta" + } + } +} \ No newline at end of file
diff --git a/dev/defs/inputs/user_input.json b/dev/defs/inputs/user_input.json new file mode 100644 index 0000000..fc78059 --- /dev/null +++ b/dev/defs/inputs/user_input.json
@@ -0,0 +1,19 @@ +{ + "name": "UserInput", + "key": { + "int": 663, + "string": "userinput" + }, + "properties": { + "targetId": { + "type": "Id", + "initialValue": "Core.missingId", + "key": { + "int": 970, + "string": "targetid" + }, + "description": "Identifier used to track where this input is used", + "runtime": false + } + } +} \ No newline at end of file
diff --git a/include/rive/animation/keyboard_listener_group.hpp b/include/rive/animation/keyboard_listener_group.hpp new file mode 100644 index 0000000..8780c96 --- /dev/null +++ b/include/rive/animation/keyboard_listener_group.hpp
@@ -0,0 +1,44 @@ +#ifndef _RIVE_KEYBOARD_LISTENER_GROUP_HPP_ +#define _RIVE_KEYBOARD_LISTENER_GROUP_HPP_ + +#include "rive/input/keyboard_listener.hpp" +#include "rive/listener_type.hpp" + +namespace rive +{ +class FocusData; +class StateMachineListener; +class StateMachineInstance; + +/// A KeyboardListenerGroup manages a keyboard listener that's attached +/// to a FocusData. When a key is pressed, the focus manager will call onKey +/// only on the focused one. +class KeyboardListenerGroup : public KeyboardListener +{ +public: + KeyboardListenerGroup(FocusData* focusData, + const StateMachineListener* listener, + StateMachineInstance* stateMachineInstance); + ~KeyboardListenerGroup(); + + /// Get the listener this group is managing. + const StateMachineListener* listener() const { return m_listener; } + + /// Get the FocusData this group is attached to. + FocusData* focusData() const { return m_focusData; } + + /// Called when the associated FocusData receives a key input. + bool keyInput(Key key, + KeyModifiers modifiers, + bool isPressed, + bool isRepeat) override; + +private: + FocusData* m_focusData; + const StateMachineListener* m_listener; + StateMachineInstance* m_stateMachineInstance; +}; + +} // namespace rive + +#endif
diff --git a/include/rive/animation/listener_types/listener_input_type_keyboard.hpp b/include/rive/animation/listener_types/listener_input_type_keyboard.hpp new file mode 100644 index 0000000..03df193 --- /dev/null +++ b/include/rive/animation/listener_types/listener_input_type_keyboard.hpp
@@ -0,0 +1,13 @@ +#ifndef _RIVE_LISTENER_INPUT_TYPE_KEYBOARD_HPP_ +#define _RIVE_LISTENER_INPUT_TYPE_KEYBOARD_HPP_ +#include "rive/generated/animation/listener_types/listener_input_type_keyboard_base.hpp" +#include <stdio.h> +namespace rive +{ +class ListenerInputTypeKeyboard : public ListenerInputTypeKeyboardBase +{ +public: +}; +} // namespace rive + +#endif \ No newline at end of file
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp index 9da349d..ce10afd 100644 --- a/include/rive/animation/state_machine_instance.hpp +++ b/include/rive/animation/state_machine_instance.hpp
@@ -5,6 +5,7 @@ #include <stddef.h> #include <vector> #include <unordered_map> +#include "rive/animation/keyboard_listener_group.hpp" #include "rive/animation/linear_animation_instance.hpp" #include "rive/animation/state_instance.hpp" #include "rive/animation/state_transition.hpp" @@ -292,6 +293,8 @@ FocusManager m_focusManager; FocusManager* m_externalFocusManager = nullptr; std::vector<std::unique_ptr<FocusListenerGroup>> m_focusListenerGroups; + std::vector<std::unique_ptr<KeyboardListenerGroup>> + m_keyboardListenerGroups; // Queued focus events for deferred processing struct QueuedFocusEvent
diff --git a/include/rive/focus_data.hpp b/include/rive/focus_data.hpp index c820ec6..67acf53 100644 --- a/include/rive/focus_data.hpp +++ b/include/rive/focus_data.hpp
@@ -3,6 +3,7 @@ #include "rive/component_dirt.hpp" #include "rive/generated/focus_data_base.hpp" #include "rive/input/focus_node.hpp" +#include "rive/input/keyboard_listener.hpp" #include "rive/input/focusable.hpp" #include "rive/math/aabb.hpp" #include "rive/refcnt.hpp" @@ -26,6 +27,12 @@ /// Unregister a listener. void removeFocusListener(FocusListener* listener); + /// Register a listener to be notified of key input events. + void addKeyboardListener(KeyboardListener* listener); + + /// Unregister a keyboard listener. + void removeKeyboardListener(KeyboardListener* listener); + /// Programmatically focus this node. void focus(); @@ -75,6 +82,7 @@ const AABB& elementBounds); rcp<FocusNode> m_focusNode; std::vector<FocusListener*> m_focusListeners; + std::vector<KeyboardListener*> m_keyboardListeners; }; } // namespace rive
diff --git a/include/rive/generated/animation/listener_types/listener_input_type_keyboard_base.hpp b/include/rive/generated/animation/listener_types/listener_input_type_keyboard_base.hpp new file mode 100644 index 0000000..768d297 --- /dev/null +++ b/include/rive/generated/animation/listener_types/listener_input_type_keyboard_base.hpp
@@ -0,0 +1,36 @@ +#ifndef _RIVE_LISTENER_INPUT_TYPE_KEYBOARD_BASE_HPP_ +#define _RIVE_LISTENER_INPUT_TYPE_KEYBOARD_BASE_HPP_ +#include "rive/animation/listener_types/listener_input_type.hpp" +namespace rive +{ +class ListenerInputTypeKeyboardBase : public ListenerInputType +{ +protected: + typedef ListenerInputType Super; + +public: + static const uint16_t typeKey = 665; + + /// Helper to quickly determine if a core object extends another without + /// RTTI at runtime. + bool isTypeOf(uint16_t typeKey) const override + { + switch (typeKey) + { + case ListenerInputTypeKeyboardBase::typeKey: + case ListenerInputTypeBase::typeKey: + return true; + default: + return false; + } + } + + uint16_t coreType() const override { return typeKey; } + + Core* clone() const override; + +protected: +}; +} // 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 44e6b13..3ce8a70 100644 --- a/include/rive/generated/core_registry.hpp +++ b/include/rive/generated/core_registry.hpp
@@ -44,6 +44,7 @@ #include "rive/animation/listener_trigger_change.hpp" #include "rive/animation/listener_types/listener_input_type.hpp" #include "rive/animation/listener_types/listener_input_type_event.hpp" +#include "rive/animation/listener_types/listener_input_type_keyboard.hpp" #include "rive/animation/listener_types/listener_input_type_viewmodel.hpp" #include "rive/animation/listener_viewmodel_change.hpp" #include "rive/animation/nested_bool.hpp" @@ -198,6 +199,8 @@ #include "rive/event.hpp" #include "rive/focus_data.hpp" #include "rive/foreground_layout_drawable.hpp" +#include "rive/inputs/keyboard_input.hpp" +#include "rive/inputs/user_input.hpp" #include "rive/joystick.hpp" #include "rive/layout/artboard_component_list_override.hpp" #include "rive/layout/axis.hpp" @@ -586,6 +589,8 @@ return new ListenerInputType(); case ListenerInputTypeEventBase::typeKey: return new ListenerInputTypeEvent(); + case ListenerInputTypeKeyboardBase::typeKey: + return new ListenerInputTypeKeyboard(); case ListenerInputTypeViewModelBase::typeKey: return new ListenerInputTypeViewModel(); case ExitStateBase::typeKey: @@ -852,6 +857,10 @@ return new FileAssetContents(); case AudioEventBase::typeKey: return new AudioEvent(); + case UserInputBase::typeKey: + return new UserInput(); + case KeyboardInputBase::typeKey: + return new KeyboardInput(); case ScriptInputArtboardBase::typeKey: return new ScriptInputArtboard(); } @@ -1595,6 +1604,15 @@ case AudioEventBase::assetIdPropertyKey: object->as<AudioEventBase>()->assetId(value); break; + case KeyboardInputBase::keyTypePropertyKey: + object->as<KeyboardInputBase>()->keyType(value); + break; + case KeyboardInputBase::keyPhasePropertyKey: + object->as<KeyboardInputBase>()->keyPhase(value); + break; + case KeyboardInputBase::modifiersPropertyKey: + object->as<KeyboardInputBase>()->modifiers(value); + break; case ScriptInputArtboardBase::artboardIdPropertyKey: object->as<ScriptInputArtboardBase>()->artboardId(value); break; @@ -3068,6 +3086,12 @@ return object->as<ScriptAssetBase>()->generatorFunctionRef(); case AudioEventBase::assetIdPropertyKey: return object->as<AudioEventBase>()->assetId(); + case KeyboardInputBase::keyTypePropertyKey: + return object->as<KeyboardInputBase>()->keyType(); + case KeyboardInputBase::keyPhasePropertyKey: + return object->as<KeyboardInputBase>()->keyPhase(); + case KeyboardInputBase::modifiersPropertyKey: + return object->as<KeyboardInputBase>()->modifiers(); case ScriptInputArtboardBase::artboardIdPropertyKey: return object->as<ScriptInputArtboardBase>()->artboardId(); } @@ -3953,6 +3977,9 @@ case FileAssetBase::assetIdPropertyKey: case ScriptAssetBase::generatorFunctionRefPropertyKey: case AudioEventBase::assetIdPropertyKey: + case KeyboardInputBase::keyTypePropertyKey: + case KeyboardInputBase::keyPhasePropertyKey: + case KeyboardInputBase::modifiersPropertyKey: case ScriptInputArtboardBase::artboardIdPropertyKey: return CoreUintType::id; case ViewModelComponentBase::namePropertyKey: @@ -4739,6 +4766,12 @@ return object->is<ScriptAssetBase>(); case AudioEventBase::assetIdPropertyKey: return object->is<AudioEventBase>(); + case KeyboardInputBase::keyTypePropertyKey: + return object->is<KeyboardInputBase>(); + case KeyboardInputBase::keyPhasePropertyKey: + return object->is<KeyboardInputBase>(); + case KeyboardInputBase::modifiersPropertyKey: + return object->is<KeyboardInputBase>(); case ScriptInputArtboardBase::artboardIdPropertyKey: return object->is<ScriptInputArtboardBase>(); case ViewModelComponentBase::namePropertyKey:
diff --git a/include/rive/generated/inputs/keyboard_input_base.hpp b/include/rive/generated/inputs/keyboard_input_base.hpp new file mode 100644 index 0000000..5163da3 --- /dev/null +++ b/include/rive/generated/inputs/keyboard_input_base.hpp
@@ -0,0 +1,107 @@ +#ifndef _RIVE_KEYBOARD_INPUT_BASE_HPP_ +#define _RIVE_KEYBOARD_INPUT_BASE_HPP_ +#include "rive/core/field_types/core_uint_type.hpp" +#include "rive/inputs/user_input.hpp" +namespace rive +{ +class KeyboardInputBase : public UserInput +{ +protected: + typedef UserInput Super; + +public: + static const uint16_t typeKey = 664; + + /// Helper to quickly determine if a core object extends another without + /// RTTI at runtime. + bool isTypeOf(uint16_t typeKey) const override + { + switch (typeKey) + { + case KeyboardInputBase::typeKey: + case UserInputBase::typeKey: + return true; + default: + return false; + } + } + + uint16_t coreType() const override { return typeKey; } + + static const uint16_t keyTypePropertyKey = 971; + static const uint16_t keyPhasePropertyKey = 972; + static const uint16_t modifiersPropertyKey = 973; + +protected: + uint32_t m_KeyType = -1; + uint32_t m_KeyPhase = 0; + uint32_t m_Modifiers = 0; + +public: + inline uint32_t keyType() const { return m_KeyType; } + void keyType(uint32_t value) + { + if (m_KeyType == value) + { + return; + } + m_KeyType = value; + keyTypeChanged(); + } + + inline uint32_t keyPhase() const { return m_KeyPhase; } + void keyPhase(uint32_t value) + { + if (m_KeyPhase == value) + { + return; + } + m_KeyPhase = value; + keyPhaseChanged(); + } + + inline uint32_t modifiers() const { return m_Modifiers; } + void modifiers(uint32_t value) + { + if (m_Modifiers == value) + { + return; + } + m_Modifiers = value; + modifiersChanged(); + } + + Core* clone() const override; + void copy(const KeyboardInputBase& object) + { + m_KeyType = object.m_KeyType; + m_KeyPhase = object.m_KeyPhase; + m_Modifiers = object.m_Modifiers; + UserInput::copy(object); + } + + bool deserialize(uint16_t propertyKey, BinaryReader& reader) override + { + switch (propertyKey) + { + case keyTypePropertyKey: + m_KeyType = CoreUintType::deserialize(reader); + return true; + case keyPhasePropertyKey: + m_KeyPhase = CoreUintType::deserialize(reader); + return true; + case modifiersPropertyKey: + m_Modifiers = CoreUintType::deserialize(reader); + return true; + } + return UserInput::deserialize(propertyKey, reader); + } + +protected: + virtual void keyTypeChanged() {} + virtual void keyPhaseChanged() {} + virtual void modifiersChanged() {} +}; +} // namespace rive + +#endif \ No newline at end of file
diff --git a/include/rive/generated/inputs/user_input_base.hpp b/include/rive/generated/inputs/user_input_base.hpp new file mode 100644 index 0000000..fe57b33 --- /dev/null +++ b/include/rive/generated/inputs/user_input_base.hpp
@@ -0,0 +1,41 @@ +#ifndef _RIVE_USER_INPUT_BASE_HPP_ +#define _RIVE_USER_INPUT_BASE_HPP_ +#include "rive/core.hpp" +namespace rive +{ +class UserInputBase : public Core +{ +protected: + typedef Core Super; + +public: + static const uint16_t typeKey = 663; + + /// Helper to quickly determine if a core object extends another without + /// RTTI at runtime. + bool isTypeOf(uint16_t typeKey) const override + { + switch (typeKey) + { + case UserInputBase::typeKey: + return true; + default: + return false; + } + } + + uint16_t coreType() const override { return typeKey; } + + Core* clone() const override; + void copy(const UserInputBase& object) {} + + bool deserialize(uint16_t propertyKey, BinaryReader& reader) override + { + return false; + } + +protected: +}; +} // namespace rive + +#endif \ No newline at end of file
diff --git a/include/rive/input/keyboard_listener.hpp b/include/rive/input/keyboard_listener.hpp new file mode 100644 index 0000000..6356c00 --- /dev/null +++ b/include/rive/input/keyboard_listener.hpp
@@ -0,0 +1,24 @@ +#ifndef _RIVE_KEYBOARD_LISTENER_HPP_ +#define _RIVE_KEYBOARD_LISTENER_HPP_ +#include "rive/input/focusable.hpp" + +namespace rive +{ + +/// Interface for objects that want to be notified of key inputs on a +/// FocusData. +class KeyboardListener +{ +public: + virtual ~KeyboardListener() = default; + + /// Called when the associated FocusData receives key inputs. + virtual bool keyInput(Key key, + KeyModifiers modifiers, + bool isPressed, + bool isRepeat) = 0; +}; + +} // namespace rive + +#endif
diff --git a/include/rive/inputs/keyboard_input.hpp b/include/rive/inputs/keyboard_input.hpp new file mode 100644 index 0000000..a376ceb --- /dev/null +++ b/include/rive/inputs/keyboard_input.hpp
@@ -0,0 +1,13 @@ +#ifndef _RIVE_KEYBOARD_INPUT_HPP_ +#define _RIVE_KEYBOARD_INPUT_HPP_ +#include "rive/generated/inputs/keyboard_input_base.hpp" +#include <stdio.h> +namespace rive +{ +class KeyboardInput : public KeyboardInputBase +{ +public: +}; +} // namespace rive + +#endif \ No newline at end of file
diff --git a/include/rive/inputs/user_input.hpp b/include/rive/inputs/user_input.hpp new file mode 100644 index 0000000..d00a737 --- /dev/null +++ b/include/rive/inputs/user_input.hpp
@@ -0,0 +1,13 @@ +#ifndef _RIVE_USER_INPUT_HPP_ +#define _RIVE_USER_INPUT_HPP_ +#include "rive/generated/inputs/user_input_base.hpp" +#include <stdio.h> +namespace rive +{ +class UserInput : public UserInputBase +{ +public: +}; +} // namespace rive + +#endif \ No newline at end of file
diff --git a/include/rive/listener_type.hpp b/include/rive/listener_type.hpp index b1f9db5..ad8e646 100644 --- a/include/rive/listener_type.hpp +++ b/include/rive/listener_type.hpp
@@ -19,6 +19,7 @@ drag = 12, focus = 13, blur = 14, + keyboard = 15, }; } #endif \ No newline at end of file
diff --git a/src/animation/keyboard_listener_group.cpp b/src/animation/keyboard_listener_group.cpp new file mode 100644 index 0000000..860cfff --- /dev/null +++ b/src/animation/keyboard_listener_group.cpp
@@ -0,0 +1,34 @@ +#include "rive/animation/keyboard_listener_group.hpp" +#include "rive/animation/state_machine_instance.hpp" +#include "rive/animation/state_machine_listener.hpp" +#include "rive/focus_data.hpp" + +using namespace rive; + +KeyboardListenerGroup::KeyboardListenerGroup( + FocusData* focusData, + const StateMachineListener* listener, + StateMachineInstance* stateMachineInstance) : + m_focusData(focusData), + m_listener(listener), + m_stateMachineInstance(stateMachineInstance) +{ + // Register ourselves as a listener on the FocusData + m_focusData->addKeyboardListener(this); +} + +KeyboardListenerGroup::~KeyboardListenerGroup() +{ + m_focusData->removeKeyboardListener(this); +} + +bool KeyboardListenerGroup::keyInput(Key key, + KeyModifiers modifiers, + bool isPressed, + bool isRepeat) +{ + listener()->performChanges(m_stateMachineInstance, Vec2D(), Vec2D(), 0); + // Always return false for now. In the future we will let listeners decide + // whether they stop event propagation + return false; +}
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp index 0914407..90bbb90 100644 --- a/src/animation/state_machine_instance.cpp +++ b/src/animation/state_machine_instance.cpp
@@ -832,6 +832,7 @@ case ListenerType::drag: case ListenerType::focus: case ListenerType::blur: + case ListenerType::keyboard: break; } } @@ -857,6 +858,7 @@ case ListenerType::drag: case ListenerType::focus: case ListenerType::blur: + case ListenerType::keyboard: break; } } @@ -970,6 +972,7 @@ case ListenerType::drag: case ListenerType::focus: case ListenerType::blur: + case ListenerType::keyboard: break; } } @@ -994,6 +997,7 @@ case ListenerType::drag: case ListenerType::focus: case ListenerType::blur: + case ListenerType::keyboard: break; } } @@ -1600,6 +1604,34 @@ } continue; } + if (listener->hasListener(ListenerType::keyboard)) + { + auto target = m_artboardInstance->resolve(listener->targetId()); + if (target != nullptr && target->is<Node>()) + { + auto node = target->as<Node>(); + // Find FocusData child of the node + FocusData* focusData = nullptr; + for (auto child : node->children()) + { + if (child->is<FocusData>()) + { + focusData = child->as<FocusData>(); + break; + } + } + if (focusData != nullptr) + { + auto keyboardGroup = + rivestd::make_unique<KeyboardListenerGroup>(focusData, + listener, + this); + m_keyboardListenerGroups.push_back( + std::move(keyboardGroup)); + } + } + continue; + } auto listenerGroup = rivestd::make_unique<ListenerGroup>(listener); auto target = m_artboardInstance->resolve(listener->targetId()); if (target != nullptr && target->is<Component>())
diff --git a/src/focus_data.cpp b/src/focus_data.cpp index 3c864b8..eb187e9 100644 --- a/src/focus_data.cpp +++ b/src/focus_data.cpp
@@ -70,6 +70,22 @@ } } +void FocusData::addKeyboardListener(KeyboardListener* listener) +{ + m_keyboardListeners.push_back(listener); +} + +void FocusData::removeKeyboardListener(KeyboardListener* listener) +{ + auto it = std::find(m_keyboardListeners.begin(), + m_keyboardListeners.end(), + listener); + if (it != m_keyboardListeners.end()) + { + m_keyboardListeners.erase(it); + } +} + void FocusData::focus() { // Note: In C++ runtime, focus() needs a FocusManager to set focus. @@ -83,12 +99,23 @@ bool isPressed, bool isRepeat) { + + // Notify listeners + bool handled = false; + for (auto* listener : m_keyboardListeners) + { + handled = listener->keyInput(value, modifiers, isPressed, isRepeat); + if (handled) + { + break; + } + } // Search only the children of this FocusData's owner (parent Node), // not the entire artboard. auto* parentNode = parent(); if (parentNode == nullptr || !parentNode->is<Node>()) { - return false; + return handled; } // If the parent is Focusable and immediately handles the input, we're done! auto* focusable = Focusable::from(parentNode); @@ -97,6 +124,11 @@ { return true; } + // If it was already handled, we're done too! + if (handled) + { + return handled; + } for (auto* child : parentNode->as<Node>()->children()) { if (sendInputToFocusableChildren(child,
diff --git a/src/generated/animation/listener_types/listener_input_type_keyboard_base.cpp b/src/generated/animation/listener_types/listener_input_type_keyboard_base.cpp new file mode 100644 index 0000000..663f415 --- /dev/null +++ b/src/generated/animation/listener_types/listener_input_type_keyboard_base.cpp
@@ -0,0 +1,11 @@ +#include "rive/generated/animation/listener_types/listener_input_type_keyboard_base.hpp" +#include "rive/animation/listener_types/listener_input_type_keyboard.hpp" + +using namespace rive; + +Core* ListenerInputTypeKeyboardBase::clone() const +{ + auto cloned = new ListenerInputTypeKeyboard(); + cloned->copy(*this); + return cloned; +}
diff --git a/src/generated/inputs/keyboard_input_base.cpp b/src/generated/inputs/keyboard_input_base.cpp new file mode 100644 index 0000000..8846aca --- /dev/null +++ b/src/generated/inputs/keyboard_input_base.cpp
@@ -0,0 +1,11 @@ +#include "rive/generated/inputs/keyboard_input_base.hpp" +#include "rive/inputs/keyboard_input.hpp" + +using namespace rive; + +Core* KeyboardInputBase::clone() const +{ + auto cloned = new KeyboardInput(); + cloned->copy(*this); + return cloned; +}
diff --git a/src/generated/inputs/user_input_base.cpp b/src/generated/inputs/user_input_base.cpp new file mode 100644 index 0000000..a8a0447 --- /dev/null +++ b/src/generated/inputs/user_input_base.cpp
@@ -0,0 +1,11 @@ +#include "rive/generated/inputs/user_input_base.hpp" +#include "rive/inputs/user_input.hpp" + +using namespace rive; + +Core* UserInputBase::clone() const +{ + auto cloned = new UserInput(); + cloned->copy(*this); + return cloned; +}
diff --git a/tests/unit_tests/assets/keyboard_listener.riv b/tests/unit_tests/assets/keyboard_listener.riv new file mode 100644 index 0000000..fc60e56 --- /dev/null +++ b/tests/unit_tests/assets/keyboard_listener.riv Binary files differ
diff --git a/tests/unit_tests/runtime/focus_test.cpp b/tests/unit_tests/runtime/focus_test.cpp index f4ce08e..3c61f8b 100644 --- a/tests/unit_tests/runtime/focus_test.cpp +++ b/tests/unit_tests/runtime/focus_test.cpp
@@ -731,3 +731,83 @@ CHECK(silver.matches("focus_collapsing")); } + +TEST_CASE("Focused elements receive keyboard inputs", "[silver]") +{ + rive::SerializingFactory silver; + auto file = ReadRiveFile("assets/keyboard_listener.riv", &silver); + + auto artboard = file->artboardDefault(); + silver.frameSize(artboard->width(), artboard->height()); + + auto stateMachine = artboard->stateMachineAt(0); + int viewModelId = artboard.get()->viewModelId(); + + auto vmi = viewModelId == -1 + ? file->createViewModelInstance(artboard.get()) + : file->createViewModelInstance(viewModelId, 0); + + stateMachine->bindViewModelInstance(vmi); + auto renderer = silver.makeRenderer(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + auto focusManager = artboard->focusManager(); + // Child index 5 + focusManager->focusPrevious(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + focusManager->keyInput(rive::Key::space, + rive::KeyModifiers::none, + false, + false); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + // Child index 4 + focusManager->focusPrevious(); + // Child index 3 + focusManager->focusPrevious(); + // Child index 2 + focusManager->focusPrevious(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + focusManager->keyInput(rive::Key::space, + rive::KeyModifiers::none, + false, + false); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + // Child index 1 + focusManager->focusPrevious(); + // Child index 0 + focusManager->focusPrevious(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + focusManager->keyInput(rive::Key::space, + rive::KeyModifiers::none, + false, + false); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + focusManager->focusPrevious(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + focusManager->keyInput(rive::Key::space, + rive::KeyModifiers::none, + false, + false); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + + CHECK(silver.matches("keyboard_listener")); +} \ No newline at end of file
diff --git a/tests/unit_tests/silvers/keyboard_listener.sriv b/tests/unit_tests/silvers/keyboard_listener.sriv new file mode 100644 index 0000000..cef7aea --- /dev/null +++ b/tests/unit_tests/silvers/keyboard_listener.sriv Binary files differ