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 ...
This commit is contained in:
parent
862e733ef5
commit
9b63ba6205
3 changed files with 92 additions and 16 deletions
|
@ -272,6 +272,7 @@ pub struct App {
|
||||||
// TypeId is the type of the event that the listener callback expects
|
// TypeId is the type of the event that the listener callback expects
|
||||||
pub(crate) event_listeners: SubscriberSet<EntityId, (TypeId, Listener)>,
|
pub(crate) event_listeners: SubscriberSet<EntityId, (TypeId, Listener)>,
|
||||||
pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>,
|
pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>,
|
||||||
|
pub(crate) keystroke_interceptors: SubscriberSet<(), KeystrokeObserver>,
|
||||||
pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>,
|
pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>,
|
||||||
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
|
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
|
||||||
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
|
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
|
||||||
|
@ -344,6 +345,7 @@ impl App {
|
||||||
event_listeners: SubscriberSet::new(),
|
event_listeners: SubscriberSet::new(),
|
||||||
release_listeners: SubscriberSet::new(),
|
release_listeners: SubscriberSet::new(),
|
||||||
keystroke_observers: SubscriberSet::new(),
|
keystroke_observers: SubscriberSet::new(),
|
||||||
|
keystroke_interceptors: SubscriberSet::new(),
|
||||||
keyboard_layout_observers: SubscriberSet::new(),
|
keyboard_layout_observers: SubscriberSet::new(),
|
||||||
global_observers: SubscriberSet::new(),
|
global_observers: SubscriberSet::new(),
|
||||||
quit_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.
|
/// Register key bindings.
|
||||||
pub fn bind_keys(&mut self, bindings: impl IntoIterator<Item = KeyBinding>) {
|
pub fn bind_keys(&mut self, bindings: impl IntoIterator<Item = KeyBinding>) {
|
||||||
self.keymap.borrow_mut().add_bindings(bindings);
|
self.keymap.borrow_mut().add_bindings(bindings);
|
||||||
|
|
|
@ -1369,6 +1369,31 @@ impl Window {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dispatch_keystroke_interceptors(
|
||||||
|
&mut self,
|
||||||
|
event: &dyn Any,
|
||||||
|
context_stack: Vec<KeyContext>,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() 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
|
/// 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.
|
/// 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) {
|
pub fn defer(&self, cx: &mut App, f: impl FnOnce(&mut Window, &mut App) + 'static) {
|
||||||
|
@ -3522,6 +3547,13 @@ impl Window {
|
||||||
return;
|
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();
|
let mut currently_pending = self.pending_input.take().unwrap_or_default();
|
||||||
if currently_pending.focus.is_some() && currently_pending.focus != self.focus {
|
if currently_pending.focus.is_some() && currently_pending.focus != self.focus {
|
||||||
currently_pending = PendingInput::default();
|
currently_pending = PendingInput::default();
|
||||||
|
@ -3570,7 +3602,6 @@ impl Window {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.propagate_event = true;
|
|
||||||
for binding in match_result.bindings {
|
for binding in match_result.bindings {
|
||||||
self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx);
|
self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx);
|
||||||
if !cx.propagate_event {
|
if !cx.propagate_event {
|
||||||
|
|
|
@ -916,7 +916,7 @@ impl KeybindingEditorModal {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Self {
|
) -> 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 context_editor = cx.new(|cx| {
|
||||||
let mut editor = Editor::single_line(window, cx);
|
let mut editor = Editor::single_line(window, cx);
|
||||||
|
@ -1315,14 +1315,22 @@ async fn save_keybinding_update(
|
||||||
struct KeystrokeInput {
|
struct KeystrokeInput {
|
||||||
keystrokes: Vec<Keystroke>,
|
keystrokes: Vec<Keystroke>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
|
intercept_subscription: Option<Subscription>,
|
||||||
|
_focus_subscriptions: [Subscription; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeystrokeInput {
|
impl KeystrokeInput {
|
||||||
fn new(cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let focus_handle = cx.focus_handle();
|
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 {
|
Self {
|
||||||
keystrokes: Vec::new(),
|
keystrokes: Vec::new(),
|
||||||
focus_handle,
|
focus_handle,
|
||||||
|
intercept_subscription: None,
|
||||||
|
_focus_subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1351,21 +1359,13 @@ impl KeystrokeInput {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_key_down(
|
fn handle_keystroke(&mut self, keystroke: &Keystroke, cx: &mut Context<Self>) {
|
||||||
&mut self,
|
|
||||||
event: &gpui::KeyDownEvent,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if event.is_held {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Some(last) = self.keystrokes.last_mut()
|
if let Some(last) = self.keystrokes.last_mut()
|
||||||
&& last.key.is_empty()
|
&& last.key.is_empty()
|
||||||
{
|
{
|
||||||
*last = event.keystroke.clone();
|
*last = keystroke.clone();
|
||||||
} else {
|
} else if Some(keystroke) != self.keystrokes.last() {
|
||||||
self.keystrokes.push(event.keystroke.clone());
|
self.keystrokes.push(keystroke.clone());
|
||||||
}
|
}
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -1391,6 +1391,24 @@ impl KeystrokeInput {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
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>,
|
||||||
|
) {
|
||||||
|
self.intercept_subscription.take();
|
||||||
|
}
|
||||||
|
|
||||||
fn keystrokes(&self) -> &[Keystroke] {
|
fn keystrokes(&self) -> &[Keystroke] {
|
||||||
if self
|
if self
|
||||||
.keystrokes
|
.keystrokes
|
||||||
|
@ -1418,7 +1436,6 @@ impl Render for KeystrokeInput {
|
||||||
.id("keybinding_input")
|
.id("keybinding_input")
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
|
.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))
|
.on_key_up(cx.listener(Self::on_key_up))
|
||||||
.focus(|mut style| {
|
.focus(|mut style| {
|
||||||
style.border_color = Some(colors.border_focused);
|
style.border_color = Some(colors.border_focused);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue