Improve Windows text input for international keyboard layouts and IMEs (#41259)

John Tur created

- Custom handling of dead keys has been removed. UX for dead keys is now
the same as other applications on Windows.
- We could bring back some kind of custom UI, but only if UX is fully
compatible with expected Windows behavior (e.g. ability to move the
cursor after typing a dead key).
  - Fixes https://github.com/zed-industries/zed/issues/38838
- Character input via AltGr shift state now always has priority over
keybindings. This applies regardless of whether the keystroke used the
AltGr key or Ctrl+Alt to enter the shift state.
- In particular, we use the following heuristic to determine whether a
keystroke should trigger character input first or trigger keybindings
first:
- If the keystroke does not have any of Ctrl/Alt/Win down, trigger
keybindings first.
- Otherwise, determine the character that would be entered by the
keystroke. If it is a control character, or no character at all, trigger
keybindings first.
- Otherwise, the keystroke has _any_ of Ctrl/Alt/Win down and generates
a printable character. Compare this character against the character that
would be generated if the keystroke had _none_ of Ctrl/Alt/Win down:
- If the character is the same, the modifiers are not significant;
trigger keybindings first.
- If there is no active input handler, or the active input handler
indicates that it isn't accepting text input (e.g. when an operator is
pending in Vim mode), character entry is not useful; trigger keybindings
first.
- Otherwise, assume the modifiers enable access to an otherwise
difficult-to-enter key; trigger character entry first.
  - Fixes https://github.com/zed-industries/zed/issues/35862
- Fixes
https://github.com/zed-industries/zed/issues/40054#issuecomment-3447833349
  - Fixes https://github.com/zed-industries/zed/issues/41486
- TranslateMessage calls are no longer skipped for unhandled keystrokes.
This fixes language input keys on Japanese and Korean keyboards (and
surely other cases as well).
- To avoid any other missing-TranslateMessage headaches in the future,
the message loop has been rewritten in a "traditional" Win32 style,
where accelerators are handled in the message loop and TranslateMessage
is called in the intended manner.
  - Fixes https://github.com/zed-industries/zed/issues/39971
  - Fixes https://github.com/zed-industries/zed/issues/40300
  - Fixes https://github.com/zed-industries/zed/issues/40321
  - Fixes https://github.com/zed-industries/zed/issues/40335
  - Fixes https://github.com/zed-industries/zed/issues/40592
  - Fixes https://github.com/zed-industries/zed/issues/40638
- As a bonus, Alt+Space now opens the system menu, since it is triggered
by the WM_SYSCHAR generated by TranslateMessage.
- VK_PROCESSKEYs are now ignored rather than being unwrapped and matched
against keybindings. This ensures that IMEs will reliably receieve
keystrokes that they express interest in. This matches the behavior of
native Windows applications.
  - Fixes https://github.com/zed-industries/zed/issues/36736
  - Fixes https://github.com/zed-industries/zed/issues/39608
  - Fixes https://github.com/zed-industries/zed/issues/39991
  - Fixes https://github.com/zed-industries/zed/issues/41223
  - Fixes https://github.com/zed-industries/zed/issues/41656
  - Fixes https://github.com/zed-industries/zed/issues/34180
  - Fixes https://github.com/zed-industries/zed/issues/41766


Release Notes:

- windows: Improved keyboard input handling for international keyboard
layouts and IMEs

Change summary

crates/editor/src/editor.rs                      |   4 
crates/gpui/src/input.rs                         |  10 
crates/gpui/src/interactive.rs                   |   6 
crates/gpui/src/platform.rs                      |  10 
crates/gpui/src/platform/linux/wayland/client.rs |   3 
crates/gpui/src/platform/linux/x11/client.rs     |   1 
crates/gpui/src/platform/mac/events.rs           |   1 
crates/gpui/src/platform/mac/window.rs           |   1 
crates/gpui/src/platform/windows/events.rs       | 234 ++++++++---------
crates/gpui/src/platform/windows/keyboard.rs     |  35 --
crates/gpui/src/platform/windows/platform.rs     |  21 +
crates/gpui/src/platform/windows/window.rs       |   3 
crates/gpui/src/window.rs                        |  42 ++
13 files changed, 196 insertions(+), 175 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -24194,6 +24194,10 @@ impl EntityInputHandler for Editor {
         let utf16_offset = anchor.to_offset_utf16(&position_map.snapshot.buffer_snapshot());
         Some(utf16_offset.0)
     }
