gpui: Prevent multi-modifier gestures from dispatching standalone modifiers (#62036)

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.
This commit is contained in:
liam authored and GitHub committed 2026-08-05 16:31:15 +00:00
1 parent 38df25d54c
commit a8cae3bd77
2 files changed
+35 -5

No files matched your search

+29 -1
View File
@@ -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));
}
}
+6 -4
View File
@@ -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::<ModifiersChangedEvent>() {
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::<KeyDownEvent>() {
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!(