From a8cae3bd77f6e1c1dde98bb1dcebba0d254dbd71 Mon Sep 17 00:00:00 2001 From: liam Date: Wed, 5 Aug 2026 12:31:15 -0400 Subject: [PATCH] gpui: Prevent multi-modifier gestures from dispatching standalone modifiers (#62036) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #61888. Multi-modifier gestures could leave GPUI’s standalone-modifier state armed after one modifier was released. For example, releasing `Alt` before `Shift` during an `Alt+Shift` gesture caused the final `Shift` release to be synthesized as a standalone shift keystroke. This diff invalidates standalone-modifier synthesis whenever multiple modifiers are active, while preserving genuine modifier-only key bindings. Release Notes: - Fixed Alt+Shift gestures potentially triggering standalone Shift key bindings. --- crates/gpui/src/interactive.rs | 30 +++++++++++++++++++++++++++++- crates/gpui/src/window.rs | 10 ++++++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index 872bf91c60..ac5210a11d 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -835,7 +835,7 @@ mod test { use crate::{ self as gpui, AppContext as _, Context, FocusHandle, InteractiveElement, IntoElement, - KeyBinding, Keystroke, ParentElement, Render, TestAppContext, Window, div, + KeyBinding, Keystroke, Modifiers, ParentElement, Render, TestAppContext, Window, div, }; struct TestView { @@ -902,4 +902,32 @@ mod test { }) .unwrap(); } + + #[gpui::test] + fn test_multi_modifier_gesture_does_not_dispatch_standalone_modifier_binding( + cx: &mut TestAppContext, + ) { + let (test_view, cx) = cx.add_window_view(|_, cx| TestView { + saw_key_down: false, + saw_action: false, + focus_handle: cx.focus_handle(), + }); + + cx.update(|_, cx| { + cx.bind_keys(vec![KeyBinding::new("shift", TestAction, None)]); + }); + test_view.update_in(cx, |test_view, window, cx| { + window.focus(&test_view.focus_handle, cx); + }); + + cx.simulate_modifiers_change(Modifiers::alt()); + cx.simulate_modifiers_change(Modifiers::alt() | Modifiers::shift()); + cx.simulate_modifiers_change(Modifiers::shift()); + cx.simulate_modifiers_change(Modifiers::none()); + assert!(!test_view.read_with(cx, |test_view, _| test_view.saw_action)); + + cx.simulate_modifiers_change(Modifiers::shift()); + cx.simulate_modifiers_change(Modifiers::none()); + assert!(test_view.read_with(cx, |test_view, _| test_view.saw_action)); + } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 1b5011e076..cceadc8e2c 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1149,7 +1149,7 @@ pub struct Window { #[derive(Clone, Debug, Default)] struct ModifierState { modifiers: Modifiers, - saw_keystroke: bool, + saw_other_input: bool, } /// Tracks input event timestamps to determine if input is arriving at a high rate. @@ -5167,7 +5167,7 @@ impl Window { if let Some(event) = event.downcast_ref::() { if event.modifiers.number_of_modifiers() == 0 && self.pending_modifier.modifiers.number_of_modifiers() == 1 - && !self.pending_modifier.saw_keystroke + && !self.pending_modifier.saw_other_input { let key = match self.pending_modifier.modifiers { modifiers if modifiers.shift => Some("shift"), @@ -5189,11 +5189,13 @@ impl Window { if self.pending_modifier.modifiers.number_of_modifiers() == 0 && event.modifiers.number_of_modifiers() == 1 { - self.pending_modifier.saw_keystroke = false + self.pending_modifier.saw_other_input = false + } else if event.modifiers.number_of_modifiers() > 1 { + self.pending_modifier.saw_other_input = true } self.pending_modifier.modifiers = event.modifiers } else if let Some(key_down_event) = event.downcast_ref::() { - self.pending_modifier.saw_keystroke = true; + self.pending_modifier.saw_other_input = true; keystroke = Some(key_down_event.keystroke.clone()); if key_down_event.keystroke.key_char.is_some() && matches!(