+
+    fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
+        self.input_enabled
+    }
 }
 
 trait SelectionExt {

crates/gpui/src/input.rs 🔗

@@ -70,6 +70,11 @@ pub trait EntityInputHandler: 'static + Sized {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<usize>;
+
+    /// See [`InputHandler::accepts_text_input`] for details
+    fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
+        true
+    }
 }
 
 /// The canonical implementation of [`crate::PlatformInputHandler`]. Call [`Window::handle_input`]
@@ -177,4 +182,9 @@ impl<V: EntityInputHandler> InputHandler for ElementInputHandler<V> {
             view.character_index_for_point(point, window, cx)
         })
     }
+
+    fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
+        self.view
+            .update(cx, |view, cx| view.accepts_text_input(window, cx))
+    }
 }

crates/gpui/src/interactive.rs 🔗

@@ -25,6 +25,12 @@ pub struct KeyDownEvent {
 
     /// Whether the key is currently held down.
     pub is_held: bool,
+
+    /// Whether the modifiers are excessive for producing this character.
+    /// When false, the modifiers are essential for character input (e.g., AltGr),
+    /// and character input should be prioritized over keybindings.
+    /// When true, the modifiers are for keybindings (e.g., Ctrl+A).
+    pub prefer_character_input: bool,
 }
 
 impl Sealed for KeyDownEvent {}

crates/gpui/src/platform.rs 🔗

@@ -1012,6 +1012,11 @@ impl PlatformInputHandler {
             .ok()
             .flatten()
     }
+
+    #[allow(dead_code)]
+    pub(crate) fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
+        self.handler.accepts_text_input(window, cx)
+    }
 }
 
 /// A struct representing a selection in a text buffer, in UTF16 characters.
@@ -1120,6 +1125,11 @@ pub trait InputHandler: 'static {
     fn apple_press_and_hold_enabled(&mut self) -> bool {
         true
     }
+
+    /// Returns whether this handler is accepting text input to be inserted.
+    fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
+        true
+    }
 }
 
 /// The variables that can be configured when creating a new window

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -1382,6 +1382,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
                         let input = PlatformInput::KeyDown(KeyDownEvent {
                             keystroke: keystroke.clone(),
                             is_held: false,
+                            prefer_character_input: false,
                         });
 
                         state.repeat.current_id += 1;
@@ -1395,6 +1396,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
                                 let input = PlatformInput::KeyDown(KeyDownEvent {
                                     keystroke,
                                     is_held: true,
+                                    prefer_character_input: false,
                                 });
                                 move |_event, _metadata, this| {
                                     let mut client = this.get_client();
@@ -1479,6 +1481,7 @@ impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
                                 key_char: Some(commit_text),
                             },
                             is_held: false,
