Register key and action listeners using `Interactive::initialize`

Antonio Scandurra and Thorsten created

Co-Authored-By: Thorsten <mrnugget@gmail.com>

Change summary

crates/editor2/src/element.rs          | 362 +++++++++++++--------------
crates/gpui2/src/elements/div.rs       |   1 
crates/gpui2/src/interactive.rs        |  87 +++--
crates/gpui2/src/key_dispatch.rs       |  50 ++-
crates/gpui2/src/window.rs             |  62 ++++
crates/storybook2/src/stories/focus.rs |   6 
6 files changed, 317 insertions(+), 251 deletions(-)

Detailed changes

crates/editor2/src/element.rs 🔗

@@ -15,10 +15,10 @@ use crate::{
 use anyhow::Result;
 use collections::{BTreeMap, HashMap};
 use gpui::{
-    black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
-    BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element,
-    ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler,
-    KeyContext, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, MouseButton,
+    black, hsla, point, px, relative, size, transparent_black, Action, ActionListener, AnyElement,
+    AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase,
+    Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla,
+    InputHandler, KeyContext, KeyDownEvent, KeyMatch, Line, LineLayout, Modifiers, MouseButton,
     MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size,
     Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout,
 };
@@ -2459,7 +2459,166 @@ impl Element<Editor> for EditorElement {
             cx.with_key_dispatch(
                 dispatch_context,
                 Some(editor.focus_handle.clone()),
-                |_, _| {},
+                |_, cx| {
+                    handle_action(cx, Editor::move_left);
+                    handle_action(cx, Editor::move_right);
+                    handle_action(cx, Editor::move_down);
+                    handle_action(cx, Editor::move_up);
+                    // on_action(cx, Editor::new_file); todo!()
+                    // on_action(cx, Editor::new_file_in_direction); todo!()
+                    handle_action(cx, Editor::cancel);
+                    handle_action(cx, Editor::newline);
+                    handle_action(cx, Editor::newline_above);
+                    handle_action(cx, Editor::newline_below);
+                    handle_action(cx, Editor::backspace);
+                    handle_action(cx, Editor::delete);
+                    handle_action(cx, Editor::tab);
+                    handle_action(cx, Editor::tab_prev);
+                    handle_action(cx, Editor::indent);
+                    handle_action(cx, Editor::outdent);
+                    handle_action(cx, Editor::delete_line);
+                    handle_action(cx, Editor::join_lines);
+                    handle_action(cx, Editor::sort_lines_case_sensitive);
+                    handle_action(cx, Editor::sort_lines_case_insensitive);
+                    handle_action(cx, Editor::reverse_lines);
+                    handle_action(cx, Editor::shuffle_lines);
+                    handle_action(cx, Editor::convert_to_upper_case);
+                    handle_action(cx, Editor::convert_to_lower_case);
+                    handle_action(cx, Editor::convert_to_title_case);
+                    handle_action(cx, Editor::convert_to_snake_case);
+                    handle_action(cx, Editor::convert_to_kebab_case);
+                    handle_action(cx, Editor::convert_to_upper_camel_case);
+                    handle_action(cx, Editor::convert_to_lower_camel_case);
+                    handle_action(cx, Editor::delete_to_previous_word_start);
+                    handle_action(cx, Editor::delete_to_previous_subword_start);
+                    handle_action(cx, Editor::delete_to_next_word_end);
+                    handle_action(cx, Editor::delete_to_next_subword_end);
+                    handle_action(cx, Editor::delete_to_beginning_of_line);
+                    handle_action(cx, Editor::delete_to_end_of_line);
+                    handle_action(cx, Editor::cut_to_end_of_line);
+                    handle_action(cx, Editor::duplicate_line);
+                    handle_action(cx, Editor::move_line_up);
+                    handle_action(cx, Editor::move_line_down);
+                    handle_action(cx, Editor::transpose);
+                    handle_action(cx, Editor::cut);
+                    handle_action(cx, Editor::copy);
+                    handle_action(cx, Editor::paste);
+                    handle_action(cx, Editor::undo);
+                    handle_action(cx, Editor::redo);
+                    handle_action(cx, Editor::move_page_up);
+                    handle_action(cx, Editor::move_page_down);
+                    handle_action(cx, Editor::next_screen);
+                    handle_action(cx, Editor::scroll_cursor_top);
+                    handle_action(cx, Editor::scroll_cursor_center);
+                    handle_action(cx, Editor::scroll_cursor_bottom);
+                    handle_action(cx, |editor, _: &LineDown, cx| {
+                        editor.scroll_screen(&ScrollAmount::Line(1.), cx)
+                    });
+                    handle_action(cx, |editor, _: &LineUp, cx| {
+                        editor.scroll_screen(&ScrollAmount::Line(-1.), cx)
+                    });
+                    handle_action(cx, |editor, _: &HalfPageDown, cx| {
+                        editor.scroll_screen(&ScrollAmount::Page(0.5), cx)
+                    });
+                    handle_action(cx, |editor, _: &HalfPageUp, cx| {
+                        editor.scroll_screen(&ScrollAmount::Page(-0.5), cx)
+                    });
+                    handle_action(cx, |editor, _: &PageDown, cx| {
+                        editor.scroll_screen(&ScrollAmount::Page(1.), cx)
+                    });
+                    handle_action(cx, |editor, _: &PageUp, cx| {
+                        editor.scroll_screen(&ScrollAmount::Page(-1.), cx)
+                    });
+                    handle_action(cx, Editor::move_to_previous_word_start);
+                    handle_action(cx, Editor::move_to_previous_subword_start);
+                    handle_action(cx, Editor::move_to_next_word_end);
+                    handle_action(cx, Editor::move_to_next_subword_end);
+                    handle_action(cx, Editor::move_to_beginning_of_line);
+                    handle_action(cx, Editor::move_to_end_of_line);
+                    handle_action(cx, Editor::move_to_start_of_paragraph);
+                    handle_action(cx, Editor::move_to_end_of_paragraph);
+                    handle_action(cx, Editor::move_to_beginning);
+                    handle_action(cx, Editor::move_to_end);
+                    handle_action(cx, Editor::select_up);
+                    handle_action(cx, Editor::select_down);
+                    handle_action(cx, Editor::select_left);
+                    handle_action(cx, Editor::select_right);
+                    handle_action(cx, Editor::select_to_previous_word_start);
+                    handle_action(cx, Editor::select_to_previous_subword_start);
+                    handle_action(cx, Editor::select_to_next_word_end);
+                    handle_action(cx, Editor::select_to_next_subword_end);
+                    handle_action(cx, Editor::select_to_beginning_of_line);
+                    handle_action(cx, Editor::select_to_end_of_line);
+                    handle_action(cx, Editor::select_to_start_of_paragraph);
+                    handle_action(cx, Editor::select_to_end_of_paragraph);
+                    handle_action(cx, Editor::select_to_beginning);
+                    handle_action(cx, Editor::select_to_end);
+                    handle_action(cx, Editor::select_all);
+                    handle_action(cx, |editor, action, cx| {
+                        editor.select_all_matches(action, cx).log_err();
+                    });
+                    handle_action(cx, Editor::select_line);
+                    handle_action(cx, Editor::split_selection_into_lines);
+                    handle_action(cx, Editor::add_selection_above);
+                    handle_action(cx, Editor::add_selection_below);
+                    handle_action(cx, |editor, action, cx| {
+                        editor.select_next(action, cx).log_err();
+                    });
+                    handle_action(cx, |editor, action, cx| {
+                        editor.select_previous(action, cx).log_err();
+                    });
+                    handle_action(cx, Editor::toggle_comments);
+                    handle_action(cx, Editor::select_larger_syntax_node);
+                    handle_action(cx, Editor::select_smaller_syntax_node);
+                    handle_action(cx, Editor::move_to_enclosing_bracket);
+                    handle_action(cx, Editor::undo_selection);
+                    handle_action(cx, Editor::redo_selection);
+                    handle_action(cx, Editor::go_to_diagnostic);
+                    handle_action(cx, Editor::go_to_prev_diagnostic);
+                    handle_action(cx, Editor::go_to_hunk);
+                    handle_action(cx, Editor::go_to_prev_hunk);
+                    handle_action(cx, Editor::go_to_definition);
+                    handle_action(cx, Editor::go_to_definition_split);
+                    handle_action(cx, Editor::go_to_type_definition);
+                    handle_action(cx, Editor::go_to_type_definition_split);
+                    handle_action(cx, Editor::fold);
+                    handle_action(cx, Editor::fold_at);
+                    handle_action(cx, Editor::unfold_lines);
+                    handle_action(cx, Editor::unfold_at);
+                    handle_action(cx, Editor::fold_selected_ranges);
+                    handle_action(cx, Editor::show_completions);
+                    handle_action(cx, Editor::toggle_code_actions);
+                    // on_action(cx, Editor::open_excerpts); todo!()
+                    handle_action(cx, Editor::toggle_soft_wrap);
+                    handle_action(cx, Editor::toggle_inlay_hints);
+                    handle_action(cx, Editor::reveal_in_finder);
+                    handle_action(cx, Editor::copy_path);
+                    handle_action(cx, Editor::copy_relative_path);
+                    handle_action(cx, Editor::copy_highlight_json);
+                    handle_action(cx, |editor, action, cx| {
+                        editor
+                            .format(action, cx)
+                            .map(|task| task.detach_and_log_err(cx));
+                    });
+                    handle_action(cx, Editor::restart_language_server);
+                    handle_action(cx, Editor::show_character_palette);
+                    // on_action(cx, Editor::confirm_completion); todo!()
+                    handle_action(cx, |editor, action, cx| {
+                        editor
+                            .confirm_code_action(action, cx)
+                            .map(|task| task.detach_and_log_err(cx));
+                    });
+                    // on_action(cx, Editor::rename); todo!()
+                    // on_action(cx, Editor::confirm_rename); todo!()
+                    // on_action(cx, Editor::find_all_references); todo!()
+                    handle_action(cx, Editor::next_copilot_suggestion);
+                    handle_action(cx, Editor::previous_copilot_suggestion);
+                    handle_action(cx, Editor::copilot_suggest);
+                    handle_action(cx, Editor::context_menu_first);
+                    handle_action(cx, Editor::context_menu_prev);
+                    handle_action(cx, Editor::context_menu_next);
+                    handle_action(cx, Editor::context_menu_last);
+                },
             )
         });
     }
