gpui: Fix stale pending input when a window is blurred (#63271)

This prepares for the keybinding hints PR.

To reproduce, press `ctrl-b` in a GPUI app with `ctrl-b h` and `ctrl-b
j` bindings, then call `window.blur()`. The live pending state clears,
but observers still report `ctrl-b`.

`Window::blur` now clears pending input and notifies observers after the
current effect cycle. I don't think we currently exercise this path in
Zed, and I was only able to reproduce it with an example GPUI app.

Release Notes:

- N/A
This commit is contained in:
Smit Barmase authored and GitHub committed 2026-08-26 21:36:59 +00:00
1 parent 3ba893e4ac
commit b16d08d473
12 files changed
+76 -42

No files matched your search

+1 -1
View File
@@ -7280,7 +7280,7 @@ mod tests {
user_message_editor.update_in(cx, |_editor, window, cx| {
window.dispatch_action(Box::new(zed_actions::agent::Chat), cx);
});
cx.update(|window, _cx| window.blur());
cx.update(|window, cx| window.blur(cx));
cx.run_until_parked();
workspace.update_in(cx, |workspace, window, cx| {
+1 -1
View File
@@ -11833,7 +11833,7 @@ mod tests {
);
// Blur the editor so that it displays placeholder text.
window.blur();
window.blur(cx);
})
.unwrap();
+3 -6
View File
@@ -1764,9 +1764,9 @@ impl App {
if focus.ref_count.load(SeqCst) == 0 {
for window_handle in self.windows() {
window_handle
.update(self, |_, window, _| {
.update(self, |_, window, cx| {
if window.focus == Some(handle_id) {
window.blur();
window.blur(cx);
}
})
.unwrap();
@@ -2390,10 +2390,7 @@ impl App {
for window in self.windows() {
window
.update(self, |_, window, cx| {
if window.pending_input_keystrokes().is_some() {
window.clear_pending_keystrokes();
window.pending_input_changed(cx);
}
window.clear_pending_keystrokes(cx);
})
.ok();
}
+1 -1
View File
@@ -338,7 +338,7 @@ mod tests {
assert_eq!(test_window.text_input_configurations().len(), 3);
// Losing focus reverts the platform to the default configuration.
cx.update_window(window, |_, window, _| window.blur())
cx.update_window(window, |_, window, cx| window.blur(cx))
.unwrap();
draw(cx);
assert_eq!(
+30 -4
View File
@@ -830,7 +830,7 @@ mod tests {
}
#[crate::test]
fn test_pending_input_observers_notified_on_focus_change(cx: &mut TestAppContext) {
fn test_pending_input_observers_notified_on_focus_change_and_blur(cx: &mut TestAppContext) {
#[derive(Clone)]
struct CustomElement {
focus_handle: FocusHandle,
@@ -988,6 +988,8 @@ mod tests {
cx.update(|cx| {
cx.bind_keys([KeyBinding::new("ctrl-b", TestAction, Some("Terminal"))]);
cx.bind_keys([KeyBinding::new("ctrl-b h", TestAction, Some("Terminal"))]);
cx.bind_keys([KeyBinding::new("ctrl-d", TestAction, None)]);
cx.bind_keys([KeyBinding::new("ctrl-d h", TestAction, None)]);
});
let (test, cx) = cx.add_window_view(|_, cx| CustomElement::new(cx));
@@ -1034,6 +1036,30 @@ mod tests {
let count_after_focus_change = *pending_input_changed_count.borrow();
assert!(count_after_focus_change > *count_after_pending_for_assertion.borrow());
});
cx.update(|window, cx| window.focus(&focus_handle, cx));
cx.simulate_keystrokes("ctrl-b");
let count_before_blur = *pending_input_changed_count.borrow();
cx.update(|window, cx| {
assert!(window.has_pending_keystrokes());
window.blur(cx);
assert!(!window.has_pending_keystrokes());
assert!(window.pending_input_is_none());
});
cx.update(|_, _| {
assert!(*pending_input_changed_count.borrow() > count_before_blur);
});
cx.update(|window, cx| window.disable_focus(cx));
cx.simulate_keystrokes("ctrl-d");
cx.update(|window, cx| {
assert!(window.has_pending_keystrokes());
window.blur(cx);
assert!(window.pending_input_is_none());
});
}
#[crate::test]
@@ -1226,10 +1252,10 @@ mod tests {
let prefers_ime_after_blur = {
let mut platform_window = cx.test_window(cx.window_handle());
let mut input_handler = platform_window.take_input_handler();
cx.update(|window, _| {
window.blur();
cx.update(|window, cx| {
window.blur(cx);
assert!(!window.has_pending_keystrokes());
assert!(window.pending_input_keystrokes().is_none());
assert!(window.pending_input_is_none());
});
let prefers_ime = input_handler
.as_mut()
+29 -18
View File
@@ -2059,24 +2059,15 @@ impl Window {
self.focus = Some(handle.id);
self.focus_generation = self.focus_generation.wrapping_add(1);
self.clear_pending_keystrokes();
// Avoid re-entrant entity updates by deferring observer notifications to the end of the
// current effect cycle, and only for this window.
let window_handle = self.handle;
cx.defer(move |cx| {
window_handle
.update(cx, |_, window, cx| {
window.pending_input_changed(cx);
})
.ok();
});
self.clear_pending_keystrokes(cx);
self.refresh();
}
/// Remove focus from all elements within this context's window.
pub fn blur(&mut self) {
pub fn blur(&mut self, cx: &mut App) {
self.clear_pending_keystrokes(cx);
if !self.focus_enabled {
return;
}
@@ -2089,8 +2080,8 @@ impl Window {
}
/// Blur the window and don't allow anything in it to be focused again.
pub fn disable_focus(&mut self) {
self.blur();
pub fn disable_focus(&mut self, cx: &mut App) {
self.blur(cx);
self.focus_enabled = false;
}
@@ -5458,6 +5449,19 @@ impl Window {
.retain(&(), |callback| callback(self, cx));
}
fn defer_pending_input_changed(&self, cx: &mut App) {
// Avoid re-entrant entity updates by deferring observer notifications to the end of the
// current effect cycle, and only for this window.
let window_handle = self.handle;
cx.defer(move |cx| {
window_handle
.update(cx, |_, window, cx| {
window.pending_input_changed(cx);
})
.ok();
});
}
fn dispatch_key_down_up_event(
&mut self,
event: &dyn Any,
@@ -5522,8 +5526,15 @@ impl Window {
self.active_pending_input().is_some()
}
pub(crate) fn clear_pending_keystrokes(&mut self) {
self.pending_input.take();
#[cfg(test)]
pub(crate) fn pending_input_is_none(&self) -> bool {
self.pending_input.is_none()
}
pub(crate) fn clear_pending_keystrokes(&mut self, cx: &mut App) {
if self.pending_input.take().is_some() {
self.defer_pending_input_changed(cx);
}
}
/// Returns the currently pending input keystrokes that might result in a multi-stroke key binding.
@@ -6181,7 +6192,7 @@ impl Window {
}
}
accesskit::Action::Blur => {
self.blur();
self.blur(cx);
}
_ => {
log::debug!(
+2 -2
View File
@@ -984,8 +984,8 @@ impl Render for ImageViewToolbarControls {
})
.child(editor.clone())
.on_action::<menu::Confirm>({
move |_: &menu::Confirm, window, _| {
window.blur();
move |_: &menu::Confirm, window, cx| {
window.blur(cx);
}
})
.on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
@@ -1103,7 +1103,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
);
// Dismiss the rename editor when it loses focus.
workspace.update_in(cx, |_, window, _| window.blur());
workspace.update_in(cx, |_, window, cx| window.blur(cx));
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
@@ -6869,7 +6869,7 @@ async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppC
" > test"
]
);
workspace.update_in(cx, |_, window, _| window.blur());
workspace.update_in(cx, |_, window, cx| window.blur(cx));
cx.executor().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
@@ -800,8 +800,8 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
})
.child(editor)
.on_action::<menu::Confirm>({
move |_, window, _| {
window.blur();
move |_, window, cx| {
window.blur(cx);
}
})
.into_any_element()
+2 -2
View File
@@ -1455,8 +1455,8 @@ async fn test_keyboard_focus_in_does_not_set_selection(cx: &mut TestAppContext)
sidebar.selection = Some(0);
});
cx.update(|window, _cx| {
window.blur();
cx.update(|window, cx| {
window.blur(cx);
});
cx.run_until_parked();
+1 -1
View File
@@ -79,7 +79,7 @@ async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
// Selections aren't changed if editor is blurred but vim-mode is still disabled.
cx.cx.set_state("«hjklˇ»");
cx.assert_editor_state("«hjklˇ»");
cx.update_editor(|_, window, _cx| window.blur());
cx.update_editor(|_, window, cx| window.blur(cx));
cx.assert_editor_state("«hjklˇ»");
cx.update_editor(|_, window, cx| cx.focus_self(window));
cx.assert_editor_state("«hjklˇ»");
+2 -2
View File
@@ -13190,7 +13190,7 @@ mod tests {
item.entry_id = None;
});
item.is_dirty = true;
window.blur();
window.blur(cx);
});
cx.run_until_parked();
item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
@@ -16001,7 +16001,7 @@ mod tests {
assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
});
cx.update(|window, _| window.blur());
cx.update(|window, cx| window.blur(cx));
cx.executor().run_until_parked();
workspace.update(cx, |_, cx| cx.notify());
cx.executor().run_until_parked();