+                            prefer_character_input: false,
                         }));
                     } else {
                         window.handle_ime(ImeInput::InsertText(commit_text));

crates/gpui/src/platform/linux/x11/client.rs 🔗

@@ -1047,6 +1047,7 @@ impl X11Client {
                 window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
                     keystroke,
                     is_held: false,
+                    prefer_character_input: false,
                 }));
             }
             Event::KeyRelease(event) => {

crates/gpui/src/platform/mac/events.rs 🔗

@@ -131,6 +131,7 @@ impl PlatformInput {
                 NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent {
                     keystroke: parse_keystroke(native_event),
                     is_held: native_event.isARepeat() == YES,
+                    prefer_character_input: false,
                 })),
                 NSEventType::NSKeyUp => Some(Self::KeyUp(KeyUpEvent {
                     keystroke: parse_keystroke(native_event),

crates/gpui/src/platform/mac/window.rs 🔗

@@ -2318,6 +2318,7 @@ extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
         let handled = (callback)(PlatformInput::KeyDown(KeyDownEvent {
             keystroke,
             is_held: false,
+            prefer_character_input: false,
         }));
         state.as_ref().lock().do_command_handled = Some(!handled.propagate);
     }

crates/gpui/src/platform/windows/events.rs 🔗

@@ -26,6 +26,7 @@ pub(crate) const WM_GPUI_DOCK_MENU_ACTION: u32 = WM_USER + 4;
 pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5;
 pub(crate) const WM_GPUI_KEYBOARD_LAYOUT_CHANGED: u32 = WM_USER + 6;
 pub(crate) const WM_GPUI_GPU_DEVICE_LOST: u32 = WM_USER + 7;
+pub(crate) const WM_GPUI_KEYDOWN: u32 = WM_USER + 8;
 
 const SIZE_MOVE_LOOP_TIMER_ID: usize = 1;
 const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1;
@@ -92,13 +93,10 @@ impl WindowsWindowInner {
             }
             WM_MOUSEWHEEL => self.handle_mouse_wheel_msg(handle, wparam, lparam),
             WM_MOUSEHWHEEL => self.handle_mouse_horizontal_wheel_msg(handle, wparam, lparam),
-            WM_SYSKEYDOWN => self.handle_syskeydown_msg(handle, wparam, lparam),
-            WM_SYSKEYUP => self.handle_syskeyup_msg(handle, wparam, lparam),
-            WM_SYSCOMMAND => self.handle_system_command(wparam),
-            WM_KEYDOWN => self.handle_keydown_msg(handle, wparam, lparam),
-            WM_KEYUP => self.handle_keyup_msg(handle, wparam, lparam),
+            WM_SYSKEYUP => self.handle_syskeyup_msg(wparam, lparam),
+            WM_KEYUP => self.handle_keyup_msg(wparam, lparam),
+            WM_GPUI_KEYDOWN => self.handle_keydown_msg(wparam, lparam),
             WM_CHAR => self.handle_char_msg(wparam),
-            WM_DEADCHAR => self.handle_dead_char_msg(wparam),
             WM_IME_STARTCOMPOSITION => self.handle_ime_position(handle),
             WM_IME_COMPOSITION => self.handle_ime_composition(handle, lparam),
             WM_SETCURSOR => self.handle_set_cursor(handle, lparam),
@@ -327,35 +325,9 @@ impl WindowsWindowInner {
         Some(0)
     }
 
-    fn handle_syskeydown_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
+    fn handle_syskeyup_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
         let mut lock = self.state.borrow_mut();
-        let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
-            PlatformInput::KeyDown(KeyDownEvent {
-                keystroke,
-                is_held: lparam.0 & (0x1 << 30) > 0,
-            })
-        })?;
-        let mut func = lock.callbacks.input.take()?;
-        drop(lock);
-
-        let handled = !func(input).propagate;
-
-        let mut lock = self.state.borrow_mut();
-        lock.callbacks.input = Some(func);
-
-        if handled {
-            lock.system_key_handled = true;
-            Some(0)
-        } else {
-            // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
-            // shortcuts.
-            None
-        }
-    }
-
-    fn handle_syskeyup_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
-        let mut lock = self.state.borrow_mut();
-        let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
+        let input = handle_key_event(wparam, lparam, &mut lock, |keystroke, _| {
             PlatformInput::KeyUp(KeyUpEvent { keystroke })
         })?;
         let mut func = lock.callbacks.input.take()?;
