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

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
This commit is contained in:
张小白 2025-06-05 23:57:47 +08:00 committed by GitHub
parent 738cfdff84
commit 5b9d3ea097
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 48 additions and 34 deletions

View file

@ -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);

View file

@ -231,9 +231,6 @@ impl WindowsPlatform {
}
}
_ => {
// todo(windows)
// crate `windows 0.56` reports true as Err
TranslateMessage(&msg).as_bool();
DispatchMessageW(&msg);
}
}

View file

@ -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,