From 2ead8c42fb6792095d7cb02f7b89e467421dc8a0 Mon Sep 17 00:00:00 2001 From: John Tur Date: Tue, 4 Nov 2025 20:28:12 -0500 Subject: [PATCH] Improve Windows text input for international keyboard layouts and IMEs (#41259) - 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 --- crates/editor/src/editor.rs | 4 + crates/gpui/src/input.rs | 10 + crates/gpui/src/interactive.rs | 6 + crates/gpui/src/platform.rs | 10 + .../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(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 437c3db7e13fc266a8b374ee230d1a59cd9f6c65..ad837e3260998a0f0c5e7c3568436785c0ead031 100644 --- a/crates/editor/src/editor.rs +++ b/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) -> bool { + self.input_enabled + } } trait SelectionExt { diff --git a/crates/gpui/src/input.rs b/crates/gpui/src/input.rs index dc36ef9e16feedf31c01cd38327fd12729f894b3..c9c0a85cad2283c07af094e0f742c580341758ec 100644 --- a/crates/gpui/src/input.rs +++ b/crates/gpui/src/input.rs @@ -70,6 +70,11 @@ pub trait EntityInputHandler: 'static + Sized { window: &mut Window, cx: &mut Context, ) -> Option; + + /// See [`InputHandler::accepts_text_input`] for details + fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context) -> bool { + true + } } /// The canonical implementation of [`crate::PlatformInputHandler`]. Call [`Window::handle_input`] @@ -177,4 +182,9 @@ impl InputHandler for ElementInputHandler { 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)) + } } diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index dd521ff718322d663f761e05598edce83432bf2d..4aa0715144090d19be313e8c321a67a7e8682d8a 100644 --- a/crates/gpui/src/interactive.rs +++ b/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 {} diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 20a135df51cc935ce725f88e3978abb9f3fc07c9..decdc547353f9290b710b337c7dd99cdae188918 100644 --- a/crates/gpui/src/platform.rs +++ b/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 diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 6461bf69738cfae2f791bf8eea69fe9a2a038a43..ee2590aa4dfb9a34f61aa2d0d112a201093b10cd 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -1382,6 +1382,7 @@ impl Dispatch 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 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 for WaylandClientStatePtr { key_char: Some(commit_text), }, is_held: false, + prefer_character_input: false, })); } else { window.handle_ime(ImeInput::InsertText(commit_text)); diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index fa9d0181c095819823553da9e7f6be27598aea78..98e70a3071e4cc440da0240b6ecb30c884b52b4b 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/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) => { diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index 938db4b76205ee43eb979995c240b8d96e2aa57a..acc392a5f3429f20931455ea06733376ea0f587a 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/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), diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 95efffa3e77cdbeebf53acd47dd1aa9b33cb24ab..9c56d24e6857ca8fd8cc891f7c4f6657a06f86f0 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/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); } diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 9c10dcec4bb629bfbc78b76e74db099ed605d8be..ff88bea3fc6235522ce6f4e62e08be744e804ed2 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/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 { + fn handle_syskeyup_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option { 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 { - 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 { + fn handle_keydown_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option { 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 { + fn handle_keyup_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option { 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 { - 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 { - 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 { // 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( - handle: HWND, wparam: WPARAM, lparam: LPARAM, state: &mut WindowsWindowState, f: F, ) -> Option 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 { +) -> 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 { @@ -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, diff --git a/crates/gpui/src/platform/windows/keyboard.rs b/crates/gpui/src/platform/windows/keyboard.rs index cd0c1da10561d7bfafafbc70989344826e8e5b16..627988be5701f6d2725d3f6042e9f994ef9e6d5b 100644 --- a/crates/gpui/src/platform/windows/keyboard.rs +++ b/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]) diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index b7f69961c4f0d69d42de1f4f031d8a2a253ac538..a8e13469427eecbdc64afea534366e73d04e7e2d 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/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); + } } } diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index e765fa1a22d54a645d094f0df3250f75c94387af..ea1027f826ae91516d00993115673d2e3f61879b 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -45,7 +45,6 @@ pub struct WindowsWindowState { pub pending_surrogate: Option, pub last_reported_modifiers: Option, pub last_reported_capslock: Option, - 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, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 66ab327a19e2c326f8ba2cdc8710b3d772b45a59..e5b0ae4929cec3728047ee008db87278b99d790f 100644 --- a/crates/gpui/src/window.rs +++ b/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::() + .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;