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