Nnnnn state machine key input listeners part 2 (#11936) 8a82cf2e25 * feature(keyboard input): add support for keys with modifiers Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head index 9e9b1ac..2605958 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -235eba5b6be67890cc222da38fb73f5d56ccc987 +8a82cf2e255414ea71bacb79e6d18321033795ec
diff --git a/include/rive/animation/listener_types/listener_input_type_keyboard.hpp b/include/rive/animation/listener_types/listener_input_type_keyboard.hpp index 03df193..88462fb 100644 --- a/include/rive/animation/listener_types/listener_input_type_keyboard.hpp +++ b/include/rive/animation/listener_types/listener_input_type_keyboard.hpp
@@ -1,13 +1,41 @@ #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> +#include "rive/input/focusable.hpp" + +#include <vector> + namespace rive { +class KeyboardInput; +class StateMachineListener; + class ListenerInputTypeKeyboard : public ListenerInputTypeKeyboardBase { public: + size_t keyboardInputCount() const { return m_keyboardInputs.size(); } + const KeyboardInput* keyboardInput(size_t index) const; + void addKeyboardInput(KeyboardInput* input); + + static bool keyPhaseMatches(uint32_t keyPhase, + bool isPressed, + bool isRepeat); + static bool keyboardInputMatches(const KeyboardInput& input, + Key key, + KeyModifiers modifiers, + bool isPressed, + bool isRepeat); + static bool keyboardListenerConstraintsMet( + const StateMachineListener* listener, + Key key, + KeyModifiers modifiers, + bool isPressed, + bool isRepeat); + +private: + std::vector<KeyboardInput*> m_keyboardInputs; }; } // namespace rive -#endif \ No newline at end of file +#endif
diff --git a/include/rive/importers/listener_input_type_keyboard_importer.hpp b/include/rive/importers/listener_input_type_keyboard_importer.hpp new file mode 100644 index 0000000..9e6f760 --- /dev/null +++ b/include/rive/importers/listener_input_type_keyboard_importer.hpp
@@ -0,0 +1,26 @@ +#ifndef _RIVE_LISTENER_INPUT_TYPE_KEYBOARD_IMPORTER_HPP_ +#define _RIVE_LISTENER_INPUT_TYPE_KEYBOARD_IMPORTER_HPP_ + +#include "rive/importers/import_stack.hpp" + +namespace rive +{ +class ListenerInputTypeKeyboard; + +class ListenerInputTypeKeyboardImporter : public ImportStackObject +{ +private: + ListenerInputTypeKeyboard* m_listenerInputTypeKeyboard; + +public: + explicit ListenerInputTypeKeyboardImporter( + ListenerInputTypeKeyboard* listenerInputTypeKeyboard); + ListenerInputTypeKeyboard* listenerInputTypeKeyboard() const + { + return m_listenerInputTypeKeyboard; + } + StatusCode resolve() override; +}; +} // namespace rive + +#endif
diff --git a/include/rive/inputs/keyboard_input.hpp b/include/rive/inputs/keyboard_input.hpp index a376ceb..8f13604 100644 --- a/include/rive/inputs/keyboard_input.hpp +++ b/include/rive/inputs/keyboard_input.hpp
@@ -1,12 +1,14 @@ #ifndef _RIVE_KEYBOARD_INPUT_HPP_ #define _RIVE_KEYBOARD_INPUT_HPP_ #include "rive/generated/inputs/keyboard_input_base.hpp" +#include "rive/inputs/keyboard_key_phase.hpp" #include <stdio.h> namespace rive { class KeyboardInput : public KeyboardInputBase { public: + StatusCode import(ImportStack& importStack) override; }; } // namespace rive
diff --git a/include/rive/inputs/keyboard_key_phase.hpp b/include/rive/inputs/keyboard_key_phase.hpp new file mode 100644 index 0000000..3d5d11b --- /dev/null +++ b/include/rive/inputs/keyboard_key_phase.hpp
@@ -0,0 +1,18 @@ +#ifndef _RIVE_KEYBOARD_KEY_PHASE_HPP_ +#define _RIVE_KEYBOARD_KEY_PHASE_HPP_ + +#include <cstdint> + +namespace rive +{ +/// Bitmask for \c KeyboardInputBase::keyPhase() (property key 972). +struct KeyboardKeyPhaseMask +{ + static constexpr uint32_t down = 1u << 0; + static constexpr uint32_t repeat = 1u << 1; + static constexpr uint32_t up = 1u << 2; + static constexpr uint32_t all = down | repeat | up; +}; +} // namespace rive + +#endif
diff --git a/src/animation/keyboard_listener_group.cpp b/src/animation/keyboard_listener_group.cpp index 860cfff..d79b392 100644 --- a/src/animation/keyboard_listener_group.cpp +++ b/src/animation/keyboard_listener_group.cpp
@@ -1,4 +1,5 @@ #include "rive/animation/keyboard_listener_group.hpp" +#include "rive/animation/listener_types/listener_input_type_keyboard.hpp" #include "rive/animation/state_machine_instance.hpp" #include "rive/animation/state_machine_listener.hpp" #include "rive/focus_data.hpp" @@ -27,6 +28,14 @@ bool isPressed, bool isRepeat) { + if (!ListenerInputTypeKeyboard::keyboardListenerConstraintsMet(listener(), + key, + modifiers, + isPressed, + isRepeat)) + { + return false; + } 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
diff --git a/src/animation/listener_types/listener_input_type_keyboard.cpp b/src/animation/listener_types/listener_input_type_keyboard.cpp new file mode 100644 index 0000000..d9fe5ba --- /dev/null +++ b/src/animation/listener_types/listener_input_type_keyboard.cpp
@@ -0,0 +1,116 @@ +#include "rive/animation/listener_types/listener_input_type_keyboard.hpp" +#include "rive/animation/state_machine_listener.hpp" +#include "rive/inputs/keyboard_input.hpp" +#include "rive/rive_types.hpp" + +#include <algorithm> + +using namespace rive; + +const KeyboardInput* ListenerInputTypeKeyboard::keyboardInput( + size_t index) const +{ + if (index < m_keyboardInputs.size()) + { + return m_keyboardInputs[index]; + } + return nullptr; +} + +void ListenerInputTypeKeyboard::addKeyboardInput(KeyboardInput* input) +{ + if (input == nullptr) + { + return; + } + for (KeyboardInput* existing : m_keyboardInputs) + { + if (existing == input) + { + return; + } + } + m_keyboardInputs.push_back(input); +} + +bool ListenerInputTypeKeyboard::keyPhaseMatches(uint32_t keyPhase, + bool isPressed, + bool isRepeat) +{ + const uint32_t mask = keyPhase & KeyboardKeyPhaseMask::all; + if (mask == 0) + { + return false; + } + if (isPressed && isRepeat) + { + return (mask & KeyboardKeyPhaseMask::repeat) != 0; + } + if (isPressed) + { + return (mask & KeyboardKeyPhaseMask::down) != 0; + } + return (mask & KeyboardKeyPhaseMask::up) != 0; +} + +bool ListenerInputTypeKeyboard::keyboardInputMatches(const KeyboardInput& input, + Key key, + KeyModifiers modifiers, + bool isPressed, + bool isRepeat) +{ + const uint32_t keyValue = static_cast<uint32_t>(key); + if (input.keyType() != static_cast<uint32_t>(-1) && + input.keyType() != keyValue) + { + return false; + } + if (input.modifiers() != static_cast<uint32_t>(modifiers)) + { + return false; + } + return keyPhaseMatches(input.keyPhase(), isPressed, isRepeat); +} + +bool ListenerInputTypeKeyboard::keyboardListenerConstraintsMet( + const StateMachineListener* listener, + Key key, + KeyModifiers modifiers, + bool isPressed, + bool isRepeat) +{ + if (listener == nullptr) + { + return false; + } + for (size_t i = 0; i < listener->listenerInputTypeCount(); ++i) + { + const ListenerInputType* lit = listener->listenerInputType(i); + if (lit == nullptr || !lit->is<ListenerInputTypeKeyboard>()) + { + continue; + } + const ListenerInputTypeKeyboard* litk = + lit->as<ListenerInputTypeKeyboard>(); + if (litk->keyboardInputCount() == 0) + { + return true; + } + bool anyMatches = false; + for (size_t j = 0; j < litk->keyboardInputCount(); ++j) + { + const KeyboardInput* ki = litk->keyboardInput(j); + if (ki != nullptr && + keyboardInputMatches(*ki, key, modifiers, isPressed, isRepeat)) + { + anyMatches = true; + break; + } + } + if (anyMatches) + { + return true; + } + } + return false; +}
diff --git a/src/file.cpp b/src/file.cpp index 3f410d5..7ac8e1b 100644 --- a/src/file.cpp +++ b/src/file.cpp
@@ -32,6 +32,7 @@ #include "rive/importers/state_transition_importer.hpp" #include "rive/importers/state_machine_layer_component_importer.hpp" #include "rive/importers/transition_viewmodel_condition_importer.hpp" +#include "rive/importers/listener_input_type_keyboard_importer.hpp" #include "rive/importers/viewmodel_importer.hpp" #include "rive/importers/viewmodel_instance_importer.hpp" #include "rive/importers/viewmodel_instance_list_importer.hpp" @@ -463,6 +464,12 @@ m_factory); stackType = FileAsset::typeKey; break; + case ListenerInputTypeKeyboardBase::typeKey: + stackObject = + rivestd::make_unique<ListenerInputTypeKeyboardImporter>( + object->as<ListenerInputTypeKeyboard>()); + stackType = ListenerInputTypeKeyboardBase::typeKey; + break; #ifdef WITH_RIVE_SCRIPTING case ScriptAsset::typeKey: {
diff --git a/src/focus_data.cpp b/src/focus_data.cpp index eb187e9..bc26d96 100644 --- a/src/focus_data.cpp +++ b/src/focus_data.cpp
@@ -99,7 +99,6 @@ bool isPressed, bool isRepeat) { - // Notify listeners bool handled = false; for (auto* listener : m_keyboardListeners)
diff --git a/src/importers/listener_input_type_keyboard_importer.cpp b/src/importers/listener_input_type_keyboard_importer.cpp new file mode 100644 index 0000000..dd091bb --- /dev/null +++ b/src/importers/listener_input_type_keyboard_importer.cpp
@@ -0,0 +1,13 @@ +#include "rive/importers/listener_input_type_keyboard_importer.hpp" + +using namespace rive; + +ListenerInputTypeKeyboardImporter::ListenerInputTypeKeyboardImporter( + ListenerInputTypeKeyboard* listenerInputTypeKeyboard) : + m_listenerInputTypeKeyboard(listenerInputTypeKeyboard) +{} + +StatusCode ListenerInputTypeKeyboardImporter::resolve() +{ + return StatusCode::Ok; +}
diff --git a/src/inputs/keyboard_input.cpp b/src/inputs/keyboard_input.cpp new file mode 100644 index 0000000..22ab2d5 --- /dev/null +++ b/src/inputs/keyboard_input.cpp
@@ -0,0 +1,28 @@ +#include "rive/inputs/keyboard_input.hpp" +#include "rive/animation/listener_types/listener_input_type_keyboard.hpp" +#include "rive/generated/animation/listener_types/listener_input_type_keyboard_base.hpp" +#include "rive/generated/artboard_base.hpp" +#include "rive/importers/artboard_importer.hpp" +#include "rive/importers/listener_input_type_keyboard_importer.hpp" + +using namespace rive; + +StatusCode KeyboardInput::import(ImportStack& importStack) +{ + auto* litImporter = importStack.latest<ListenerInputTypeKeyboardImporter>( + ListenerInputTypeKeyboardBase::typeKey); + if (litImporter == nullptr) + { + return StatusCode::MissingObject; + } + litImporter->listenerInputTypeKeyboard()->addKeyboardInput(this); + + auto artboardImporter = + importStack.latest<ArtboardImporter>(ArtboardBase::typeKey); + if (artboardImporter == nullptr) + { + return StatusCode::MissingObject; + } + artboardImporter->addComponent(this); + return Super::import(importStack); +}
diff --git a/tests/unit_tests/assets/keyboard_listener.riv b/tests/unit_tests/assets/keyboard_listener.riv index fc60e56..50e80cd 100644 --- a/tests/unit_tests/assets/keyboard_listener.riv +++ 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 3c61f8b..f2f1adf 100644 --- a/tests/unit_tests/runtime/focus_test.cpp +++ b/tests/unit_tests/runtime/focus_test.cpp
@@ -810,4 +810,120 @@ artboard->draw(renderer.get()); CHECK(silver.matches("keyboard_listener")); -} \ No newline at end of file +} + +TEST_CASE("Keyboard inputs with different key combinations", "[silver]") +{ + rive::SerializingFactory silver; + auto file = ReadRiveFile("assets/keyboard_listener.riv", &silver); + + auto artboard = file->artboardNamed("KeyboardInput"); + 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); + auto keyCountProp = + vmi->propertyValue("keyCount")->as<rive::ViewModelInstanceNumber>(); + + stateMachine->bindViewModelInstance(vmi); + auto renderer = silver.makeRenderer(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + + auto focusManager = artboard->focusManager(); + focusManager->focusNext(); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + silver.addFrame(); + // Key "a" on phase down with no modifiers is captured + focusManager->keyInput(rive::Key::a, rive::KeyModifiers::none, true, false); + stateMachine->advanceAndApply(0.016f); + CHECK(keyCountProp->propertyValue() == 1); + artboard->draw(renderer.get()); + silver.addFrame(); + // Key "a" on phase repeat with no modifiers is not captured + focusManager->keyInput(rive::Key::a, rive::KeyModifiers::none, true, true); + stateMachine->advanceAndApply(0.016f); + CHECK(keyCountProp->propertyValue() == 1); + // Key "a" on phase up with no modifiers is captured + focusManager->keyInput(rive::Key::a, + rive::KeyModifiers::none, + false, + false); + stateMachine->advanceAndApply(0.016f); + CHECK(keyCountProp->propertyValue() == 2); + + // Key "a" on phase down with modifiers is not captured + focusManager->keyInput(rive::Key::a, + rive::KeyModifiers::shift, + true, + false); + stateMachine->advanceAndApply(0.016f); + CHECK(keyCountProp->propertyValue() == 2); + + // Key "e" on any phase is not captured + focusManager->keyInput(rive::Key::e, + rive::KeyModifiers::none, + false, + false); + focusManager->keyInput(rive::Key::e, rive::KeyModifiers::none, true, true); + focusManager->keyInput(rive::Key::e, rive::KeyModifiers::none, true, false); + CHECK(keyCountProp->propertyValue() == 2); + stateMachine->advanceAndApply(0.016f); + // Key "b" on phase down with no modifiers is NOT captured + focusManager->keyInput(rive::Key::b, rive::KeyModifiers::none, true, false); + // Key "b" on phase up with no modifiers is NOT captured + CHECK(keyCountProp->propertyValue() == 2); + stateMachine->advanceAndApply(0.016f); + focusManager->keyInput(rive::Key::b, + rive::KeyModifiers::none, + false, + false); + stateMachine->advanceAndApply(0.016f); + CHECK(keyCountProp->propertyValue() == 3); + // Key "b" on phase repeat with no modifiers is captured + focusManager->keyInput(rive::Key::b, rive::KeyModifiers::none, true, true); + stateMachine->advanceAndApply(0.016f); + CHECK(keyCountProp->propertyValue() == 4); + // Key "d" on phase down with no modifiers is not captured + focusManager->keyInput(rive::Key::d, rive::KeyModifiers::none, true, false); + stateMachine->advanceAndApply(0.016f); + CHECK(keyCountProp->propertyValue() == 4); + // Key "d" on phase down with shift + command modifiers is captured + focusManager->keyInput(rive::Key::d, + rive::KeyModifiers::shift | rive::KeyModifiers::meta, + true, + false); + stateMachine->advanceAndApply(0.016f); + CHECK(keyCountProp->propertyValue() == 5); + // Key "c" on phase down with shift + command modifiers is NOT captured + focusManager->keyInput(rive::Key::c, + rive::KeyModifiers::shift | rive::KeyModifiers::meta, + true, + false); + stateMachine->advanceAndApply(0.016f); + CHECK(keyCountProp->propertyValue() == 5); + // Key "c" on phase down with shift modifiers is captured + focusManager->keyInput(rive::Key::c, + rive::KeyModifiers::shift, + true, + false); + stateMachine->advanceAndApply(0.016f); + CHECK(keyCountProp->propertyValue() == 6); + // Key "x" on phase down with shift modifiers is NOT captured + focusManager->keyInput(rive::Key::x, + rive::KeyModifiers::shift, + true, + false); + stateMachine->advanceAndApply(0.016f); + CHECK(keyCountProp->propertyValue() == 6); + + artboard->draw(renderer.get()); + + CHECK(silver.matches("keyboard_listener-KeyboardInput")); +}
diff --git a/tests/unit_tests/silvers/keyboard_listener-KeyboardInput.sriv b/tests/unit_tests/silvers/keyboard_listener-KeyboardInput.sriv new file mode 100644 index 0000000..e6063ab --- /dev/null +++ b/tests/unit_tests/silvers/keyboard_listener-KeyboardInput.sriv Binary files differ
diff --git a/tests/unit_tests/silvers/keyboard_listener.sriv b/tests/unit_tests/silvers/keyboard_listener.sriv index cef7aea..b03337f 100644 --- a/tests/unit_tests/silvers/keyboard_listener.sriv +++ b/tests/unit_tests/silvers/keyboard_listener.sriv Binary files differ