diff --git a/nebula_app/src/input/keyboard.rs b/nebula_app/src/input/keyboard.rs index 9fbb4c6..cd46a26 100644 --- a/nebula_app/src/input/keyboard.rs +++ b/nebula_app/src/input/keyboard.rs @@ -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> Processor { /// Process key input. pub fn key_input(&mut self, key: KeyEvent) { @@ -995,7 +988,7 @@ impl> Processor { 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> Processor { #[inline(never)] fn build_sequence(key: KeyEvent, mods: ModifiersState, mode: TermMode) -> Vec { 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 Option> { - 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> { - 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); - } -} diff --git a/nebula_app/src/input/mod.rs b/nebula_app/src/input/mod.rs index b98dad5..ce34ba0 100644 --- a/nebula_app/src/input/mod.rs +++ b/nebula_app/src/input/mod.rs @@ -33,6 +33,7 @@ mod action; mod chrome; pub mod keyboard; mod mouse; +mod terminal_input; mod touch; #[cfg(test)] diff --git a/nebula_app/src/input/terminal_input.rs b/nebula_app/src/input/terminal_input.rs new file mode 100644 index 0000000..10b7531 --- /dev/null +++ b/nebula_app/src/input/terminal_input.rs @@ -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> { + #[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_"); + } +} diff --git a/third_party/winit-0.30.13/README.nebula.md b/third_party/winit-0.30.13/README.nebula.md index 4291960..968b505 100644 --- a/third_party/winit-0.30.13/README.nebula.md +++ b/third_party/winit-0.30.13/README.nebula.md @@ -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` 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. diff --git a/third_party/winit-0.30.13/src/platform/windows.rs b/third_party/winit-0.30.13/src/platform/windows.rs index e4593be..b07c93b 100644 --- a/third_party/winit-0.30.13/src/platform/windows.rs +++ b/third_party/winit-0.30.13/src/platform/windows.rs @@ -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`]. diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/keyboard.rs b/third_party/winit-0.30.13/src/platform_impl/windows/keyboard.rs index 9c10949..69ea7be 100644 --- a/third_party/winit-0.30.13/src/platform_impl/windows/keyboard.rs +++ b/third_party/winit-0.30.13/src/platform_impl/windows/keyboard.rs @@ -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, @@ -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 { + 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); + } +} diff --git a/third_party/winit-0.30.13/src/platform_impl/windows/mod.rs b/third_party/winit-0.30.13/src/platform_impl/windows/mod.rs index 9bb02fd..d4ff68a 100644 --- a/third_party/winit-0.30.13/src/platform_impl/windows/mod.rs +++ b/third_party/winit-0.30.13/src/platform_impl/windows/mod.rs @@ -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, pub key_without_modifiers: Key, + pub raw: RawKeyEventInfo, } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]