From 9b63ba6205be428e4fbf18bf411c1f617b6e4398 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 9 Jul 2025 17:44:32 -0500 Subject: [PATCH] gpui: Add `cx.intercept_keystrokes` API to intercept keystrokes before action dispatch (#34084) Closes #ISSUE `cx.intercept_keystrokes` functions as a sibling API to `cx.observe_keystrokes`. Under the hood the two API's are basically identical, however, `cx.observe_keystrokes` runs _after_ all event dispatch handling (including action dispatch) while `cx.intercept_keystrokes` runs _before_. This allows for `cx.stop_propagation()` calls within the `cx.intercept_keystrokes` callback to prevent action dispatch. The motivating example usage behind this API is also included in this PR. It is used as part of a keystroke input component that needs to intercept keystrokes before action dispatch to display them. cc: @mikayla-maki Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/gpui/src/app.rs | 28 ++++++++++++++++ crates/gpui/src/window.rs | 33 ++++++++++++++++++- crates/settings_ui/src/keybindings.rs | 47 ++++++++++++++++++--------- 3 files changed, 92 insertions(+), 16 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index ef462ae084bd66ee2e851772e5ab659906aa446a..6cecfcc0e42b239dc98db9391650f0618530d52c 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -272,6 +272,7 @@ pub struct App { // TypeId is the type of the event that the listener callback expects pub(crate) event_listeners: SubscriberSet, pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>, + pub(crate) keystroke_interceptors: SubscriberSet<(), KeystrokeObserver>, pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>, pub(crate) release_listeners: SubscriberSet, pub(crate) global_observers: SubscriberSet, @@ -344,6 +345,7 @@ impl App { event_listeners: SubscriberSet::new(), release_listeners: SubscriberSet::new(), keystroke_observers: SubscriberSet::new(), + keystroke_interceptors: SubscriberSet::new(), keyboard_layout_observers: SubscriberSet::new(), global_observers: SubscriberSet::new(), quit_observers: SubscriberSet::new(), @@ -1322,6 +1324,32 @@ impl App { ) } + /// Register a callback to be invoked when a keystroke is received by the application + /// in any window. Note that this fires _before_ all other action and event mechanisms have resolved + /// unlike [`App::observe_keystrokes`] which fires after. This means that `cx.stop_propagation` calls + /// within interceptors will prevent action dispatch + pub fn intercept_keystrokes( + &mut self, + mut f: impl FnMut(&KeystrokeEvent, &mut Window, &mut App) + 'static, + ) -> Subscription { + fn inner( + keystroke_interceptors: &SubscriberSet<(), KeystrokeObserver>, + handler: KeystrokeObserver, + ) -> Subscription { + let (subscription, activate) = keystroke_interceptors.insert((), handler); + activate(); + subscription + } + + inner( + &mut self.keystroke_interceptors, + Box::new(move |event, window, cx| { + f(event, window, cx); + true + }), + ) + } + /// Register key bindings. pub fn bind_keys(&mut self, bindings: impl IntoIterator) { self.keymap.borrow_mut().add_bindings(bindings); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 8c01b8afcfd2b7948cabe925550008590b3c3576..e9145bd9f5181da662a882f0f12e340e34d4822f 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1369,6 +1369,31 @@ impl Window { }); } + pub(crate) fn dispatch_keystroke_interceptors( + &mut self, + event: &dyn Any, + context_stack: Vec, + cx: &mut App, + ) { + let Some(key_down_event) = event.downcast_ref::() else { + return; + }; + + cx.keystroke_interceptors + .clone() + .retain(&(), move |callback| { + (callback)( + &KeystrokeEvent { + keystroke: key_down_event.keystroke.clone(), + action: None, + context_stack: context_stack.clone(), + }, + self, + cx, + ) + }); + } + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities /// that are currently on the stack to be returned to the app. pub fn defer(&self, cx: &mut App, f: impl FnOnce(&mut Window, &mut App) + 'static) { @@ -3522,6 +3547,13 @@ impl Window { return; }; + cx.propagate_event = true; + self.dispatch_keystroke_interceptors(event, self.context_stack(), cx); + if !cx.propagate_event { + self.finish_dispatch_key_event(event, dispatch_path, self.context_stack(), cx); + return; + } + let mut currently_pending = self.pending_input.take().unwrap_or_default(); if currently_pending.focus.is_some() && currently_pending.focus != self.focus { currently_pending = PendingInput::default(); @@ -3570,7 +3602,6 @@ impl Window { return; } - cx.propagate_event = true; for binding in match_result.bindings { self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx); if !cx.propagate_event { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 2073a9bfc37f626b6cf03191eb5ae7ea34fd097a..9f6992173e91467060b4590b2c4bb6b18bb82436 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -916,7 +916,7 @@ impl KeybindingEditorModal { window: &mut Window, cx: &mut App, ) -> Self { - let keybind_editor = cx.new(KeystrokeInput::new); + let keybind_editor = cx.new(|cx| KeystrokeInput::new(window, cx)); let context_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); @@ -1315,14 +1315,22 @@ async fn save_keybinding_update( struct KeystrokeInput { keystrokes: Vec, focus_handle: FocusHandle, + intercept_subscription: Option, + _focus_subscriptions: [Subscription; 2], } impl KeystrokeInput { - fn new(cx: &mut Context) -> Self { + fn new(window: &mut Window, cx: &mut Context) -> Self { let focus_handle = cx.focus_handle(); + let _focus_subscriptions = [ + cx.on_focus_in(&focus_handle, window, Self::on_focus_in), + cx.on_focus_out(&focus_handle, window, Self::on_focus_out), + ]; Self { keystrokes: Vec::new(), focus_handle, + intercept_subscription: None, + _focus_subscriptions, } } @@ -1351,21 +1359,13 @@ impl KeystrokeInput { cx.notify(); } - fn on_key_down( - &mut self, - event: &gpui::KeyDownEvent, - _window: &mut Window, - cx: &mut Context, - ) { - if event.is_held { - return; - } + fn handle_keystroke(&mut self, keystroke: &Keystroke, cx: &mut Context) { if let Some(last) = self.keystrokes.last_mut() && last.key.is_empty() { - *last = event.keystroke.clone(); - } else { - self.keystrokes.push(event.keystroke.clone()); + *last = keystroke.clone(); + } else if Some(keystroke) != self.keystrokes.last() { + self.keystrokes.push(keystroke.clone()); } cx.stop_propagation(); cx.notify(); @@ -1391,6 +1391,24 @@ impl KeystrokeInput { cx.notify(); } + fn on_focus_in(&mut self, _window: &mut Window, cx: &mut Context) { + if self.intercept_subscription.is_none() { + let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, _window, cx| { + this.handle_keystroke(&event.keystroke, cx); + }); + self.intercept_subscription = Some(cx.intercept_keystrokes(listener)) + } + } + + fn on_focus_out( + &mut self, + _event: gpui::FocusOutEvent, + _window: &mut Window, + _cx: &mut Context, + ) { + self.intercept_subscription.take(); + } + fn keystrokes(&self) -> &[Keystroke] { if self .keystrokes @@ -1418,7 +1436,6 @@ impl Render for KeystrokeInput { .id("keybinding_input") .track_focus(&self.focus_handle) .on_modifiers_changed(cx.listener(Self::on_modifiers_changed)) - .on_key_down(cx.listener(Self::on_key_down)) .on_key_up(cx.listener(Self::on_key_up)) .focus(|mut style| { style.border_color = Some(colors.border_focused);