@@ -369,27 +341,24 @@ impl WindowsWindowInner {
 
     // It's a known bug that you can't trigger `ctrl-shift-0`. See:
     // https://superuser.com/questions/1455762/ctrl-shift-number-key-combination-has-stopped-working-for-a-few-numbers
-    fn handle_keydown_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
+    fn handle_keydown_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
         let mut lock = self.state.borrow_mut();
-        let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
-            PlatformInput::KeyDown(KeyDownEvent {
-                keystroke,
-                is_held: lparam.0 & (0x1 << 30) > 0,
-            })
-        }) else {
+        let Some(input) = handle_key_event(
+            wparam,
+            lparam,
+            &mut lock,
+            |keystroke, prefer_character_input| {
+                PlatformInput::KeyDown(KeyDownEvent {
+                    keystroke,
+                    is_held: lparam.0 & (0x1 << 30) > 0,
+                    prefer_character_input,
+                })
+            },
+        ) else {
             return Some(1);
         };
         drop(lock);
 
-        let is_composing = self
-            .with_input_handler(|input_handler| input_handler.marked_text_range())
-            .flatten()
-            .is_some();
-        if is_composing {
-            translate_message(handle, wparam, lparam);
-            return Some(0);
-        }
-
         let Some(mut func) = self.state.borrow_mut().callbacks.input.take() else {
             return Some(1);
         };
@@ -398,17 +367,12 @@ impl WindowsWindowInner {
 
         self.state.borrow_mut().callbacks.input = Some(func);
 
-        if handled {
-            Some(0)
-        } else {
-            translate_message(handle, wparam, lparam);
-            Some(1)
-        }
+        if handled { Some(0) } else { Some(1) }
     }
 
-    fn handle_keyup_msg(&self, handle: HWND, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
+    fn handle_keyup_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
         let mut lock = self.state.borrow_mut();
-        let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
+        let Some(input) = handle_key_event(wparam, lparam, &mut lock, |keystroke, _| {
             PlatformInput::KeyUp(KeyUpEvent { keystroke })
         }) else {
             return Some(1);
@@ -434,14 +398,6 @@ impl WindowsWindowInner {
         Some(0)
     }
 
-    fn handle_dead_char_msg(&self, wparam: WPARAM) -> Option<isize> {
-        let ch = char::from_u32(wparam.0 as u32)?.to_string();
-        self.with_input_handler(|input_handler| {
-            input_handler.replace_and_mark_text_in_range(None, &ch, None);
-        });
-        None
-    }
-
     fn handle_mouse_down_msg(
         &self,
         handle: HWND,
@@ -1127,17 +1083,6 @@ impl WindowsWindowInner {
         Some(0)
     }
 
-    fn handle_system_command(&self, wparam: WPARAM) -> Option<isize> {
-        if wparam.0 == SC_KEYMENU as usize {
-            let mut lock = self.state.borrow_mut();
-            if lock.system_key_handled {
-                lock.system_key_handled = false;
-                return Some(0);
-            }
-        }
-        None
-    }
-
     fn handle_system_theme_changed(&self, handle: HWND, lparam: LPARAM) -> Option<isize> {
         // lParam is a pointer to a string that indicates the area containing the system parameter
         // that was changed.
@@ -1281,30 +1226,14 @@ impl WindowsWindowInner {
     }
 }
 
-#[inline]
-fn translate_message(handle: HWND, wparam: WPARAM, lparam: LPARAM) {
-    let msg = MSG {
-        hwnd: handle,
-        message: WM_KEYDOWN,
-        wParam: wparam,
-        lParam: lparam,
-        // It seems like leaving the following two parameters empty doesn't break key events, they still work as expected.
-        // But if any bugs pop up after this PR, this is probably the place to look first.
-        time: 0,
-        pt: POINT::default(),
-    };
-    unsafe { TranslateMessage(&msg).ok().log_err() };
-}
-
 fn handle_key_event<F>(
-    handle: HWND,
     wparam: WPARAM,
     lparam: LPARAM,
     state: &mut WindowsWindowState,
     f: F,
 ) -> Option<PlatformInput>
 where
-    F: FnOnce(Keystroke) -> PlatformInput,
+    F: FnOnce(Keystroke, bool) -> PlatformInput,
 {
     let virtual_key = VIRTUAL_KEY(wparam.loword());
     let modifiers = current_modifiers();
@@ -1323,10 +1252,7 @@ where
                 capslock: current_capslock(),
             }))
         }
-        VK_PACKET => {
-            translate_message(handle, wparam, lparam);
-            None
-        }
+        VK_PACKET => None,
         VK_CAPITAL => {
             let capslock = current_capslock();
             if state
@@ -1342,13 +1268,8 @@ where
             }))
         }
         vkey => {
-            let vkey = if vkey == VK_PROCESSKEY {
-                VIRTUAL_KEY(unsafe { ImmGetVirtualKey(handle) } as u16)
-            } else {
-                vkey
-            };
             let keystroke = parse_normal_key(vkey, lparam, modifiers)?;
-            Some(f(keystroke))
+            Some(f(keystroke.0, keystroke.1))
         }
     }
 }
