windows: Fix keystroke (#30753)

Closes #22656

Part of #29144, this PR completely rewrites the key handling logic on
Windows, making it much more consistent with how things work on macOS.
However, one remaining issue is that on Windows, we should be using
`Ctrl+Shift+4` instead of `Ctrl+$`. That part is expected to be
addressed in #29144.


Release Notes:

- N/A
This commit is contained in:
张小白 2025-05-15 20:49:06 +08:00 committed by GitHub
parent f021b401f4
commit 58ba833792
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 287 additions and 280 deletions

View file

@ -56,6 +56,7 @@ impl Keystroke {
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
/// both possibilities for self against the target.
pub(crate) fn should_match(&self, target: &Keystroke) -> bool {
#[cfg(not(target_os = "windows"))]
if let Some(key_char) = self
.key_char
.as_ref()
@ -72,6 +73,18 @@ impl Keystroke {
}
}
#[cfg(target_os = "windows")]
if let Some(key_char) = self
.key_char
.as_ref()
.filter(|key_char| key_char != &&self.key)
{
// On Windows, if key_char is set, then the typed keystroke produced the key_char
if &target.key == key_char && target.modifiers == Modifiers::none() {
return true;
}
}
target.modifiers == self.modifiers && target.key == self.key
}

View file

@ -84,11 +84,11 @@ pub(crate) fn handle_msg(
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, state_ptr),
WM_SYSKEYUP => handle_syskeyup_msg(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, state_ptr),
WM_CHAR => handle_char_msg(wparam, lparam, state_ptr),
WM_KEYUP => handle_keyup_msg(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),
WM_IME_COMPOSITION => handle_ime_composition(handle, lparam, state_ptr),
@ -344,170 +344,120 @@ fn handle_syskeydown_msg(
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let mut lock = state_ptr.state.borrow_mut();
let vkey = wparam.loword();
let input = if is_modifier(VIRTUAL_KEY(vkey)) {
let modifiers = current_modifiers();
if let Some(prev_modifiers) = lock.last_reported_modifiers {
if prev_modifiers == modifiers {
return Some(0);
}
}
lock.last_reported_modifiers = Some(modifiers);
PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers })
} else {
let keystroke = parse_syskeydown_msg_keystroke(wparam)?;
let input = handle_key_event(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 result = if !func(input).propagate {
state_ptr.state.borrow_mut().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
};
state_ptr.state.borrow_mut().callbacks.input = Some(func);
result
}
let handled = !func(input).propagate;
fn handle_syskeyup_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
let mut lock = state_ptr.state.borrow_mut();
let vkey = wparam.loword();
let input = if is_modifier(VIRTUAL_KEY(vkey)) {
let modifiers = current_modifiers();
if let Some(prev_modifiers) = lock.last_reported_modifiers {
if prev_modifiers == modifiers {
return Some(0);
}
}
lock.last_reported_modifiers = Some(modifiers);
PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers })
} else {
let keystroke = parse_syskeydown_msg_keystroke(wparam)?;
PlatformInput::KeyUp(KeyUpEvent { keystroke })
};
let mut func = lock.callbacks.input.take()?;
drop(lock);
let result = if !func(input).propagate {
lock.callbacks.input = Some(func);
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}`
// shortcuts.
None
};
state_ptr.state.borrow_mut().callbacks.input = Some(func);
result
}
}
fn handle_syskeyup_msg(
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| {
PlatformInput::KeyUp(KeyUpEvent { keystroke })
})?;
let mut func = lock.callbacks.input.take()?;
drop(lock);
func(input);
state_ptr.state.borrow_mut().callbacks.input = Some(func);
// Always return 0 to indicate that the message was handled, so we could properly handle `ModifiersChanged` event.
Some(0)
}
// 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(
wparam: WPARAM,
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let Some(keystroke_or_modifier) = parse_keystroke_from_vkey(wparam, false) else {
return Some(1);
};
let mut lock = state_ptr.state.borrow_mut();
let event = match keystroke_or_modifier {
KeystrokeOrModifier::Keystroke(keystroke) => PlatformInput::KeyDown(KeyDownEvent {
let Some(input) = handle_key_event(wparam, lparam, &mut lock, |keystroke| {
PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: lparam.0 & (0x1 << 30) > 0,
}),
KeystrokeOrModifier::Modifier(modifiers) => {
if let Some(prev_modifiers) = lock.last_reported_modifiers {
if prev_modifiers == modifiers {
return Some(0);
}
}
lock.last_reported_modifiers = Some(modifiers);
PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers })
}
})
}) else {
return Some(1);
};
let Some(mut func) = lock.callbacks.input.take() else {
return Some(1);
};
drop(lock);
let result = if func(event).default_prevented {
Some(0)
} else {
Some(1)
};
state_ptr.state.borrow_mut().callbacks.input = Some(func);
let handled = !func(input).propagate;
result
}
fn handle_keyup_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
let Some(keystroke_or_modifier) = parse_keystroke_from_vkey(wparam, true) else {
return Some(1);
};
let mut lock = state_ptr.state.borrow_mut();
lock.callbacks.input = Some(func);
let event = match keystroke_or_modifier {
KeystrokeOrModifier::Keystroke(keystroke) => PlatformInput::KeyUp(KeyUpEvent { keystroke }),
KeystrokeOrModifier::Modifier(modifiers) => {
if let Some(prev_modifiers) = lock.last_reported_modifiers {
if prev_modifiers == modifiers {
return Some(0);
}
}
lock.last_reported_modifiers = Some(modifiers);
PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers })
}
};
let Some(mut func) = lock.callbacks.input.take() else {
return Some(1);
};
drop(lock);
let result = if func(event).default_prevented {
if handled {
lock.suppress_next_char_msg = true;
Some(0)
} else {
Some(1)
};
state_ptr.state.borrow_mut().callbacks.input = Some(func);
result
}
}
fn handle_char_msg(
fn handle_keyup_msg(
wparam: WPARAM,
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let Some(keystroke) = parse_char_msg_keystroke(wparam) else {
let mut lock = state_ptr.state.borrow_mut();
let Some(input) = handle_key_event(wparam, lparam, &mut lock, |keystroke| {
PlatformInput::KeyUp(KeyUpEvent { keystroke })
}) else {
return Some(1);
};
let mut lock = state_ptr.state.borrow_mut();
let Some(mut func) = lock.callbacks.input.take() else {
return Some(1);
};
drop(lock);
let key_char = keystroke.key_char.clone();
let event = KeyDownEvent {
keystroke,
is_held: lparam.0 & (0x1 << 30) > 0,
let result = if func(input).default_prevented {
Some(0)
} else {
Some(1)
};
let dispatch_event_result = func(PlatformInput::KeyDown(event));
state_ptr.state.borrow_mut().callbacks.input = Some(func);
if dispatch_event_result.default_prevented || !dispatch_event_result.propagate {
return Some(0);
}
let Some(ime_char) = key_char else {
result
}
fn handle_char_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
let Some(input) = char::from_u32(wparam.0 as u32)
.filter(|c| !c.is_control())
.map(String::from)
else {
return Some(1);
};
with_input_handler(&state_ptr, |input_handler| {
input_handler.replace_text_in_range(None, &ime_char);
input_handler.replace_text_in_range(None, &input);
});
Some(0)
@ -1297,153 +1247,118 @@ fn handle_input_language_changed(
Some(0)
}
fn parse_syskeydown_msg_keystroke(wparam: WPARAM) -> Option<Keystroke> {
let modifiers = current_modifiers();
let vk_code = wparam.loword();
fn handle_key_event<F>(
wparam: WPARAM,
lparam: LPARAM,
state: &mut WindowsWindowState,
f: F,
) -> Option<PlatformInput>
where
F: FnOnce(Keystroke) -> PlatformInput,
{
state.suppress_next_char_msg = false;
let virtual_key = VIRTUAL_KEY(wparam.loword());
let mut modifiers = current_modifiers();
// on Windows, F10 can trigger this event, not just the alt key,
// so when F10 was pressed, handle only it
if !modifiers.alt {
if vk_code == VK_F10.0 {
let offset = vk_code - VK_F1.0;
return Some(Keystroke {
modifiers,
key: format!("f{}", offset + 1),
key_char: None,
});
} else {
return None;
match virtual_key {
VK_PROCESSKEY => {
// IME composition
None
}
}
let key = match VIRTUAL_KEY(vk_code) {
VK_BACK => "backspace",
VK_RETURN => "enter",
VK_TAB => "tab",
VK_UP => "up",
VK_DOWN => "down",
VK_RIGHT => "right",
VK_LEFT => "left",
VK_HOME => "home",
VK_END => "end",
VK_PRIOR => "pageup",
VK_NEXT => "pagedown",
VK_BROWSER_BACK => "back",
VK_BROWSER_FORWARD => "forward",
VK_ESCAPE => "escape",
VK_INSERT => "insert",
VK_DELETE => "delete",
VK_APPS => "menu",
_ => {
let basic_key = basic_vkcode_to_string(vk_code, modifiers);
if basic_key.is_some() {
return basic_key;
} else {
if vk_code >= VK_F1.0 && vk_code <= VK_F24.0 {
let offset = vk_code - VK_F1.0;
return Some(Keystroke {
modifiers,
key: format!("f{}", offset + 1),
key_char: None,
});
} else {
return None;
}
VK_SHIFT | VK_CONTROL | VK_MENU | VK_LWIN | VK_RWIN => {
if state
.last_reported_modifiers
.is_some_and(|prev_modifiers| prev_modifiers == modifiers)
{
return None;
}
state.last_reported_modifiers = Some(modifiers);
Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
modifiers,
}))
}
vkey => {
let keystroke = parse_normal_key(vkey, lparam, modifiers)?;
Some(f(keystroke))
}
}
.to_owned();
}
fn parse_immutable(vkey: VIRTUAL_KEY) -> Option<String> {
Some(
match vkey {
VK_SPACE => "space",
VK_BACK => "backspace",
VK_RETURN => "enter",
VK_TAB => "tab",
VK_UP => "up",
VK_DOWN => "down",
VK_RIGHT => "right",
VK_LEFT => "left",
VK_HOME => "home",
VK_END => "end",
VK_PRIOR => "pageup",
VK_NEXT => "pagedown",
VK_BROWSER_BACK => "back",
VK_BROWSER_FORWARD => "forward",
VK_ESCAPE => "escape",
VK_INSERT => "insert",
VK_DELETE => "delete",
VK_APPS => "menu",
VK_F1 => "f1",
VK_F2 => "f2",
VK_F3 => "f3",
VK_F4 => "f4",
VK_F5 => "f5",
VK_F6 => "f6",
VK_F7 => "f7",
VK_F8 => "f8",
VK_F9 => "f9",
VK_F10 => "f10",
VK_F11 => "f11",
VK_F12 => "f12",
VK_F13 => "f13",
VK_F14 => "f14",
VK_F15 => "f15",
VK_F16 => "f16",
VK_F17 => "f17",
VK_F18 => "f18",
VK_F19 => "f19",
VK_F20 => "f20",
VK_F21 => "f21",
VK_F22 => "f22",
VK_F23 => "f23",
VK_F24 => "f24",
_ => return None,
}
.to_string(),
)
}
fn parse_normal_key(
vkey: VIRTUAL_KEY,
lparam: LPARAM,
mut modifiers: Modifiers,
) -> Option<Keystroke> {
let mut key_char = None;
let key = parse_immutable(vkey).or_else(|| {
let scan_code = lparam.hiword() & 0xFF;
key_char = generate_key_char(
vkey,
scan_code as u32,
modifiers.control,
modifiers.shift,
modifiers.alt,
);
get_keystroke_key(vkey, scan_code as u32, &mut modifiers)
})?;
Some(Keystroke {
modifiers,
key,
key_char: None,
key_char,
})
}
enum KeystrokeOrModifier {
Keystroke(Keystroke),
Modifier(Modifiers),
}
fn parse_keystroke_from_vkey(wparam: WPARAM, is_keyup: bool) -> Option<KeystrokeOrModifier> {
let vk_code = wparam.loword();
let modifiers = current_modifiers();
let key = match VIRTUAL_KEY(vk_code) {
VK_BACK => "backspace",
VK_RETURN => "enter",
VK_TAB => "tab",
VK_UP => "up",
VK_DOWN => "down",
VK_RIGHT => "right",
VK_LEFT => "left",
VK_HOME => "home",
VK_END => "end",
VK_PRIOR => "pageup",
VK_NEXT => "pagedown",
VK_BROWSER_BACK => "back",
VK_BROWSER_FORWARD => "forward",
VK_ESCAPE => "escape",
VK_INSERT => "insert",
VK_DELETE => "delete",
VK_APPS => "menu",
_ => {
if is_modifier(VIRTUAL_KEY(vk_code)) {
return Some(KeystrokeOrModifier::Modifier(modifiers));
}
if modifiers.control || modifiers.alt || is_keyup {
let basic_key = basic_vkcode_to_string(vk_code, modifiers);
if let Some(basic_key) = basic_key {
return Some(KeystrokeOrModifier::Keystroke(basic_key));
}
}
if vk_code >= VK_F1.0 && vk_code <= VK_F24.0 {
let offset = vk_code - VK_F1.0;
return Some(KeystrokeOrModifier::Keystroke(Keystroke {
modifiers,
key: format!("f{}", offset + 1),
key_char: None,
}));
};
return None;
}
}
.to_owned();
Some(KeystrokeOrModifier::Keystroke(Keystroke {
modifiers,
key,
key_char: None,
}))
}
fn parse_char_msg_keystroke(wparam: WPARAM) -> Option<Keystroke> {
let first_char = char::from_u32((wparam.0 as u16).into())?;
if first_char.is_control() {
None
} else {
let mut modifiers = current_modifiers();
// for characters that use 'shift' to type it is expected that the
// shift is not reported if the uppercase/lowercase are the same and instead only the key is reported
if first_char.to_ascii_uppercase() == first_char.to_ascii_lowercase() {
modifiers.shift = false;
}
let key = match first_char {
' ' => "space".to_string(),
first_char => first_char.to_lowercase().to_string(),
};
Some(Keystroke {
modifiers,
key,
key_char: Some(first_char.to_string()),
})
}
}
fn parse_ime_compostion_string(ctx: HIMC) -> Option<String> {
unsafe {
let string_len = ImmGetCompositionStringW(ctx, GCS_COMPSTR, None, 0);
@ -1494,40 +1409,11 @@ fn parse_ime_compostion_result(ctx: HIMC) -> Option<String> {
}
}
fn basic_vkcode_to_string(code: u16, modifiers: Modifiers) -> Option<Keystroke> {
let mapped_code = unsafe { MapVirtualKeyW(code as u32, MAPVK_VK_TO_CHAR) };
let key = match mapped_code {
0 => None,
raw_code => char::from_u32(raw_code),
}?
.to_ascii_lowercase();
let key = if matches!(code as u32, 112..=135) {
format!("f{key}")
} else {
key.to_string()
};
Some(Keystroke {
modifiers,
key,
key_char: None,
})
}
#[inline]
fn is_virtual_key_pressed(vkey: VIRTUAL_KEY) -> bool {
unsafe { GetKeyState(vkey.0 as i32) < 0 }
}
fn is_modifier(virtual_key: VIRTUAL_KEY) -> bool {
matches!(
virtual_key,
VK_CONTROL | VK_MENU | VK_SHIFT | VK_LWIN | VK_RWIN
)
}
#[inline]
pub(crate) fn current_modifiers() -> Modifiers {
Modifiers {
@ -1639,7 +1525,12 @@ fn with_input_handler<F, R>(state_ptr: &Rc<WindowsWindowStatePtr>, f: F) -> Opti
where
F: FnOnce(&mut PlatformInputHandler) -> R,
{
let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?;
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 result = f(&mut input_handler);
state_ptr.state.borrow_mut().input_handler = Some(input_handler);
Some(result)
@ -1653,6 +1544,9 @@ 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

@ -1,10 +1,16 @@
use anyhow::Result;
use windows::Win32::UI::{
Input::KeyboardAndMouse::GetKeyboardLayoutNameW, WindowsAndMessaging::KL_NAMELENGTH,
Input::KeyboardAndMouse::{
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU,
VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102,
VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
},
WindowsAndMessaging::KL_NAMELENGTH,
};
use windows_core::HSTRING;
use crate::PlatformKeyboardLayout;
use crate::{Modifiers, PlatformKeyboardLayout};
pub(crate) struct WindowsKeyboardLayout {
id: String,
@ -41,3 +47,94 @@ impl WindowsKeyboardLayout {
}
}
}
pub(crate) fn get_keystroke_key(
vkey: VIRTUAL_KEY,
scan_code: u32,
modifiers: &mut Modifiers,
) -> Option<String> {
if modifiers.shift && need_to_convert_to_shifted_key(vkey) {
get_shifted_key(vkey, scan_code).inspect(|_| {
modifiers.shift = false;
})
} else {
get_key_from_vkey(vkey)
}
}
fn get_key_from_vkey(vkey: VIRTUAL_KEY) -> Option<String> {
let key_data = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_CHAR) };
if key_data == 0 {
return None;
}
// The high word contains dead key flag, the low word contains the character
let key = char::from_u32(key_data & 0xFFFF)?;
Some(key.to_ascii_lowercase().to_string())
}
#[inline]
fn need_to_convert_to_shifted_key(vkey: VIRTUAL_KEY) -> bool {
matches!(
vkey,
VK_OEM_3
| VK_OEM_MINUS
| VK_OEM_PLUS
| VK_OEM_4
| VK_OEM_5
| VK_OEM_6
| VK_OEM_1
| VK_OEM_7
| VK_OEM_COMMA
| VK_OEM_PERIOD
| VK_OEM_2
| VK_OEM_102
| VK_OEM_8
| VK_ABNT_C1
| VK_0
| VK_1
| VK_2
| VK_3
| VK_4
| VK_5
| VK_6
| VK_7
| VK_8
| VK_9
)
}
fn get_shifted_key(vkey: VIRTUAL_KEY, scan_code: u32) -> Option<String> {
generate_key_char(vkey, scan_code, false, true, false)
}
pub(crate) fn generate_key_char(
vkey: VIRTUAL_KEY,
scan_code: u32,
control: bool,
shift: bool,
alt: bool,
) -> Option<String> {
let mut state = [0; 256];
if control {
state[VK_CONTROL.0 as usize] = 0x80;
}
if shift {
state[VK_SHIFT.0 as usize] = 0x80;
}
if alt {
state[VK_MENU.0 as usize] = 0x80;
}
let mut buffer = [0; 8];
let len = unsafe { ToUnicode(vkey.0 as u32, scan_code, Some(&state), &mut buffer, 1 << 2) };
if len > 0 {
let candidate = String::from_utf16_lossy(&buffer[..len as usize]);
if !candidate.is_empty() && !candidate.chars().next().unwrap().is_control() {
return Some(candidate);
}
}
None
}

View file

@ -43,6 +43,7 @@ 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,
@ -102,6 +103,7 @@ 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();
@ -121,6 +123,7 @@ impl WindowsWindowState {
callbacks,
input_handler,
last_reported_modifiers,
suppress_next_char_msg,
system_key_handled,
hovered,
renderer,