fix: preserve native Windows key events for ConPTY input
This commit is contained in:
1 parent
ffb2eebaf6
commit
8fa86ead34
7 files changed
+425
-103
No files matched your search
@@ -4,8 +4,6 @@ use winit::event::{ElementState, KeyEvent};
|
||||
#[cfg(target_os = "macos")]
|
||||
use winit::keyboard::ModifiersKeyState;
|
||||
use winit::keyboard::{Key, KeyLocation, ModifiersState, NamedKey};
|
||||
#[cfg(target_os = "windows")]
|
||||
use winit::platform::scancode::PhysicalKeyExtScancode;
|
||||
#[cfg(target_os = "macos")]
|
||||
use winit::platform::macos::OptionAsAlt;
|
||||
|
||||
@@ -16,14 +14,9 @@ use winit::platform::modifier_supplement::KeyEventExtModifierSupplement;
|
||||
use crate::config::{Action, BindingKey, BindingMode, KeyBinding};
|
||||
use crate::display::window::ImeInhibitor;
|
||||
use crate::event::TYPING_SEARCH_DELAY;
|
||||
use crate::input::{ActionContext, Execute, Processor};
|
||||
use crate::input::{ActionContext, Execute, Processor, terminal_input};
|
||||
use crate::scheduler::{TimerId, Topic};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use windows_sys::Win32::UI::Input::KeyboardAndMouse::{
|
||||
MapVirtualKeyW, MAPVK_VSC_TO_VK_EX,
|
||||
};
|
||||
|
||||
impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
|
||||
/// Process key input.
|
||||
pub fn key_input(&mut self, key: KeyEvent) {
|
||||
@@ -995,7 +988,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
|
||||
mods: ModifiersState,
|
||||
) -> bool {
|
||||
if mode.contains(TermMode::WIN32_INPUT_MODE)
|
||||
&& build_win32_input_sequence(key, mods).is_some()
|
||||
&& terminal_input::build_win32_input_sequence(key).is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -1159,7 +1152,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
|
||||
#[inline(never)]
|
||||
fn build_sequence(key: KeyEvent, mods: ModifiersState, mode: TermMode) -> Vec<u8> {
|
||||
if mode.contains(TermMode::WIN32_INPUT_MODE) {
|
||||
if let Some(sequence) = build_win32_input_sequence(&key, mods) {
|
||||
if let Some(sequence) = terminal_input::build_win32_input_sequence(&key) {
|
||||
return sequence;
|
||||
}
|
||||
}
|
||||
@@ -1231,74 +1224,6 @@ fn build_sequence(key: KeyEvent, mods: ModifiersState, mode: TermMode) -> Vec<u8
|
||||
payload.into_bytes()
|
||||
}
|
||||
|
||||
/// Build a ConPTY Win32 input record for functional keys and modified chords.
|
||||
/// Printable text deliberately returns `None`, so IME and layout-generated
|
||||
/// characters continue through the normal UTF-8 path used by every shell.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn build_win32_input_sequence(key: &KeyEvent, mods: ModifiersState) -> Option<Vec<u8>> {
|
||||
if win32_input_uses_text_path(&key.logical_key) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (virtual_key, scan_code, enhanced) = win32_key_codes(key.physical_key)?;
|
||||
|
||||
const SHIFT_PRESSED: u32 = 0x10;
|
||||
const RIGHT_ALT_PRESSED: u32 = 0x01;
|
||||
const LEFT_ALT_PRESSED: u32 = 0x02;
|
||||
const RIGHT_CTRL_PRESSED: u32 = 0x04;
|
||||
const LEFT_CTRL_PRESSED: u32 = 0x08;
|
||||
const ENHANCED_KEY: u32 = 0x100;
|
||||
|
||||
let current_is_shift = matches!(key.logical_key, Key::Named(NamedKey::Shift));
|
||||
let current_is_alt = matches!(key.logical_key, Key::Named(NamedKey::Alt));
|
||||
let current_is_ctrl = matches!(key.logical_key, Key::Named(NamedKey::Control));
|
||||
let pressed = key.state == ElementState::Pressed;
|
||||
let mut control_key_state = 0;
|
||||
if (mods.shift_key() || (current_is_shift && pressed)) && !(current_is_shift && !pressed) {
|
||||
control_key_state |= SHIFT_PRESSED;
|
||||
}
|
||||
if (mods.alt_key() || (current_is_alt && pressed)) && !(current_is_alt && !pressed) {
|
||||
control_key_state |=
|
||||
if current_is_alt && enhanced { RIGHT_ALT_PRESSED } else { LEFT_ALT_PRESSED };
|
||||
}
|
||||
if (mods.control_key() || (current_is_ctrl && pressed)) && !(current_is_ctrl && !pressed) {
|
||||
control_key_state |=
|
||||
if current_is_ctrl && enhanced { RIGHT_CTRL_PRESSED } else { LEFT_CTRL_PRESSED };
|
||||
}
|
||||
if enhanced {
|
||||
control_key_state |= ENHANCED_KEY;
|
||||
}
|
||||
|
||||
let down = u8::from(pressed);
|
||||
Some(
|
||||
format!(
|
||||
"\x1b[{virtual_key};{scan_code};0;{down};{control_key_state};1_"
|
||||
)
|
||||
.into_bytes(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn win32_input_uses_text_path(key: &Key) -> bool {
|
||||
matches!(key, Key::Character(_) | Key::Named(NamedKey::Space))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn win32_key_codes(key: winit::keyboard::PhysicalKey) -> Option<(u32, u32, bool)> {
|
||||
let extended_scan_code = key.to_scancode()?;
|
||||
let virtual_key = unsafe { MapVirtualKeyW(extended_scan_code, MAPVK_VSC_TO_VK_EX) };
|
||||
(virtual_key != 0).then_some((
|
||||
virtual_key,
|
||||
extended_scan_code & 0xff,
|
||||
extended_scan_code > 0xff,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn build_win32_input_sequence(_: &KeyEvent, _: ModifiersState) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Helper to build escape sequence payloads from [`KeyEvent`].
|
||||
pub struct SequenceBuilder {
|
||||
mode: TermMode,
|
||||
@@ -1654,26 +1579,3 @@ fn is_control_character(text: &str) -> bool {
|
||||
let codepoint = text.bytes().next().unwrap();
|
||||
text.len() == 1 && (codepoint < 0x20 || (0x7f..=0x9f).contains(&codepoint))
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "windows"))]
|
||||
mod win32_input_tests {
|
||||
use super::*;
|
||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
|
||||
#[test]
|
||||
fn printable_symbols_remain_on_the_utf8_text_path() {
|
||||
for text in [",", "。", "/", "【", "】"] {
|
||||
assert!(win32_input_uses_text_path(&Key::Character(text.into())));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_uses_the_native_windows_key_identity() {
|
||||
let (virtual_key, scan_code, enhanced) =
|
||||
win32_key_codes(PhysicalKey::Code(KeyCode::Enter)).unwrap();
|
||||
|
||||
assert_eq!(virtual_key, 0x0d);
|
||||
assert_eq!(scan_code, 0x1c);
|
||||
assert!(!enhanced);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ mod action;
|
||||
mod chrome;
|
||||
pub mod keyboard;
|
||||
mod mouse;
|
||||
mod terminal_input;
|
||||
mod touch;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
//! Platform input encoders kept outside the general keyboard dispatcher.
|
||||
//!
|
||||
//! The dispatcher decides whether an event belongs to the terminal or to the
|
||||
//! Nebula chrome. This module owns the wire format for platform terminal modes;
|
||||
//! keeping that contract isolated makes adding a Linux or macOS raw backend a
|
||||
//! local change instead of another branch in `keyboard.rs`.
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::fmt::Write;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use winit::event::ElementState;
|
||||
use winit::event::KeyEvent;
|
||||
#[cfg(target_os = "windows")]
|
||||
use winit::keyboard::{Key, NamedKey};
|
||||
#[cfg(target_os = "windows")]
|
||||
use winit::platform::modifier_supplement::KeyEventExtModifierSupplement;
|
||||
|
||||
/// Build one or more ConPTY Win32 input-mode records for a native key event.
|
||||
///
|
||||
/// Dead keys intentionally return `None`; their eventual composition is
|
||||
/// delivered through Winit's OS-resolved text/IME path. Every other key keeps
|
||||
/// its native Windows identity. Printable keys additionally carry the UTF-16
|
||||
/// code units emitted by WM_CHAR, matching KEY_EVENT_RECORD semantics without
|
||||
/// reconstructing VKEYs from text.
|
||||
#[inline]
|
||||
pub(crate) fn build_win32_input_sequence(key: &KeyEvent) -> Option<Vec<u8>> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if uses_composition_path(&key.logical_key) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let raw = winit::platform::windows::KeyEventExtWindows::raw_key_event(key);
|
||||
let mut sequence = String::with_capacity(48);
|
||||
match key.logical_key.as_ref() {
|
||||
Key::Character(_) | Key::Named(NamedKey::Space) => {
|
||||
let mut wrote_text = false;
|
||||
if let Some(text) = key.text_with_all_modifiers().filter(|text| !text.is_empty()) {
|
||||
for unicode_char in text.encode_utf16() {
|
||||
append_raw_key_event(&mut sequence, raw, key.state, unicode_char);
|
||||
wrote_text = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !wrote_text {
|
||||
append_raw_key_event(&mut sequence, raw, key.state, raw.unicode_char);
|
||||
}
|
||||
},
|
||||
_ => append_raw_key_event(&mut sequence, raw, key.state, 0),
|
||||
}
|
||||
return Some(sequence.into_bytes());
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let _ = key;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[inline]
|
||||
fn uses_composition_path(key: &Key) -> bool {
|
||||
matches!(key, Key::Dead(_))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[inline]
|
||||
fn append_raw_key_event(
|
||||
sequence: &mut String,
|
||||
raw: winit::platform::windows::RawKeyEventInfo,
|
||||
state: ElementState,
|
||||
unicode_char: u16,
|
||||
) {
|
||||
// ConPTY Win32 input mode 的字段顺序是 CSI Vk;Sc;Uc;Kd;Cs;Rc_。
|
||||
let key_down = u8::from(state == ElementState::Pressed);
|
||||
let repeat_count = raw.repeat_count.max(1);
|
||||
let _ = write!(
|
||||
sequence,
|
||||
"\x1b[{};{};{};{};{};{}_",
|
||||
raw.virtual_key, raw.scan_code, unicode_char, key_down, raw.control_key_state, repeat_count
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "windows"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use winit::platform::windows::RawKeyEventInfo;
|
||||
|
||||
#[test]
|
||||
fn only_dead_keys_stay_on_the_composition_path() {
|
||||
assert!(!uses_composition_path(&Key::Character("/".into())));
|
||||
assert!(!uses_composition_path(&Key::Character("【】".into())));
|
||||
assert!(uses_composition_path(&Key::Dead(Some('\''))));
|
||||
assert!(!uses_composition_path(&Key::Named(NamedKey::Space)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn functional_key_uses_the_captured_native_identity() {
|
||||
let raw = RawKeyEventInfo {
|
||||
virtual_key: 0x0d,
|
||||
scan_code: 0x1c,
|
||||
repeat_count: 1,
|
||||
is_extended: false,
|
||||
unicode_char: 0,
|
||||
control_key_state: 0x10,
|
||||
};
|
||||
|
||||
let mut sequence = String::new();
|
||||
append_raw_key_event(&mut sequence, raw, ElementState::Pressed, 0);
|
||||
assert_eq!(sequence.as_bytes(), b"\x1b[13;28;0;1;16;1_");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printable_key_keeps_native_identity_and_unicode_char() {
|
||||
let raw = RawKeyEventInfo {
|
||||
virtual_key: 0xbf,
|
||||
scan_code: 0x35,
|
||||
repeat_count: 1,
|
||||
is_extended: false,
|
||||
unicode_char: b'/' as u16,
|
||||
control_key_state: 0,
|
||||
};
|
||||
|
||||
let mut sequence = String::new();
|
||||
append_raw_key_event(&mut sequence, raw, ElementState::Pressed, raw.unicode_char);
|
||||
assert_eq!(sequence.as_bytes(), b"\x1b[191;53;47;1;0;1_");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_count_zero_is_normalized_to_protocol_default() {
|
||||
let raw = RawKeyEventInfo {
|
||||
virtual_key: 0x25,
|
||||
scan_code: 0x4b,
|
||||
repeat_count: 0,
|
||||
is_extended: true,
|
||||
unicode_char: 0,
|
||||
control_key_state: 0x100,
|
||||
};
|
||||
|
||||
let mut sequence = String::new();
|
||||
append_raw_key_event(&mut sequence, raw, ElementState::Released, 0);
|
||||
assert_eq!(sequence.as_bytes(), b"\x1b[37;75;0;0;256;1_");
|
||||
}
|
||||
}
|
||||
+11
@@ -5,8 +5,19 @@ Windows backend backport from upstream commit `488c036a05d418e13bbdcdc349e2db2f6
|
||||
|
||||
- Windows 10 builds below 22000 keep the legacy mixed-DPI reposition workaround.
|
||||
- Windows 11 builds use the `WM_DPICHANGED` suggested rectangle directly.
|
||||
- Windows key events retain the raw `WM_KEYDOWN/UP` virtual key, scan code,
|
||||
repeat count, extended flag, layout-resolved UTF-16 character, and
|
||||
`KEY_EVENT_RECORD` control-key state. The
|
||||
public `winit::platform::windows::KeyEventExtWindows` trait exposes that
|
||||
snapshot to the terminal input adapter without a second Win32 message hook
|
||||
or a `MapVirtualKeyW` reverse lookup.
|
||||
|
||||
The upstream commit is on a newer winit API generation whose `EventLoop` is no
|
||||
longer generic. Keeping this 0.30.13 overlay preserves Nebula's existing
|
||||
`EventLoop<Event>` and `rwh_06` integration. Remove the overlay once an
|
||||
API-compatible crates.io release contains the same fix.
|
||||
|
||||
When updating this overlay, preserve the Windows `KeyEventExtra::raw` field and
|
||||
the `PartialKeyEventInfo`/`KeyLParam` propagation. The terminal's Win32 input
|
||||
mode depends on those fields for layout-independent functional keys and
|
||||
modifier chords; printable and IME text remains on Winit's normal text path.
|
||||
+19
-1
@@ -7,11 +7,13 @@ use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::dpi::PhysicalSize;
|
||||
use crate::event::DeviceId;
|
||||
use crate::event::{DeviceId, KeyEvent};
|
||||
use crate::event_loop::EventLoopBuilder;
|
||||
use crate::monitor::MonitorHandle;
|
||||
use crate::window::{BadIcon, Icon, Window, WindowAttributes};
|
||||
|
||||
pub use crate::platform_impl::RawKeyEventInfo;
|
||||
|
||||
/// Window Handle type used by Win32 API
|
||||
pub type HWND = isize;
|
||||
/// Menu Handle type used by Win32 API
|
||||
@@ -19,6 +21,22 @@ pub type HMENU = isize;
|
||||
/// Monitor Handle type used by Win32 API
|
||||
pub type HMONITOR = isize;
|
||||
|
||||
/// Provides the unmodified Win32 fields needed by ConPTY's Win32 input mode.
|
||||
///
|
||||
/// Winit already receives these values from WM_KEYDOWN/WM_KEYUP. Exposing the
|
||||
/// captured snapshot avoids reconstructing a virtual key from a Unicode value,
|
||||
/// which is incorrect for layout-dependent keys, AltGr, dead keys, and IMEs.
|
||||
pub trait KeyEventExtWindows {
|
||||
fn raw_key_event(&self) -> RawKeyEventInfo;
|
||||
}
|
||||
|
||||
impl KeyEventExtWindows for KeyEvent {
|
||||
#[inline]
|
||||
fn raw_key_event(&self) -> RawKeyEventInfo {
|
||||
self.platform_specific.raw
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a system-drawn backdrop material of a window.
|
||||
///
|
||||
/// For a detailed explanation, see [`DWM_SYSTEMBACKDROP_TYPE docs`].
|
||||
|
||||
+227
-1
@@ -33,7 +33,7 @@ use crate::platform_impl::platform::event_loop::ProcResult;
|
||||
use crate::platform_impl::platform::keyboard_layout::{
|
||||
Layout, LayoutCache, WindowsModifiers, LAYOUT_CACHE,
|
||||
};
|
||||
use crate::platform_impl::platform::{loword, primarylangid, KeyEventExtra};
|
||||
use crate::platform_impl::platform::{loword, primarylangid, KeyEventExtra, RawKeyEventInfo};
|
||||
|
||||
pub type ExScancode = u16;
|
||||
|
||||
@@ -444,6 +444,20 @@ impl KeyEventBuilder {
|
||||
key_without_modifiers,
|
||||
key_state,
|
||||
is_repeat: false,
|
||||
raw: RawKeyEventInfo {
|
||||
virtual_key: vk as u16,
|
||||
scan_code: (scancode & 0xff) as u8,
|
||||
repeat_count: 1,
|
||||
is_extended: (scancode & 0xff00) == 0xe000,
|
||||
unicode_char: win32_unicode_char(&logical_key),
|
||||
control_key_state: synthetic_control_key_state(
|
||||
vk,
|
||||
caps_lock_on,
|
||||
num_lock_on,
|
||||
(scancode & 0xff00) == 0xe000,
|
||||
key_state,
|
||||
),
|
||||
},
|
||||
physical_key,
|
||||
location: get_location(scancode, locale_id),
|
||||
utf16parts: Vec::with_capacity(8),
|
||||
@@ -484,6 +498,8 @@ struct PartialKeyEventInfo {
|
||||
|
||||
key_without_modifiers: Key,
|
||||
|
||||
raw: RawKeyEventInfo,
|
||||
|
||||
/// The UTF-16 code units of the text that was produced by the keypress event.
|
||||
/// This take all modifiers into account. Including CTRL
|
||||
utf16parts: Vec<u16>,
|
||||
@@ -536,6 +552,8 @@ impl PartialKeyEventInfo {
|
||||
|
||||
let preliminary_logical_key =
|
||||
layout.get_key(mods_without_ctrl, num_lock_on, vkey, &physical_key);
|
||||
let unicode_char =
|
||||
win32_unicode_char(&layout.get_key(mods, num_lock_on, vkey, &physical_key));
|
||||
let key_is_char = matches!(preliminary_logical_key, Key::Character(_));
|
||||
let is_pressed = state == ElementState::Pressed;
|
||||
|
||||
@@ -577,6 +595,19 @@ impl PartialKeyEventInfo {
|
||||
logical_key,
|
||||
key_without_modifiers,
|
||||
is_repeat: lparam_struct.is_repeat,
|
||||
raw: RawKeyEventInfo {
|
||||
virtual_key: vkey as u16,
|
||||
scan_code: lparam_struct.scancode,
|
||||
repeat_count: lparam_struct.repeat_count,
|
||||
is_extended: lparam_struct.extended,
|
||||
unicode_char,
|
||||
control_key_state: control_key_state(
|
||||
&kbd_state,
|
||||
vkey as u16,
|
||||
lparam_struct.extended,
|
||||
state,
|
||||
),
|
||||
},
|
||||
physical_key,
|
||||
location,
|
||||
utf16parts: Vec::with_capacity(8),
|
||||
@@ -623,6 +654,17 @@ impl PartialKeyEventInfo {
|
||||
PartialLogicalKey::This(v) => v,
|
||||
};
|
||||
|
||||
let mut raw = self.raw;
|
||||
if matches!(logical_key, Key::Character(_) | Key::Named(NamedKey::Space)) {
|
||||
if let Some(text) = char_with_all_modifiers.as_deref() {
|
||||
if let Some(unicode_char) = single_utf16_code_unit(text) {
|
||||
// WM_CHAR is the authoritative layout/dead-key result. The cached
|
||||
// layout value above is primarily needed for key-up events.
|
||||
raw.unicode_char = unicode_char;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent {
|
||||
physical_key: self.physical_key,
|
||||
logical_key,
|
||||
@@ -633,6 +675,7 @@ impl PartialKeyEventInfo {
|
||||
platform_specific: KeyEventExtra {
|
||||
text_with_all_modifiers: char_with_all_modifiers,
|
||||
key_without_modifiers: self.key_without_modifiers,
|
||||
raw,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -643,6 +686,10 @@ struct KeyLParam {
|
||||
pub scancode: u8,
|
||||
pub extended: bool,
|
||||
|
||||
/// The low word of lParam is the repeat count. Preserve it instead of
|
||||
/// converting every repeated key into a synthetic single press.
|
||||
pub repeat_count: u16,
|
||||
|
||||
/// This is `previous_state XOR transition_state`. See the lParam for WM_KEYDOWN and WM_KEYUP
|
||||
/// for further details.
|
||||
pub is_repeat: bool,
|
||||
@@ -654,10 +701,148 @@ fn destructure_key_lparam(lparam: LPARAM) -> KeyLParam {
|
||||
KeyLParam {
|
||||
scancode: ((lparam >> 16) & 0xff) as u8,
|
||||
extended: ((lparam >> 24) & 0x01) != 0,
|
||||
repeat_count: (lparam & 0xffff) as u16,
|
||||
is_repeat: (previous_state ^ transition_state) != 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn single_utf16_code_unit(text: &str) -> Option<u16> {
|
||||
let mut units = text.encode_utf16();
|
||||
let first = units.next()?;
|
||||
units.next().is_none().then_some(first)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn win32_unicode_char(key: &Key) -> u16 {
|
||||
match key {
|
||||
Key::Character(text) => single_utf16_code_unit(text).unwrap_or(0),
|
||||
Key::Named(NamedKey::Space) => b' ' as u16,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// 将布局解析已经读取的键盘状态转换为 Win32 KEY_EVENT_RECORD 字段。
|
||||
/// 状态在 Winit 内只读取一次,避免应用热路径再次调用 GetKeyboardState。
|
||||
fn control_key_state(
|
||||
kbd_state: &[u8; 256],
|
||||
virtual_key: u16,
|
||||
extended: bool,
|
||||
key_state: ElementState,
|
||||
) -> u32 {
|
||||
const RIGHT_ALT_PRESSED: u32 = 0x01;
|
||||
const LEFT_ALT_PRESSED: u32 = 0x02;
|
||||
const RIGHT_CTRL_PRESSED: u32 = 0x04;
|
||||
const LEFT_CTRL_PRESSED: u32 = 0x08;
|
||||
const SHIFT_PRESSED: u32 = 0x10;
|
||||
const NUMLOCK_ON: u32 = 0x20;
|
||||
const SCROLLLOCK_ON: u32 = 0x40;
|
||||
const CAPSLOCK_ON: u32 = 0x80;
|
||||
const ENHANCED_KEY: u32 = 0x100;
|
||||
|
||||
let is_down = |vk: VIRTUAL_KEY| kbd_state[vk as usize] & 0x80 != 0;
|
||||
let mut state = 0;
|
||||
if is_down(VK_SHIFT) || is_down(VK_LSHIFT) || is_down(VK_RSHIFT) {
|
||||
state |= SHIFT_PRESSED;
|
||||
}
|
||||
if is_down(VK_LMENU) {
|
||||
state |= LEFT_ALT_PRESSED;
|
||||
}
|
||||
if is_down(VK_RMENU) {
|
||||
state |= RIGHT_ALT_PRESSED;
|
||||
}
|
||||
if state & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) == 0 && is_down(VK_MENU) {
|
||||
state |= if virtual_key as VIRTUAL_KEY == VK_MENU && extended {
|
||||
RIGHT_ALT_PRESSED
|
||||
} else {
|
||||
LEFT_ALT_PRESSED
|
||||
};
|
||||
}
|
||||
if is_down(VK_LCONTROL) {
|
||||
state |= LEFT_CTRL_PRESSED;
|
||||
}
|
||||
if is_down(VK_RCONTROL) {
|
||||
state |= RIGHT_CTRL_PRESSED;
|
||||
}
|
||||
if state & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) == 0 && is_down(VK_CONTROL) {
|
||||
state |= if virtual_key as VIRTUAL_KEY == VK_CONTROL && extended {
|
||||
RIGHT_CTRL_PRESSED
|
||||
} else {
|
||||
LEFT_CTRL_PRESSED
|
||||
};
|
||||
}
|
||||
if kbd_state[VK_NUMLOCK as usize] & 1 != 0 {
|
||||
state |= NUMLOCK_ON;
|
||||
}
|
||||
if kbd_state[VK_SCROLL as usize] & 1 != 0 {
|
||||
state |= SCROLLLOCK_ON;
|
||||
}
|
||||
if kbd_state[VK_CAPITAL as usize] & 1 != 0 {
|
||||
state |= CAPSLOCK_ON;
|
||||
}
|
||||
if extended {
|
||||
state |= ENHANCED_KEY;
|
||||
}
|
||||
|
||||
// 某些布局在 GetKeyboardState 中只报告通用修饰键,但 WM_KEY 消息仍带有
|
||||
// 左右位置;仅对 keydown 补入当前消息的身份,避免 keyup 把按下位卡住。
|
||||
if key_state == ElementState::Pressed {
|
||||
match virtual_key as VIRTUAL_KEY {
|
||||
VK_LMENU => state |= LEFT_ALT_PRESSED,
|
||||
VK_RMENU => state |= RIGHT_ALT_PRESSED,
|
||||
VK_LCONTROL => state |= LEFT_CTRL_PRESSED,
|
||||
VK_RCONTROL => state |= RIGHT_CTRL_PRESSED,
|
||||
VK_LSHIFT | VK_RSHIFT => state |= SHIFT_PRESSED,
|
||||
VK_MENU if extended => state |= RIGHT_ALT_PRESSED,
|
||||
VK_MENU => state |= LEFT_ALT_PRESSED,
|
||||
VK_CONTROL if extended => state |= RIGHT_CTRL_PRESSED,
|
||||
VK_CONTROL => state |= LEFT_CTRL_PRESSED,
|
||||
VK_SHIFT => state |= SHIFT_PRESSED,
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
fn synthetic_control_key_state(
|
||||
virtual_key: VIRTUAL_KEY,
|
||||
caps_lock_on: bool,
|
||||
num_lock_on: bool,
|
||||
extended: bool,
|
||||
key_state: ElementState,
|
||||
) -> u32 {
|
||||
const RIGHT_ALT_PRESSED: u32 = 0x01;
|
||||
const LEFT_ALT_PRESSED: u32 = 0x02;
|
||||
const RIGHT_CTRL_PRESSED: u32 = 0x04;
|
||||
const LEFT_CTRL_PRESSED: u32 = 0x08;
|
||||
const SHIFT_PRESSED: u32 = 0x10;
|
||||
const NUMLOCK_ON: u32 = 0x20;
|
||||
const CAPSLOCK_ON: u32 = 0x80;
|
||||
const ENHANCED_KEY: u32 = 0x100;
|
||||
|
||||
let mut state = 0;
|
||||
if caps_lock_on {
|
||||
state |= CAPSLOCK_ON;
|
||||
}
|
||||
if num_lock_on {
|
||||
state |= NUMLOCK_ON;
|
||||
}
|
||||
if extended {
|
||||
state |= ENHANCED_KEY;
|
||||
}
|
||||
if key_state == ElementState::Pressed {
|
||||
match virtual_key {
|
||||
VK_LSHIFT | VK_RSHIFT => state |= SHIFT_PRESSED,
|
||||
VK_LMENU => state |= LEFT_ALT_PRESSED,
|
||||
VK_RMENU => state |= RIGHT_ALT_PRESSED,
|
||||
VK_LCONTROL => state |= LEFT_CTRL_PRESSED,
|
||||
VK_RCONTROL => state |= RIGHT_CTRL_PRESSED,
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn new_ex_scancode(scancode: u8, extended: bool) -> ExScancode {
|
||||
(scancode as u16) | (if extended { 0xe000 } else { 0 })
|
||||
@@ -1241,3 +1426,44 @@ pub(crate) fn scancode_to_physicalkey(scancode: u32) -> PhysicalKey {
|
||||
_ => return PhysicalKey::Unidentified(NativeKeyCode::Windows(scancode as u16)),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn key_lparam_keeps_repeat_and_extended_bits() {
|
||||
let lparam = (3 | (0x1c << 16) | (1 << 24)) as LPARAM;
|
||||
let parsed = destructure_key_lparam(lparam);
|
||||
|
||||
assert_eq!(parsed.repeat_count, 3);
|
||||
assert_eq!(parsed.scancode, 0x1c);
|
||||
assert!(parsed.extended);
|
||||
assert!(!parsed.is_repeat);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifier_release_does_not_retain_the_pressed_bit() {
|
||||
let mut keyboard = [0; 256];
|
||||
keyboard[VK_LSHIFT as usize] = 0x80;
|
||||
|
||||
assert_ne!(
|
||||
control_key_state(&keyboard, VK_LSHIFT as u16, false, ElementState::Pressed) & 0x10,
|
||||
0
|
||||
);
|
||||
|
||||
keyboard[VK_LSHIFT as usize] = 0;
|
||||
assert_eq!(
|
||||
control_key_state(&keyboard, VK_LSHIFT as u16, false, ElementState::Released) & 0x10,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_char_requires_exactly_one_utf16_code_unit() {
|
||||
assert_eq!(single_utf16_code_unit("/"), Some(47));
|
||||
assert_eq!(single_utf16_code_unit(","), Some(0xff0c));
|
||||
assert_eq!(single_utf16_code_unit("【】"), None);
|
||||
assert_eq!(single_utf16_code_unit("😀"), None);
|
||||
}
|
||||
}
|
||||
@@ -91,10 +91,28 @@ fn wrap_device_id(id: u32) -> RootDeviceId {
|
||||
|
||||
pub type OsError = std::io::Error;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct RawKeyEventInfo {
|
||||
/// The virtual-key code copied directly from the WM_KEY* message.
|
||||
pub virtual_key: u16,
|
||||
/// The low byte of the scan-code field in the WM_KEY* lParam.
|
||||
pub scan_code: u8,
|
||||
/// The repeat count copied from the low word of WM_KEY* lParam.
|
||||
pub repeat_count: u16,
|
||||
/// Whether the WM_KEY* lParam carried the extended-key flag.
|
||||
pub is_extended: bool,
|
||||
/// The UTF-16 code unit produced by the active Windows keyboard layout.
|
||||
/// A value of zero means that the key has no single-code-unit text value.
|
||||
pub unicode_char: u16,
|
||||
/// The KEY_EVENT_RECORD-compatible control-key state captured by Winit.
|
||||
pub control_key_state: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct KeyEventExtra {
|
||||
pub text_with_all_modifiers: Option<SmolStr>,
|
||||
pub key_without_modifiers: Key,
|
||||
pub raw: RawKeyEventInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
|
||||
Reference in new issue
Block a user