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