@@ -3995,197 +4154,14 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
 //     }
 // }
 
-fn build_key_listeners(
-    global_element_id: GlobalElementId,
-) -> impl IntoIterator<Item = (TypeId, KeyListener<Editor>)> {
-    [
-        build_action_listener(Editor::move_left),
-        build_action_listener(Editor::move_right),
-        build_action_listener(Editor::move_down),
-        build_action_listener(Editor::move_up),
-        // build_action_listener(Editor::new_file), todo!()
-        // build_action_listener(Editor::new_file_in_direction), todo!()
-        build_action_listener(Editor::cancel),
-        build_action_listener(Editor::newline),
-        build_action_listener(Editor::newline_above),
-        build_action_listener(Editor::newline_below),
-        build_action_listener(Editor::backspace),
-        build_action_listener(Editor::delete),
-        build_action_listener(Editor::tab),
-        build_action_listener(Editor::tab_prev),
-        build_action_listener(Editor::indent),
-        build_action_listener(Editor::outdent),
-        build_action_listener(Editor::delete_line),
-        build_action_listener(Editor::join_lines),
-        build_action_listener(Editor::sort_lines_case_sensitive),
-        build_action_listener(Editor::sort_lines_case_insensitive),
-        build_action_listener(Editor::reverse_lines),
-        build_action_listener(Editor::shuffle_lines),
-        build_action_listener(Editor::convert_to_upper_case),
-        build_action_listener(Editor::convert_to_lower_case),
-        build_action_listener(Editor::convert_to_title_case),
-        build_action_listener(Editor::convert_to_snake_case),
-        build_action_listener(Editor::convert_to_kebab_case),
-        build_action_listener(Editor::convert_to_upper_camel_case),
-        build_action_listener(Editor::convert_to_lower_camel_case),
-        build_action_listener(Editor::delete_to_previous_word_start),
-        build_action_listener(Editor::delete_to_previous_subword_start),
-        build_action_listener(Editor::delete_to_next_word_end),
-        build_action_listener(Editor::delete_to_next_subword_end),
-        build_action_listener(Editor::delete_to_beginning_of_line),
-        build_action_listener(Editor::delete_to_end_of_line),
-        build_action_listener(Editor::cut_to_end_of_line),
-        build_action_listener(Editor::duplicate_line),
-        build_action_listener(Editor::move_line_up),
-        build_action_listener(Editor::move_line_down),
-        build_action_listener(Editor::transpose),
-        build_action_listener(Editor::cut),
-        build_action_listener(Editor::copy),
-        build_action_listener(Editor::paste),
-        build_action_listener(Editor::undo),
-        build_action_listener(Editor::redo),
-        build_action_listener(Editor::move_page_up),
-        build_action_listener(Editor::move_page_down),
-        build_action_listener(Editor::next_screen),
-        build_action_listener(Editor::scroll_cursor_top),
-        build_action_listener(Editor::scroll_cursor_center),
-        build_action_listener(Editor::scroll_cursor_bottom),
-        build_action_listener(|editor, _: &LineDown, cx| {
-            editor.scroll_screen(&ScrollAmount::Line(1.), cx)
-        }),
-        build_action_listener(|editor, _: &LineUp, cx| {
-            editor.scroll_screen(&ScrollAmount::Line(-1.), cx)
-        }),
-        build_action_listener(|editor, _: &HalfPageDown, cx| {
-            editor.scroll_screen(&ScrollAmount::Page(0.5), cx)
-        }),
-        build_action_listener(|editor, _: &HalfPageUp, cx| {
-            editor.scroll_screen(&ScrollAmount::Page(-0.5), cx)
-        }),
-        build_action_listener(|editor, _: &PageDown, cx| {
-            editor.scroll_screen(&ScrollAmount::Page(1.), cx)
-        }),
-        build_action_listener(|editor, _: &PageUp, cx| {
-            editor.scroll_screen(&ScrollAmount::Page(-1.), cx)
-        }),
-        build_action_listener(Editor::move_to_previous_word_start),
-        build_action_listener(Editor::move_to_previous_subword_start),
-        build_action_listener(Editor::move_to_next_word_end),
-        build_action_listener(Editor::move_to_next_subword_end),
-        build_action_listener(Editor::move_to_beginning_of_line),
-        build_action_listener(Editor::move_to_end_of_line),
-        build_action_listener(Editor::move_to_start_of_paragraph),
-        build_action_listener(Editor::move_to_end_of_paragraph),
-        build_action_listener(Editor::move_to_beginning),
-        build_action_listener(Editor::move_to_end),
-        build_action_listener(Editor::select_up),
-        build_action_listener(Editor::select_down),
-        build_action_listener(Editor::select_left),
-        build_action_listener(Editor::select_right),
-        build_action_listener(Editor::select_to_previous_word_start),
-        build_action_listener(Editor::select_to_previous_subword_start),
-        build_action_listener(Editor::select_to_next_word_end),
-        build_action_listener(Editor::select_to_next_subword_end),
-        build_action_listener(Editor::select_to_beginning_of_line),
-        build_action_listener(Editor::select_to_end_of_line),
-        build_action_listener(Editor::select_to_start_of_paragraph),
-        build_action_listener(Editor::select_to_end_of_paragraph),
-        build_action_listener(Editor::select_to_beginning),
-        build_action_listener(Editor::select_to_end),
-        build_action_listener(Editor::select_all),
-        build_action_listener(|editor, action, cx| {
-            editor.select_all_matches(action, cx).log_err();
-        }),
-        build_action_listener(Editor::select_line),
-        build_action_listener(Editor::split_selection_into_lines),
-        build_action_listener(Editor::add_selection_above),
-        build_action_listener(Editor::add_selection_below),
-        build_action_listener(|editor, action, cx| {
-            editor.select_next(action, cx).log_err();
-        }),
-        build_action_listener(|editor, action, cx| {
-            editor.select_previous(action, cx).log_err();
-        }),
-        build_action_listener(Editor::toggle_comments),
-        build_action_listener(Editor::select_larger_syntax_node),
-        build_action_listener(Editor::select_smaller_syntax_node),
-        build_action_listener(Editor::move_to_enclosing_bracket),
-        build_action_listener(Editor::undo_selection),
-        build_action_listener(Editor::redo_selection),
-        build_action_listener(Editor::go_to_diagnostic),
-        build_action_listener(Editor::go_to_prev_diagnostic),
-        build_action_listener(Editor::go_to_hunk),
-        build_action_listener(Editor::go_to_prev_hunk),
-        build_action_listener(Editor::go_to_definition),
-        build_action_listener(Editor::go_to_definition_split),
-        build_action_listener(Editor::go_to_type_definition),
-        build_action_listener(Editor::go_to_type_definition_split),
-        build_action_listener(Editor::fold),
-        build_action_listener(Editor::fold_at),
-        build_action_listener(Editor::unfold_lines),
-        build_action_listener(Editor::unfold_at),
-        build_action_listener(Editor::fold_selected_ranges),
-        build_action_listener(Editor::show_completions),
-        build_action_listener(Editor::toggle_code_actions),
-        // build_action_listener(Editor::open_excerpts), todo!()
-        build_action_listener(Editor::toggle_soft_wrap),
-        build_action_listener(Editor::toggle_inlay_hints),
-        build_action_listener(Editor::reveal_in_finder),
-        build_action_listener(Editor::copy_path),
-        build_action_listener(Editor::copy_relative_path),
-        build_action_listener(Editor::copy_highlight_json),
-        build_action_listener(|editor, action, cx| {
-            editor
-                .format(action, cx)
-                .map(|task| task.detach_and_log_err(cx));
-        }),
-        build_action_listener(Editor::restart_language_server),
-        build_action_listener(Editor::show_character_palette),
-        // build_action_listener(Editor::confirm_completion), todo!()
-        build_action_listener(|editor, action, cx| {
-            editor
-                .confirm_code_action(action, cx)
-                .map(|task| task.detach_and_log_err(cx));
-        }),
-        // build_action_listener(Editor::rename), todo!()
-        // build_action_listener(Editor::confirm_rename), todo!()
-        // build_action_listener(Editor::find_all_references), todo!()
-        build_action_listener(Editor::next_copilot_suggestion),
-        build_action_listener(Editor::previous_copilot_suggestion),
-        build_action_listener(Editor::copilot_suggest),
-        build_action_listener(Editor::context_menu_first),
-        build_action_listener(Editor::context_menu_prev),
-        build_action_listener(Editor::context_menu_next),
-        build_action_listener(Editor::context_menu_last),
-    ]
-}
-
-fn build_key_listener<T: 'static>(
-    listener: impl Fn(
-            &mut Editor,
-            &T,
-            &[&KeyContext],
-            DispatchPhase,
-            &mut ViewContext<Editor>,
-        ) -> Option<Box<dyn Action>>
-        + 'static,
-) -> (TypeId, KeyListener<Editor>) {
-    (
-        TypeId::of::<T>(),
-        Box::new(move |editor, event, dispatch_context, phase, cx| {
-            let key_event = event.downcast_ref::<T>()?;
-            listener(editor, key_event, dispatch_context, phase, cx)
-        }),
-    )
-}
-
-fn build_action_listener<T: Action>(
+fn handle_action<T: Action>(
+    cx: &mut ViewContext<Editor>,
     listener: impl Fn(&mut Editor, &T, &mut ViewContext<Editor>) + 'static,
-) -> (TypeId, KeyListener<Editor>) {
-    build_key_listener(move |editor, action: &T, dispatch_context, phase, cx| {
+) {
+    cx.on_action(TypeId::of::<T>(), move |editor, action, phase, cx| {
+        let action = action.downcast_ref().unwrap();
         if phase == DispatchPhase::Bubble {
             listener(editor, action, cx);
         }
-        None
     })
 }

crates/gpui2/src/elements/div.rs 🔗

@@ -234,6 +234,7 @@ where
                 element_state.focus_handle.take(),
                 cx,
                 |focus_handle, cx| {
+                    this.interactivity.initialize(cx);
                     element_state.focus_handle = focus_handle;
                     for child in &mut this.children {
                         child.initialize(view_state, cx);

crates/gpui2/src/interactive.rs 🔗

@@ -165,43 +165,40 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     }
 
     /// Capture the given action, fires during the capture phase
-    fn capture_action<A: 'static>(
+    fn capture_action<A: Action>(
         mut self,
         listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
     ) -> Self
     where
         Self: Sized,
     {
-        self.stateless_interactivity().key_listeners.push((
+        self.stateless_interactivity().action_listeners.push((
             TypeId::of::<A>(),
-            Box::new(move |view, action, _dipatch_context, phase, cx| {
+            Box::new(move |view, action, phase, cx| {
                 let action = action.downcast_ref().unwrap();
                 if phase == DispatchPhase::Capture {
                     listener(view, action, cx)
                 }
-                None
             }),
         ));
         self
     }
 
     /// Add a listener for the given action, fires during the bubble event phase
-    fn on_action<A: 'static>(
+    fn on_action<A: Action>(
         mut self,
         listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
     ) -> Self
     where
         Self: Sized,
     {
-        self.stateless_interactivity().key_listeners.push((
+        self.stateless_interactivity().action_listeners.push((
             TypeId::of::<A>(),
-            Box::new(move |view, action, _dispatch_context, phase, cx| {
+            Box::new(move |view, action, phase, cx| {
                 let action = action.downcast_ref().unwrap();
                 if phase == DispatchPhase::Bubble {
                     listener(view, action, cx)
                 }
-
-                None
             }),
         ));
         self
@@ -214,14 +211,11 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interactivity().key_listeners.push((
-            TypeId::of::<KeyDownEvent>(),
-            Box::new(move |view, event, _, phase, cx| {
-                let event = event.downcast_ref().unwrap();
-                listener(view, event, phase, cx);
-                None
-            }),
-        ));
+        self.stateless_interactivity()
+            .key_down_listeners
+            .push(Box::new(move |view, event, phase, cx| {
+                listener(view, event, phase, cx)
+            }));
         self
     }
 
@@ -232,14 +226,11 @@ pub trait StatelessInteractive<V: 'static>: Element<V> {
     where
         Self: Sized,
     {
-        self.stateless_interactivity().key_listeners.push((
-            TypeId::of::<KeyUpEvent>(),
-            Box::new(move |view, event, _, phase, cx| {
-                let event = event.downcast_ref().unwrap();
-                listener(view, event, phase, cx);
-                None
-            }),
-        ));
+        self.stateless_interactivity()
+            .key_up_listeners
+            .push(Box::new(move |view, event, phase, cx| {
+                listener(view, event, phase, cx)
+            }));
         self
     }
 
@@ -439,6 +430,26 @@ pub trait ElementInteractivity<V: 'static>: 'static {
         }
     }
 
+    fn initialize(&mut self, cx: &mut ViewContext<V>) {
+        let stateless = self.as_stateless_mut();
+
+        for listener in stateless.key_down_listeners.drain(..) {
+            cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| {
+                listener(state, event, phase, cx);
+            })
+        }
+
+        for listener in stateless.key_up_listeners.drain(..) {
+            cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| {
+                listener(state, event, phase, cx);
+            })
+        }
+
+        for (action_type, listener) in stateless.action_listeners.drain(..) {
+            cx.on_action(action_type, listener)
+        }
+    }
+
     fn paint(
         &mut self,
         bounds: Bounds<Pixels>,
@@ -765,7 +776,9 @@ pub struct StatelessInteractivity<V> {
     pub mouse_up_listeners: SmallVec<[MouseUpListener<V>; 2]>,
     pub mouse_move_listeners: SmallVec<[MouseMoveListener<V>; 2]>,
     pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener<V>; 2]>,
-    pub key_listeners: SmallVec<[(TypeId, KeyListener<V>); 32]>,
+    pub key_down_listeners: SmallVec<[KeyDownListener<V>; 2]>,
+    pub key_up_listeners: SmallVec<[KeyUpListener<V>; 2]>,
+    pub action_listeners: SmallVec<[(TypeId, ActionListener<V>); 8]>,
     pub hover_style: StyleRefinement,
     pub group_hover_style: Option<GroupStyle>,
     drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>,
@@ -867,7 +880,9 @@ impl<V> Default for StatelessInteractivity<V> {
             mouse_up_listeners: SmallVec::new(),
             mouse_move_listeners: SmallVec::new(),
             scroll_wheel_listeners: SmallVec::new(),
-            key_listeners: SmallVec::new(),
+            key_down_listeners: SmallVec::new(),
+            key_up_listeners: SmallVec::new(),
+            action_listeners: SmallVec::new(),
             hover_style: StyleRefinement::default(),
             group_hover_style: None,
             drag_over_styles: SmallVec::new(),
@@ -1202,16 +1217,14 @@ pub(crate) type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>)
 
 pub(crate) type TooltipBuilder<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
 
-pub type KeyListener<V> = Box<
-    dyn Fn(
-            &mut V,
-            &dyn Any,
-            &[&KeyContext],
-            DispatchPhase,
-            &mut ViewContext<V>,
-        ) -> Option<Box<dyn Action>>
-        + 'static,
->;
+pub(crate) type KeyDownListener<V> =
+    Box<dyn Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
+
+pub(crate) type KeyUpListener<V> =
+    Box<dyn Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
+
+pub type ActionListener<V> =
+    Box<dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) + 'static>;
 
 #[cfg(test)]
 mod test {

crates/gpui2/src/key_dispatch.rs 🔗

@@ -69,6 +69,7 @@ impl KeyDispatcher {
         });
         self.node_stack.push(node_id);
         if !context.is_empty() {
+            self.active_node().context = context.clone();
             self.context_stack.push(context);
             if let Some((context_stack, matcher)) = old_dispatcher
                 .keystroke_matchers
@@ -153,6 +154,7 @@ impl KeyDispatcher {
         // Capture phase
         self.context_stack.clear();
         cx.propagate_event = true;
+
         for node_id in &dispatch_path {
             let node = &self.nodes[node_id.0];
             if !node.context.is_empty() {
@@ -193,18 +195,16 @@ impl KeyDispatcher {
                         );
                     }
 
-                    if let Some(keystroke_matcher) = self
+                    let keystroke_matcher = self
                         .keystroke_matchers
                         .get_mut(self.context_stack.as_slice())
+                        .unwrap();
+                    if let KeyMatch::Some(action) = keystroke_matcher
+                        .match_keystroke(&key_down_event.keystroke, self.context_stack.as_slice())
                     {
-                        if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke(
-                            &key_down_event.keystroke,
-                            self.context_stack.as_slice(),
-                        ) {
-                            self.dispatch_action_on_node(*node_id, action, cx);
-                            if !cx.propagate_event {
-                                return;
-                            }
+                        self.dispatch_action_on_node(*node_id, action, cx);
+                        if !cx.propagate_event {
+                            return;
                         }
                     }
                 }
@@ -236,10 +236,17 @@ impl KeyDispatcher {
         // Capture phase
         for node_id in &dispatch_path {
             let node = &self.nodes[node_id.0];
-            for ActionListener { listener, .. } in &node.action_listeners {
-                listener(&action, DispatchPhase::Capture, cx);
-                if !cx.propagate_event {
-                    return;
+            for ActionListener {
+                action_type,
+                listener,
+            } in &node.action_listeners
+            {
+                let any_action = action.as_any();
+                if *action_type == any_action.type_id() {
+                    listener(any_action, DispatchPhase::Capture, cx);
+                    if !cx.propagate_event {
+                        return;
+                    }
                 }
             }
         }
@@ -247,11 +254,18 @@ impl KeyDispatcher {
         // Bubble phase
         for node_id in dispatch_path.iter().rev() {
             let node = &self.nodes[node_id.0];
-            for ActionListener { listener, .. } in &node.action_listeners {
-                cx.propagate_event = false; // Actions stop propagation by default during the bubble phase
-                listener(&action, DispatchPhase::Capture, cx);
-                if !cx.propagate_event {
-                    return;
+            for ActionListener {
+                action_type,
+                listener,
+            } in &node.action_listeners
+            {
+                let any_action = action.as_any();
+                if *action_type == any_action.type_id() {
+                    cx.propagate_event = false; // Actions stop propagation by default during the bubble phase
+                    listener(any_action, DispatchPhase::Bubble, cx);
+                    if !cx.propagate_event {
+                        return;
+                    }
                 }
             }
         }

crates/gpui2/src/window.rs 🔗

@@ -713,6 +713,42 @@ impl<'a> WindowContext<'a> {
             ))
     }
 
+    /// Register a key event listener on the window for the current frame. The type of event
+    /// is determined by the first parameter of the given listener. When the next frame is rendered
+    /// the listener will be cleared.
+    ///
+    /// This is a fairly low-level method, so prefer using event handlers on elements unless you have
+    /// a specific need to register a global listener.
+    pub fn on_key_event<Event: 'static>(
+        &mut self,
+        handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static,
+    ) {
+        let key_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap();
+        key_dispatcher.on_key_event(Box::new(move |event, phase, cx| {
+            if let Some(event) = event.downcast_ref::<Event>() {
+                handler(event, phase, cx)
+            }
+        }));
+    }
+
+    /// Register an action listener on the window for the current frame. The type of action
+    /// is determined by the first parameter of the given listener. When the next frame is rendered
+    /// the listener will be cleared.
+    ///
+    /// This is a fairly low-level method, so prefer using action handlers on elements unless you have
+    /// a specific need to register a global listener.
+    pub fn on_action(
+        &mut self,
+        action_type: TypeId,
+        handler: impl Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static,
+    ) {
+        let key_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap();
+        key_dispatcher.on_action(
+            action_type,
+            Box::new(move |action, phase, cx| handler(action, phase, cx)),
+        );
+    }
+
     /// The position of the mouse relative to the window.
     pub fn mouse_position(&self) -> Point<Pixels> {
         self.window.mouse_position
@@ -1955,6 +1991,32 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         });
     }
 
+    pub fn on_key_event<Event: 'static>(
+        &mut self,
+        handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext<V>) + 'static,
+    ) {
+        let handle = self.view();
+        self.window_cx.on_key_event(move |event, phase, cx| {
+            handle.update(cx, |view, cx| {
+                handler(view, event, phase, cx);
+            })
+        });
+    }
+
+    pub fn on_action(
+        &mut self,
+        action_type: TypeId,
+        handler: impl Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) + 'static,
+    ) {
+        let handle = self.view();
+        self.window_cx
+            .on_action(action_type, move |action, phase, cx| {
+                handle.update(cx, |view, cx| {
+                    handler(view, action, phase, cx);
+                })
+            });
+    }
+
     /// Set an input handler, such as [ElementInputHandler], which interfaces with the
     /// platform to receive textual input with proper integration with concerns such
     /// as IME interactions.

crates/storybook2/src/stories/focus.rs 🔗

@@ -39,10 +39,10 @@ impl Render for FocusStory {
             .focusable()
             .context("parent")
             .on_action(|_, action: &ActionA, cx| {
-                println!("Action A dispatched on parent during");
+                println!("Action A dispatched on parent");
             })
             .on_action(|_, action: &ActionB, cx| {
-                println!("Action B dispatched on parent during");
+                println!("Action B dispatched on parent");
             })
             .on_focus(|_, _, _| println!("Parent focused"))
             .on_blur(|_, _, _| println!("Parent blurred"))
@@ -79,7 +79,7 @@ impl Render for FocusStory {
                     .track_focus(&child_2)
                     .context("child-2")
                     .on_action(|_, action: &ActionC, cx| {
-                        println!("Action C dispatched on child 2 during");
+                        println!("Action C dispatched on child 2");
                     })
                     .w_full()
                     .h_6()