windows: Only call `TranslateMessage` when we can't handle the event (#32166)

张小白 created

This PR improves key handling on Windows by moving the
`TranslateMessage` call from the message loop to after we handled
`WM_KEYDOWN`. This brings Windows behavior more in line with macOS and
gives us finer control over key events. As a result, Vim keybindings now
work properly even when an IME is active. The trade-off is that it might
introduce a slight delay in text input.


Release Notes:

- N/A

Change summary

crates/gpui/src/platform/windows/events.rs   | 76 +++++++++++++--------
crates/gpui/src/platform/windows/platform.rs |  3 
crates/gpui/src/platform/windows/window.rs   |  3 
3 files changed, 48 insertions(+), 34 deletions(-)

Detailed changes

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

@@ -83,11 +83,11 @@ pub(crate) fn handle_msg(
         WM_XBUTTONUP => handle_xbutton_msg(handle, wparam, lparam, handle_mouse_up_msg, state_ptr),
         WM_MOUSEWHEEL => handle_mouse_wheel_msg(handle, wparam, lparam, state_ptr),
         WM_MOUSEHWHEEL => handle_mouse_horizontal_wheel_msg(handle, wparam, lparam, state_ptr),
-        WM_SYSKEYDOWN => handle_syskeydown_msg(wparam, lparam, state_ptr),
-        WM_SYSKEYUP => handle_syskeyup_msg(wparam, lparam, state_ptr),
+        WM_SYSKEYDOWN => handle_syskeydown_msg(handle, wparam, lparam, state_ptr),
+        WM_SYSKEYUP => handle_syskeyup_msg(handle, wparam, lparam, state_ptr),
         WM_SYSCOMMAND => handle_system_command(wparam, state_ptr),
-        WM_KEYDOWN => handle_keydown_msg(wparam, lparam, state_ptr),
-        WM_KEYUP => handle_keyup_msg(wparam, lparam, state_ptr),
+        WM_KEYDOWN => handle_keydown_msg(handle, wparam, lparam, state_ptr),
+        WM_KEYUP => handle_keyup_msg(handle, wparam, lparam, state_ptr),
         WM_CHAR => handle_char_msg(wparam, state_ptr),
         WM_DEADCHAR => handle_dead_char_msg(wparam, state_ptr),
         WM_IME_STARTCOMPOSITION => handle_ime_position(handle, state_ptr),
@@ -337,12 +337,13 @@ fn handle_mouse_leave_msg(state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize>
 }
 
 fn handle_syskeydown_msg(
+    handle: HWND,
     wparam: WPARAM,
     lparam: LPARAM,
     state_ptr: Rc<WindowsWindowStatePtr>,
 ) -> Option<isize> {
     let mut lock = state_ptr.state.borrow_mut();
-    let input = handle_key_event(wparam, lparam, &mut lock, |keystroke| {
+    let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
         PlatformInput::KeyDown(KeyDownEvent {
             keystroke,
             is_held: lparam.0 & (0x1 << 30) > 0,
@@ -358,7 +359,6 @@ fn handle_syskeydown_msg(
 
     if handled {
         lock.system_key_handled = true;
-        lock.suppress_next_char_msg = true;
         Some(0)
     } else {
         // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
@@ -368,12 +368,13 @@ fn handle_syskeydown_msg(
 }
 
 fn handle_syskeyup_msg(
+    handle: HWND,
     wparam: WPARAM,
     lparam: LPARAM,
     state_ptr: Rc<WindowsWindowStatePtr>,
 ) -> Option<isize> {
     let mut lock = state_ptr.state.borrow_mut();
-    let input = handle_key_event(wparam, lparam, &mut lock, |keystroke| {
+    let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
         PlatformInput::KeyUp(KeyUpEvent { keystroke })
     })?;
     let mut func = lock.callbacks.input.take()?;
@@ -388,12 +389,13 @@ fn handle_syskeyup_msg(
 // 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(
+    handle: HWND,
     wparam: WPARAM,
     lparam: LPARAM,
     state_ptr: Rc<WindowsWindowStatePtr>,
 ) -> Option<isize> {
     let mut lock = state_ptr.state.borrow_mut();
-    let Some(input) = handle_key_event(wparam, lparam, &mut lock, |keystroke| {
+    let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
         PlatformInput::KeyDown(KeyDownEvent {
             keystroke,
             is_held: lparam.0 & (0x1 << 30) > 0,
@@ -401,32 +403,42 @@ fn handle_keydown_msg(
     }) else {
         return Some(1);
     };
+    drop(lock);
 
-    let Some(mut func) = lock.callbacks.input.take() else {
+    let is_composing = with_input_handler(&state_ptr, |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) = state_ptr.state.borrow_mut().callbacks.input.take() else {
         return Some(1);
     };
-    drop(lock);
 
     let handled = !func(input).propagate;
 
-    let mut lock = state_ptr.state.borrow_mut();
-    lock.callbacks.input = Some(func);
+    state_ptr.state.borrow_mut().callbacks.input = Some(func);
 
     if handled {
-        lock.suppress_next_char_msg = true;
         Some(0)
     } else {
+        translate_message(handle, wparam, lparam);
         Some(1)
     }
 }
 
 fn handle_keyup_msg(
+    handle: HWND,
     wparam: WPARAM,
     lparam: LPARAM,
     state_ptr: Rc<WindowsWindowStatePtr>,
 ) -> Option<isize> {
     let mut lock = state_ptr.state.borrow_mut();
-    let Some(input) = handle_key_event(wparam, lparam, &mut lock, |keystroke| {
+    let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
         PlatformInput::KeyUp(KeyUpEvent { keystroke })
     }) else {
         return Some(1);
@@ -1213,7 +1225,23 @@ fn handle_input_language_changed(
     Some(0)
 }
 
+#[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,
@@ -1222,15 +1250,10 @@ fn handle_key_event<F>(
 where
     F: FnOnce(Keystroke) -> PlatformInput,
 {
-    state.suppress_next_char_msg = false;
     let virtual_key = VIRTUAL_KEY(wparam.loword());
     let mut modifiers = current_modifiers();
 
     match virtual_key {
-        VK_PROCESSKEY => {
-            // IME composition
-            None
-        }
         VK_SHIFT | VK_CONTROL | VK_MENU | VK_LWIN | VK_RWIN => {
             if state
                 .last_reported_modifiers
@@ -1244,6 +1267,11 @@ 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))
         }
@@ -1491,12 +1519,7 @@ fn with_input_handler<F, R>(state_ptr: &Rc<WindowsWindowStatePtr>, f: F) -> Opti
 where
     F: FnOnce(&mut PlatformInputHandler) -> R,
 {
-    let mut lock = state_ptr.state.borrow_mut();
-    if lock.suppress_next_char_msg {
-        return None;
-    }
-    let mut input_handler = lock.input_handler.take()?;
-    drop(lock);
+    let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?;
     let result = f(&mut input_handler);
     state_ptr.state.borrow_mut().input_handler = Some(input_handler);
     Some(result)
@@ -1510,9 +1533,6 @@ where
     F: FnOnce(&mut PlatformInputHandler, f32) -> Option<R>,
 {
     let mut lock = state_ptr.state.borrow_mut();
-    if lock.suppress_next_char_msg {
-        return None;
-    }
     let mut input_handler = lock.input_handler.take()?;
     let scale_factor = lock.scale_factor;
     drop(lock);

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

@@ -43,7 +43,6 @@ pub struct WindowsWindowState {
     pub callbacks: Callbacks,
     pub input_handler: Option<PlatformInputHandler>,
     pub last_reported_modifiers: Option<Modifiers>,
-    pub suppress_next_char_msg: bool,
     pub system_key_handled: bool,
     pub hovered: bool,
 
@@ -103,7 +102,6 @@ impl WindowsWindowState {
         let callbacks = Callbacks::default();
         let input_handler = None;
         let last_reported_modifiers = None;
-        let suppress_next_char_msg = false;
         let system_key_handled = false;
         let hovered = false;
         let click_state = ClickState::new();
@@ -123,7 +121,6 @@ impl WindowsWindowState {
             callbacks,
             input_handler,
             last_reported_modifiers,
-            suppress_next_char_msg,
             system_key_handled,
             hovered,
             renderer,