@@ -1408,7 +1329,7 @@ fn parse_normal_key(
     vkey: VIRTUAL_KEY,
     lparam: LPARAM,
     mut modifiers: Modifiers,
-) -> Option<Keystroke> {
+) -> Option<(Keystroke, bool)> {
     let mut key_char = None;
     let key = parse_immutable(vkey).or_else(|| {
         let scan_code = lparam.hiword() & 0xFF;
@@ -1421,11 +1342,86 @@ fn parse_normal_key(
         );
         get_keystroke_key(vkey, scan_code as u32, &mut modifiers)
     })?;
-    Some(Keystroke {
-        modifiers,
-        key,
-        key_char,
-    })
+
+    let prefer_character_input = should_prefer_character_input(vkey, lparam.hiword() & 0xFF);
+
+    Some((
+        Keystroke {
+            modifiers,
+            key,
+            key_char,
+        },
+        prefer_character_input,
+    ))
+}
+
+fn should_prefer_character_input(vkey: VIRTUAL_KEY, scan_code: u16) -> bool {
+    let mut keyboard_state = [0u8; 256];
+    unsafe {
+        if GetKeyboardState(&mut keyboard_state).is_err() {
+            return false;
+        }
+    }
+
+    let mut buffer_c = [0u16; 8];
+    let result_c = unsafe {
+        ToUnicode(
+            vkey.0 as u32,
+            scan_code as u32,
+            Some(&keyboard_state),
+            &mut buffer_c,
+            0x4,
+        )
+    };
+    if result_c < 0 {
+        return false;
+    }
+
+    let c = &buffer_c[..result_c as usize];
+    if char::decode_utf16(c.iter().copied())
+        .next()
+        .and_then(|ch| ch.ok())
+        .map(|ch| ch.is_control())
+        .unwrap_or(true)
+    {
+        return false;
+    }
+
+    let ctrl_down = (keyboard_state[VK_CONTROL.0 as usize] & 0x80) != 0;
+    let alt_down = (keyboard_state[VK_MENU.0 as usize] & 0x80) != 0;
+    let win_down = (keyboard_state[VK_LWIN.0 as usize] & 0x80) != 0
+        || (keyboard_state[VK_RWIN.0 as usize] & 0x80) != 0;
+    let has_modifiers = ctrl_down || alt_down || win_down;
+    if !has_modifiers {
+        return false;
+    }
+
+    let mut state_no_modifiers = keyboard_state;
+    state_no_modifiers[VK_CONTROL.0 as usize] = 0;
+    state_no_modifiers[VK_LCONTROL.0 as usize] = 0;
+    state_no_modifiers[VK_RCONTROL.0 as usize] = 0;
+    state_no_modifiers[VK_MENU.0 as usize] = 0;
+    state_no_modifiers[VK_LMENU.0 as usize] = 0;
+    state_no_modifiers[VK_RMENU.0 as usize] = 0;
+    state_no_modifiers[VK_LWIN.0 as usize] = 0;
+    state_no_modifiers[VK_RWIN.0 as usize] = 0;
+
+    let mut buffer_c_no_modifiers = [0u16; 8];
+    let result_c_no_modifiers = unsafe {
+        ToUnicode(
+            vkey.0 as u32,
+            scan_code as u32,
+            Some(&state_no_modifiers),
+            &mut buffer_c_no_modifiers,
+            0x4,
+        )
+    };
+    if result_c_no_modifiers <= 0 {
+        return false;
+    }
+
+    let c_no_modifiers = &buffer_c_no_modifiers[..result_c_no_modifiers as usize];
+    c != c_no_modifiers
 }
 
 fn parse_ime_composition_string(ctx: HIMC, comp_type: IME_COMPOSITION_STRING) -> Option<String> {
@@ -1460,25 +1456,11 @@ fn is_virtual_key_pressed(vkey: VIRTUAL_KEY) -> bool {
     unsafe { GetKeyState(vkey.0 as i32) < 0 }
 }
 
-fn keyboard_uses_altgr() -> bool {
-    use crate::platform::windows::keyboard::WindowsKeyboardLayout;
-    WindowsKeyboardLayout::new()
-        .map(|layout| layout.uses_altgr())
-        .unwrap_or(false)
-}
-
 #[inline]
 pub(crate) fn current_modifiers() -> Modifiers {
-    let lmenu_pressed = is_virtual_key_pressed(VK_LMENU);
-    let rmenu_pressed = is_virtual_key_pressed(VK_RMENU);
-    let lcontrol_pressed = is_virtual_key_pressed(VK_LCONTROL);
-
-    // Only treat right Alt + left Ctrl as AltGr on keyboards that actually use it
-    let altgr = keyboard_uses_altgr() && rmenu_pressed && lcontrol_pressed;
-
     Modifiers {
-        control: is_virtual_key_pressed(VK_CONTROL) && !altgr,
-        alt: (lmenu_pressed || rmenu_pressed) && !altgr,
+        control: is_virtual_key_pressed(VK_CONTROL),
+        alt: is_virtual_key_pressed(VK_MENU),
         shift: is_virtual_key_pressed(VK_SHIFT),
         platform: is_virtual_key_pressed(VK_LWIN) || is_virtual_key_pressed(VK_RWIN),
         function: false,

crates/gpui/src/platform/windows/keyboard.rs 🔗

@@ -108,39 +108,6 @@ impl WindowsKeyboardLayout {
             name: "unknown".to_string(),
         }
     }
-
-    pub(crate) fn uses_altgr(&self) -> bool {
-        // Check if this is a known AltGr layout by examining the layout ID
-        // The layout ID is a hex string like "00000409" (US) or "00000407" (German)
-        // Extract the language ID (last 4 bytes)
-        let id_bytes = self.id.as_bytes();
-        if id_bytes.len() >= 4 {
-            let lang_id = &id_bytes[id_bytes.len() - 4..];
-            // List of keyboard layouts that use AltGr (non-exhaustive)
-            matches!(
-                lang_id,
-                b"0407" | // German
-                b"040C" | // French
-                b"040A" | // Spanish
-                b"0415" | // Polish
-                b"0413" | // Dutch
-                b"0816" | // Portuguese
-                b"041D" | // Swedish
-                b"0414" | // Norwegian
-                b"040B" | // Finnish
-                b"041F" | // Turkish
-                b"0419" | // Russian
-                b"0405" | // Czech
-                b"040E" | // Hungarian
-                b"0424" | // Slovenian
-                b"041A" | // Croatian
-                b"041B" | // Slovak
-                b"0418" // Romanian
-            )
-        } else {
-            false
-        }
-    }
 }
 
 impl WindowsKeyboardMapper {
@@ -258,7 +225,7 @@ pub(crate) fn generate_key_char(
     }
 
     let mut buffer = [0; 8];
-    let len = unsafe { ToUnicode(vkey.0 as u32, scan_code, Some(&state), &mut buffer, 1 << 2) };
+    let len = unsafe { ToUnicode(vkey.0 as u32, scan_code, Some(&state), &mut buffer, 0x5) };
 
     match len {
         len if len > 0 => String::from_utf16(&buffer[..len as usize])

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -272,6 +272,22 @@ impl WindowsPlatform {
     }
 }
 
+fn translate_accelerator(msg: &MSG) -> Option<()> {
+    if msg.message != WM_KEYDOWN && msg.message != WM_SYSKEYDOWN {
+        return None;
+    }
+
+    let result = unsafe {
+        SendMessageW(
+            msg.hwnd,
+            WM_GPUI_KEYDOWN,
+            Some(msg.wParam),
+            Some(msg.lParam),
+        )
+    };
+    (result.0 == 0).then_some(())
+}
+
 impl Platform for WindowsPlatform {
     fn background_executor(&self) -> BackgroundExecutor {
         self.background_executor.clone()
@@ -312,7 +328,10 @@ impl Platform for WindowsPlatform {
         let mut msg = MSG::default();
         unsafe {
             while GetMessageW(&mut msg, None, 0, 0).as_bool() {
-                DispatchMessageW(&msg);
+                if translate_accelerator(&msg).is_none() {
+                    _ = TranslateMessage(&msg);
+                    DispatchMessageW(&msg);
+                }
             }
         }
 

crates/gpui/src/platform/windows/window.rs 🔗

@@ -45,7 +45,6 @@ pub struct WindowsWindowState {
     pub pending_surrogate: Option<u16>,
     pub last_reported_modifiers: Option<Modifiers>,
     pub last_reported_capslock: Option<Capslock>,
-    pub system_key_handled: bool,
     pub hovered: bool,
 
     pub renderer: DirectXRenderer,
@@ -112,7 +111,6 @@ impl WindowsWindowState {
         let pending_surrogate = None;
         let last_reported_modifiers = None;
         let last_reported_capslock = None;
-        let system_key_handled = false;
         let hovered = false;
         let click_state = ClickState::new();
         let nc_button_pressed = None;
@@ -133,7 +131,6 @@ impl WindowsWindowState {
             pending_surrogate,
             last_reported_modifiers,
             last_reported_capslock,
-            system_key_handled,
             hovered,
             renderer,
             click_state,

crates/gpui/src/window.rs 🔗

@@ -3558,6 +3558,7 @@ impl Window {
             PlatformInput::KeyDown(KeyDownEvent {
                 keystroke: keystroke.clone(),
                 is_held: false,
+                prefer_character_input: false,
             }),
             cx,
         );
@@ -3856,17 +3857,35 @@ impl Window {
             return;
         }
 
-        for binding in match_result.bindings {
-            self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx);
-            if !cx.propagate_event {
-                self.dispatch_keystroke_observers(
-                    event,
-                    Some(binding.action),
-                    match_result.context_stack,
-                    cx,
-                );
-                self.pending_input_changed(cx);
-                return;
+        let skip_bindings = event
+            .downcast_ref::<KeyDownEvent>()
+            .filter(|key_down_event| key_down_event.prefer_character_input)
+            .map(|_| {
+                self.platform_window
+                    .take_input_handler()
+                    .map_or(false, |mut input_handler| {
+                        let accepts = input_handler.accepts_text_input(self, cx);
+                        self.platform_window.set_input_handler(input_handler);
+                        // If modifiers are not excessive (e.g. AltGr), and the input handler is accepting text input,
+                        // we prefer the text input over bindings.
+                        accepts
+                    })
+            })
+            .unwrap_or(false);
+
+        if !skip_bindings {
+            for binding in match_result.bindings {
+                self.dispatch_action_on_node(node_id, binding.action.as_ref(), cx);
+                if !cx.propagate_event {
+                    self.dispatch_keystroke_observers(
+                        event,
+                        Some(binding.action),
+                        match_result.context_stack,
+                        cx,
+                    );
+                    self.pending_input_changed(cx);
+                    return;
+                }
             }
         }
 
@@ -3975,6 +3994,7 @@ impl Window {
             let event = KeyDownEvent {
                 keystroke: replay.keystroke.clone(),
                 is_held: false,
+                prefer_character_input: true,
             };
 
             cx.propagate_event = true;