From 4da4d637cd7bfeebc4c047aea565896ec869a91f Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Wed, 17 Jun 2020 09:23:12 -0700 Subject: [PATCH 1/2] Add keyboard event processing Provide an optional feature to process raw platform messages into keyboard events closely following the W3C KeyboardEvent standard. This is also something of a prototype for the similar feature in druid. --- Cargo.toml | 9 + README.md | 4 + examples/hello-win.rs | 44 ++- src/keyboard.rs | 661 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 5 + 5 files changed, 714 insertions(+), 9 deletions(-) create mode 100644 src/keyboard.rs diff --git a/Cargo.toml b/Cargo.toml index fe5983e..e99bf21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,19 @@ edition = "2018" [package.metadata.docs.rs] default-target = "x86_64-pc-windows-msvc" +all-features = true + +[features] +kb = ["keyboard-types"] [dependencies.winapi] version = "0.3.8" features = ["winuser"] +[dependencies.keyboard-types] +version = "0.5.0" +optional = true +default-features = false + [dependencies] wio = "0.2.2" diff --git a/README.md b/README.md index 7f13a93..790bc48 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,7 @@ One goal of the crate is to make it easier to reason about soundness, by providi Another goal is to provide reasonably good documentation, including detailed links to official documentation and other resources. Many of these lessons have been hard-learned, as part of the Windows backend for druid, and other experiments. The crate is "semi-opinionated" in that it nails down some details, especially the way threads work, but how you draw and the way you handle events is entirely up to you. It is a goal that anybody who creates a HWND from Rust should use this crate. If there's some reason it doesn't work for your use case, I'm curious why, so please file an issue. + +There is an optional `kb` feature, which does the rather tricky and fiddly job of converting platform keyboard messages into `KeyboardEvent` structs from the [keyboard-types] crate, based firmly on W3C specs. It's possible that more such features will be added (dpi handling is a strong possibility). + +[keyboard-types]: https://crates.io/crates/keyboard-types diff --git a/examples/hello-win.rs b/examples/hello-win.rs index 0b590c0..0a5e150 100644 --- a/examples/hello-win.rs +++ b/examples/hello-win.rs @@ -1,3 +1,5 @@ +#[allow(unused)] +use std::cell::RefCell; use std::ptr::null_mut; use winapi::shared::minwindef::{HINSTANCE, LPARAM, LRESULT, UINT, WPARAM}; @@ -5,26 +7,46 @@ use winapi::shared::windef::HWND; use winapi::um::wingdi::CreateSolidBrush; use winapi::um::winuser::{ LoadCursorW, LoadIconW, PostQuitMessage, ShowWindow, IDC_ARROW, IDI_APPLICATION, SW_SHOWNORMAL, - WM_DESTROY, WS_OVERLAPPEDWINDOW, + WM_CHAR, WM_DESTROY, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, + WM_SYSKEYUP, WS_OVERLAPPEDWINDOW, }; +#[cfg(feature = "kb")] +use win_win::KeyboardState; + use win_win::{WindowBuilder, WindowClass, WindowProc}; -struct MyWindowProc; +struct MyWindowProc { + #[cfg(feature = "kb")] + kb_state: RefCell, +} impl WindowProc for MyWindowProc { + #[allow(unused)] fn window_proc( &self, - _hwnd: HWND, + hwnd: HWND, msg: UINT, - _wparam: WPARAM, - _lparam: LPARAM, + wparam: WPARAM, + lparam: LPARAM, ) -> Option { - println!("msg {}", msg); - if msg == WM_DESTROY { - unsafe { + match msg { + WM_DESTROY => unsafe { PostQuitMessage(0); + }, + WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP | WM_CHAR | WM_SYSCHAR + | WM_INPUTLANGCHANGE => { + #[cfg(feature = "kb")] + if let Some(event) = unsafe { + self.kb_state + .borrow_mut() + .process_message(hwnd, msg, wparam, lparam) + } { + println!("event: {:?}", event); + return Some(0); + } } + _ => (), } None } @@ -41,7 +63,11 @@ fn main() { .background(brush) .build() .unwrap(); - let hwnd = WindowBuilder::new(MyWindowProc, &win_class) + let window_proc = MyWindowProc { + #[cfg(feature = "kb")] + kb_state: RefCell::new(KeyboardState::new()), + }; + let hwnd = WindowBuilder::new(window_proc, &win_class) .name("win-win example") .style(WS_OVERLAPPEDWINDOW) .build(); diff --git a/src/keyboard.rs b/src/keyboard.rs new file mode 100644 index 0000000..5ea3147 --- /dev/null +++ b/src/keyboard.rs @@ -0,0 +1,661 @@ +//! Keyboard handling + +// issues: +// * AltGr + +use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Location, Modifiers}; + +use std::collections::{HashMap, HashSet}; +use std::mem; +use std::ops::RangeInclusive; + +use winapi::shared::minwindef::{HKL, INT, LPARAM, UINT, WPARAM}; +use winapi::shared::ntdef::SHORT; +use winapi::shared::windef::HWND; +use winapi::um::winuser::{ + GetKeyState, GetKeyboardLayout, MapVirtualKeyExW, PeekMessageW, ToUnicodeEx, MAPVK_VK_TO_CHAR, + MAPVK_VSC_TO_VK_EX, PM_NOREMOVE, VK_CAPITAL, WM_CHAR, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, + WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, +}; + +use winapi::um::winuser::{ + VK_ACCEPT, VK_ADD, VK_APPS, VK_ATTN, VK_BACK, VK_BROWSER_BACK, VK_BROWSER_FAVORITES, + VK_BROWSER_FORWARD, VK_BROWSER_HOME, VK_BROWSER_REFRESH, VK_BROWSER_SEARCH, VK_BROWSER_STOP, + VK_CANCEL, VK_CLEAR, VK_CONTROL, VK_CONVERT, VK_CRSEL, VK_DECIMAL, VK_DELETE, VK_DIVIDE, + VK_DOWN, VK_END, VK_EREOF, VK_ESCAPE, VK_EXECUTE, VK_EXSEL, VK_F1, VK_F10, VK_F11, VK_F12, + VK_F2, VK_F3, VK_F4, VK_F5, VK_F6, VK_F7, VK_F8, VK_F9, VK_FINAL, VK_HELP, VK_HOME, VK_INSERT, + VK_JUNJA, VK_KANA, VK_KANJI, VK_LAUNCH_APP1, VK_LAUNCH_APP2, VK_LAUNCH_MAIL, + VK_LAUNCH_MEDIA_SELECT, VK_LCONTROL, VK_LEFT, VK_LMENU, VK_LSHIFT, VK_LWIN, + VK_MEDIA_NEXT_TRACK, VK_MEDIA_PLAY_PAUSE, VK_MEDIA_PREV_TRACK, VK_MEDIA_STOP, VK_MENU, + VK_MODECHANGE, VK_MULTIPLY, VK_NEXT, VK_NONCONVERT, VK_NUMLOCK, VK_NUMPAD0, VK_NUMPAD1, + VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, VK_NUMPAD7, VK_NUMPAD8, VK_NUMPAD9, + VK_OEM_ATTN, VK_OEM_CLEAR, VK_PAUSE, VK_PLAY, VK_PRINT, VK_PRIOR, VK_PROCESSKEY, VK_RCONTROL, + VK_RETURN, VK_RIGHT, VK_RMENU, VK_RSHIFT, VK_RWIN, VK_SCROLL, VK_SELECT, VK_SHIFT, VK_SLEEP, + VK_SNAPSHOT, VK_SUBTRACT, VK_TAB, VK_UP, VK_VOLUME_DOWN, VK_VOLUME_MUTE, VK_VOLUME_UP, VK_ZOOM, +}; + +const VK_ABNT_C2: INT = 0xc2; + +/// A (non-extended) virtual key code. +type VkCode = u8; + +// This is really bitfields. +type ShiftState = u8; +const SHIFT_STATE_SHIFT: ShiftState = 1; +const SHIFT_STATE_ALTGR: ShiftState = 2; +const N_SHIFT_STATE: ShiftState = 4; + +/// Per-window keyboard state. +pub struct KeyboardState { + hkl: HKL, + // A map from (vk, is_shifted) to string val + key_vals: HashMap<(VkCode, ShiftState), String>, + dead_keys: HashSet<(VkCode, ShiftState)>, + has_altgr: bool, + stash_vk: Option, + stash_utf16: Vec, +} + +/// Virtual key codes that are considered printable. +/// +/// This logic is borrowed from KeyboardLayout::GetKeyIndex +/// in Mozilla. +const PRINTABLE_VKS: &[RangeInclusive] = &[ + 0x20..=0x20, + 0x30..=0x39, + 0x41..=0x5A, + 0x60..=0x6B, + 0x6D..=0x6F, + 0xBA..=0xC2, + 0xDB..=0xDF, + 0xE1..=0xE4, +]; + +/// Bits of lparam indicating scan code, including extended bit. +const SCAN_MASK: LPARAM = 0x1ff_0000; + +/// Determine whether there are more messages in the queue for this key event. +/// +/// When this function returns `false`, there is another message in the queue +/// with a matching scan code, therefore it is reasonable to stash the data +/// from this message and defer til later to actually produce the event. +unsafe fn is_last_message(hwnd: HWND, msg: UINT, lparam: LPARAM) -> bool { + let expected_msg = match msg { + WM_KEYDOWN | WM_CHAR => WM_CHAR, + WM_SYSKEYDOWN | WM_SYSCHAR => WM_SYSCHAR, + _ => unreachable!(), + }; + let mut msg = mem::zeroed(); + let avail = PeekMessageW(&mut msg, hwnd, expected_msg, expected_msg, PM_NOREMOVE); + avail == 0 || msg.lParam & SCAN_MASK != lparam & SCAN_MASK +} + +const MODIFIER_MAP: &[(INT, Modifiers, SHORT)] = &[ + (VK_MENU, Modifiers::ALT, 0x80), + (VK_CAPITAL, Modifiers::CAPS_LOCK, 0x1), + (VK_CONTROL, Modifiers::CONTROL, 0x80), + (VK_NUMLOCK, Modifiers::NUM_LOCK, 0x1), + (VK_SCROLL, Modifiers::SCROLL_LOCK, 0x1), + (VK_SHIFT, Modifiers::SHIFT, 0x80), +]; + +/// Convert scan code to W3C standard code. +/// +/// It's hard to get an authoritative source for this; it's mostly based +/// on NativeKeyToDOMCodeName.h in Mozilla. +fn scan_to_code(scan_code: u32) -> Code { + use Code::*; + match scan_code { + 0x1 => Escape, + 0x2 => Digit1, + 0x3 => Digit2, + 0x4 => Digit3, + 0x5 => Digit4, + 0x6 => Digit5, + 0x7 => Digit6, + 0x8 => Digit7, + 0x9 => Digit8, + 0xA => Digit9, + 0xB => Digit0, + 0xC => Minus, + 0xD => Equal, + 0xE => Backspace, + 0xF => Tab, + 0x10 => KeyQ, + 0x11 => KeyW, + 0x12 => KeyE, + 0x13 => KeyR, + 0x14 => KeyT, + 0x15 => KeyY, + 0x16 => KeyU, + 0x17 => KeyI, + 0x18 => KeyO, + 0x19 => KeyP, + 0x1A => BracketLeft, + 0x1B => BracketRight, + 0x1C => Enter, + 0x1D => ControlLeft, + 0x1E => KeyA, + 0x1F => KeyS, + 0x20 => KeyD, + 0x21 => KeyF, + 0x22 => KeyG, + 0x23 => KeyH, + 0x24 => KeyJ, + 0x25 => KeyK, + 0x26 => KeyL, + 0x27 => Semicolon, + 0x28 => Quote, + 0x29 => Backquote, + 0x2A => ShiftLeft, + 0x2B => Backslash, + 0x2C => KeyZ, + 0x2D => KeyX, + 0x2E => KeyC, + 0x2F => KeyV, + 0x30 => KeyB, + 0x31 => KeyN, + 0x32 => KeyM, + 0x33 => Comma, + 0x34 => Period, + 0x35 => Slash, + 0x36 => ShiftRight, + 0x37 => NumpadMultiply, + 0x38 => AltLeft, + 0x39 => Space, + 0x3A => CapsLock, + 0x3B => F1, + 0x3C => F2, + 0x3D => F3, + 0x3E => F4, + 0x3F => F5, + 0x40 => F6, + 0x41 => F7, + 0x42 => F8, + 0x43 => F9, + 0x44 => F10, + 0x45 => Pause, + 0x46 => ScrollLock, + 0x47 => Numpad7, + 0x48 => Numpad8, + 0x49 => Numpad9, + 0x4A => NumpadSubtract, + 0x4B => Numpad4, + 0x4C => Numpad5, + 0x4D => Numpad6, + 0x4E => NumpadAdd, + 0x4F => Numpad1, + 0x50 => Numpad2, + 0x51 => Numpad3, + 0x52 => Numpad0, + 0x53 => NumpadDecimal, + 0x54 => PrintScreen, + 0x56 => IntlBackslash, + 0x57 => F11, + 0x58 => F12, + 0x59 => NumpadEqual, + 0x70 => KanaMode, + 0x71 => Lang2, + 0x72 => Lang1, + 0x73 => IntlRo, + 0x79 => Convert, + 0x7B => NonConvert, + 0x7D => IntlYen, + 0x7E => NumpadComma, + 0x110 => MediaTrackPrevious, + 0x119 => MediaTrackNext, + 0x11C => NumpadEnter, + 0x11D => ControlRight, + 0x120 => AudioVolumeMute, + 0x121 => LaunchApp2, + 0x122 => MediaPlayPause, + 0x124 => MediaStop, + 0x12E => AudioVolumeDown, + 0x130 => AudioVolumeUp, + 0x132 => BrowserHome, + 0x135 => NumpadDivide, + 0x137 => PrintScreen, + 0x138 => AltRight, + 0x145 => NumLock, + 0x147 => Home, + 0x148 => ArrowUp, + 0x149 => PageUp, + 0x14B => ArrowLeft, + 0x14D => ArrowRight, + 0x14F => End, + 0x150 => ArrowDown, + 0x151 => PageDown, + 0x152 => Insert, + 0x153 => Delete, + 0x15B => MetaLeft, + 0x15C => MetaRight, + 0x15D => ContextMenu, + 0x15E => Power, + 0x165 => BrowserSearch, + 0x166 => BrowserFavorites, + 0x167 => BrowserRefresh, + 0x168 => BrowserStop, + 0x169 => BrowserForward, + 0x16A => BrowserBack, + 0x16B => LaunchApp1, + 0x16C => LaunchMail, + 0x16D => MediaSelect, + 0x1F1 => Lang2, + 0x1F2 => Lang1, + _ => Unidentified, + } +} + +fn vk_to_key(vk: VkCode) -> Option { + use Key::*; + Some(match vk as INT { + VK_CANCEL => Cancel, + VK_BACK => Backspace, + VK_TAB => Tab, + VK_CLEAR => Clear, + VK_RETURN => Enter, + VK_SHIFT | VK_LSHIFT | VK_RSHIFT => Shift, + VK_CONTROL | VK_LCONTROL | VK_RCONTROL => Control, + VK_MENU | VK_LMENU | VK_RMENU => Alt, + VK_PAUSE => Pause, + VK_CAPITAL => CapsLock, + // TODO: disambiguate kana and hangul? same vk + VK_KANA => KanaMode, + VK_JUNJA => JunjaMode, + VK_FINAL => FinalMode, + VK_KANJI => KanjiMode, + VK_ESCAPE => Escape, + VK_NONCONVERT => NonConvert, + VK_ACCEPT => Accept, + VK_PRIOR => PageUp, + VK_NEXT => PageDown, + VK_END => End, + VK_HOME => Home, + VK_LEFT => ArrowLeft, + VK_UP => ArrowUp, + VK_RIGHT => ArrowRight, + VK_DOWN => ArrowDown, + VK_SELECT => Select, + VK_PRINT => Print, + VK_EXECUTE => Execute, + VK_SNAPSHOT => PrintScreen, + VK_INSERT => Insert, + VK_DELETE => Delete, + VK_HELP => Help, + VK_LWIN | VK_RWIN => Meta, + VK_APPS => ContextMenu, + VK_SLEEP => Standby, + 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_NUMLOCK => NumLock, + VK_SCROLL => ScrollLock, + VK_BROWSER_BACK => BrowserBack, + VK_BROWSER_FORWARD => BrowserForward, + VK_BROWSER_REFRESH => BrowserRefresh, + VK_BROWSER_STOP => BrowserStop, + VK_BROWSER_SEARCH => BrowserSearch, + VK_BROWSER_FAVORITES => BrowserFavorites, + VK_BROWSER_HOME => BrowserHome, + VK_VOLUME_MUTE => AudioVolumeMute, + VK_VOLUME_DOWN => AudioVolumeDown, + VK_VOLUME_UP => AudioVolumeUp, + VK_MEDIA_NEXT_TRACK => MediaTrackNext, + VK_MEDIA_PREV_TRACK => MediaTrackPrevious, + VK_MEDIA_STOP => MediaStop, + VK_MEDIA_PLAY_PAUSE => MediaPlayPause, + VK_LAUNCH_MAIL => LaunchMail, + VK_LAUNCH_MEDIA_SELECT => LaunchMediaPlayer, + VK_LAUNCH_APP1 => LaunchApplication1, + VK_LAUNCH_APP2 => LaunchApplication2, + VK_OEM_ATTN => Alphanumeric, + VK_CONVERT => Convert, + VK_MODECHANGE => ModeChange, + VK_PROCESSKEY => Process, + VK_ATTN => Attn, + VK_CRSEL => CrSel, + VK_EXSEL => ExSel, + VK_EREOF => EraseEof, + VK_PLAY => Play, + VK_ZOOM => ZoomToggle, + VK_OEM_CLEAR => Clear, + _ => return None, + }) +} + +fn code_unit_to_key(code_unit: u32) -> Key { + match code_unit { + 0x8 | 0x7F => Key::Backspace, + 0x9 => Key::Tab, + 0xA | 0xD => Key::Enter, + 0x1B => Key::Escape, + _ if code_unit >= 0x20 => { + if let Some(c) = std::char::from_u32(code_unit) { + Key::Character(c.to_string()) + } else { + // UTF-16 error, very unlikely + Key::Unidentified + } + } + _ => Key::Unidentified, + } +} + +/// Get location from virtual key code. +/// +/// This logic is based on NativeKey::GetKeyLocation from Mozilla. +fn vk_to_location(vk: VkCode, is_extended: bool) -> Location { + match vk as INT { + VK_LSHIFT | VK_LCONTROL | VK_LMENU | VK_LWIN => Location::Left, + VK_RSHIFT | VK_RCONTROL | VK_RMENU | VK_RWIN => Location::Right, + VK_RETURN if is_extended => Location::Numpad, + VK_INSERT | VK_DELETE | VK_END | VK_DOWN | VK_NEXT | VK_LEFT | VK_CLEAR | VK_RIGHT + | VK_HOME | VK_UP | VK_PRIOR => { + if is_extended { + Location::Standard + } else { + Location::Numpad + } + } + VK_NUMPAD0 | VK_NUMPAD1 | VK_NUMPAD2 | VK_NUMPAD3 | VK_NUMPAD4 | VK_NUMPAD5 + | VK_NUMPAD6 | VK_NUMPAD7 | VK_NUMPAD8 | VK_NUMPAD9 | VK_DECIMAL | VK_DIVIDE + | VK_MULTIPLY | VK_SUBTRACT | VK_ADD | VK_ABNT_C2 => Location::Numpad, + _ => Location::Standard, + } +} + +impl KeyboardState { + /// Create a new keyboard state. + /// + /// There should be one of these per window. It loads the current keyboard + /// layout and retains some mapping information from it. + pub fn new() -> KeyboardState { + unsafe { + let hkl = GetKeyboardLayout(0); + let key_vals = HashMap::new(); + let dead_keys = HashSet::new(); + let stash_vk = None; + let stash_utf16 = Vec::new(); + let has_altgr = false; + let mut result = KeyboardState { + hkl, + key_vals, + dead_keys, + has_altgr, + stash_vk, + stash_utf16, + }; + result.load_keyboard_layout(); + result + } + } + + /// Process one message from the platform. + /// + /// This is the main interface point for generating cooked keyboard events + /// from raw platform messages. It should be called for each relevant message, + /// which comprises: `WM_KEYDOWN`, `WM_KEYUP`, `WM_CHAR`, `WM_SYSKEYDOWN`, + /// `WM_SYSKEYUP`, `WM_SYSCHAR`, and `WM_INPUTLANGCHANGE`. + /// + /// As a general theory, many keyboard events generate a sequence of platform + /// messages. In these cases, we stash information from all messages but the + /// last, and generate the event from the last (using `PeekMessage` to detect + /// that case). Mozilla handling is slightly different; it actually tries to + /// do the processing on the first message, fetching the subsequent messages + /// from the queue. We believe our handling is simpler and more robust. + /// + /// # Safety + /// + /// The `hwnd` argument must be a valid `HWND`. Similarly, the `lparam` must + /// a valid `HKL` reference in the `WM_INPUTLANGCHANGE` message. Actual danger + /// is likely low, though. + pub unsafe fn process_message( + &mut self, + hwnd: HWND, + msg: UINT, + wparam: WPARAM, + lparam: LPARAM, + ) -> Option { + match msg { + WM_KEYDOWN | WM_SYSKEYDOWN => { + println!("keydown wparam {:x} lparam {:x}", wparam, lparam); + let scan_code = ((lparam & SCAN_MASK) >> 16) as u32; + let vk = self.refine_vk(wparam as u8, scan_code); + if is_last_message(hwnd, msg, lparam) { + let modifiers = self.get_modifiers(); + let code = scan_to_code(scan_code); + let key = vk_to_key(vk).unwrap_or_else(|| self.get_base_key(vk, modifiers)); + let repeat = (lparam & 0x4000_0000) != 0; + let is_extended = (lparam & 0x100_0000) != 0; + let location = vk_to_location(vk, is_extended); + let state = KeyState::Down; + let event = KeyboardEvent { + state, + modifiers, + code, + key, + is_composing: false, + location, + repeat, + }; + Some(event) + } else { + self.stash_vk = Some(vk); + None + } + } + WM_KEYUP | WM_SYSKEYUP => { + let scan_code = ((lparam & SCAN_MASK) >> 16) as u32; + let vk = self.refine_vk(wparam as u8, scan_code); + let modifiers = self.get_modifiers(); + let code = scan_to_code(scan_code); + let key = vk_to_key(vk).unwrap_or_else(|| self.get_base_key(vk, modifiers)); + let repeat = false; + let is_extended = (lparam & 0x100_0000) != 0; + let location = vk_to_location(vk, is_extended); + let state = KeyState::Up; + let event = KeyboardEvent { + state, + modifiers, + code, + key, + is_composing: false, + location, + repeat, + }; + Some(event) + } + WM_CHAR | WM_SYSCHAR => { + println!("char wparam {:x} lparam {:x}", wparam, lparam); + if is_last_message(hwnd, msg, lparam) { + let stash_vk = self.stash_vk.take(); + let modifiers = self.get_modifiers(); + let scan_code = ((lparam & SCAN_MASK) >> 16) as u32; + let vk = self.refine_vk(stash_vk.unwrap_or(0), scan_code); + let code = scan_to_code(scan_code); + let key = if self.stash_utf16.is_empty() && wparam < 0x20 { + vk_to_key(vk).unwrap_or_else(|| self.get_base_key(vk, modifiers)) + } else { + self.stash_utf16.push(wparam as u16); + if let Ok(s) = String::from_utf16(&self.stash_utf16) { + Key::Character(s) + } else { + Key::Unidentified + } + }; + self.stash_utf16.clear(); + let repeat = (lparam & 0x4000_0000) != 0; + let is_extended = (lparam & 0x100_0000) != 0; + let location = vk_to_location(vk, is_extended); + let state = KeyState::Down; + let event = KeyboardEvent { + state, + modifiers, + code, + key, + is_composing: false, + location, + repeat, + }; + Some(event) + } else { + self.stash_utf16.push(wparam as u16); + None + } + } + WM_INPUTLANGCHANGE => { + self.hkl = lparam as HKL; + self.load_keyboard_layout(); + None + } + _ => None, + } + } + + /// Get the modifier state. + /// + /// This function is designed to be called from a message handler, and + /// gives the modifier state at the time of the message (ie is the + /// synchronous variant). See [`GetKeyState`] for more context. + /// + /// The interpretation of modifiers depends on the keyboard layout, as + /// some layouts have [AltGr] and others do not. + /// + /// [`GetKeyState`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getkeystate + /// [AltGr]: https://en.wikipedia.org/wiki/AltGr_key + pub fn get_modifiers(&self) -> Modifiers { + unsafe { + let mut modifiers = Modifiers::empty(); + for &(vk, modifier, mask) in MODIFIER_MAP { + if GetKeyState(vk) & mask != 0 { + modifiers |= modifier; + } + } + if self.has_altgr && GetKeyState(VK_RMENU) & 0x80 != 0 { + modifiers |= Modifiers::ALT_GRAPH; + modifiers &= !(Modifiers::CONTROL | Modifiers::ALT); + } + modifiers + } + } + + /// Load a keyboard layout. + /// + /// We need to retain a map of virtual key codes in various modifier + /// states, because it's not practical to query that at keyboard event + /// time (the main culprit is that `ToUnicodeEx` is stateful). + /// + /// The logic is based on Mozilla KeyboardLayout::LoadLayout but is + /// considerably simplified. + fn load_keyboard_layout(&mut self) { + unsafe { + self.key_vals.clear(); + self.dead_keys.clear(); + self.has_altgr = false; + let mut key_state = [0u8; 256]; + let mut uni_chars = [0u16; 5]; + // Right now, we're only getting the values for base and shifted + // variants. Mozilla goes through 16 mod states. + for shift_state in 0..N_SHIFT_STATE { + let has_shift = shift_state & SHIFT_STATE_SHIFT != 0; + let has_altgr = shift_state & SHIFT_STATE_ALTGR != 0; + key_state[VK_SHIFT as usize] = if has_shift { 0x80 } else { 0 }; + key_state[VK_CONTROL as usize] = if has_altgr { 0x80 } else { 0 }; + key_state[VK_LCONTROL as usize] = if has_altgr { 0x80 } else { 0 }; + key_state[VK_MENU as usize] = if has_altgr { 0x80 } else { 0 }; + key_state[VK_RMENU as usize] = if has_altgr { 0x80 } else { 0 }; + for vk in PRINTABLE_VKS.iter().cloned().flatten() { + let ret = ToUnicodeEx( + vk as UINT, + 0, + key_state.as_ptr(), + uni_chars.as_mut_ptr(), + uni_chars.len() as _, + 0, + self.hkl, + ); + if ret > 0 { + let utf16_slice = &uni_chars[..ret as usize]; + if let Ok(strval) = String::from_utf16(utf16_slice) { + self.key_vals.insert((vk, shift_state), strval); + } + // If the AltGr version of the key has a different string than + // the base, then the layout has AltGr. Note that Mozilla also + // checks dead keys for change. + if has_altgr + && !self.has_altgr + && self.key_vals.get(&(vk, shift_state)) + != self.key_vals.get(&(vk, shift_state & !SHIFT_STATE_ALTGR)) + { + self.has_altgr = true; + } + } else if ret < 0 { + // It's a dead key, press it again to reset the state. + self.dead_keys.insert((vk, shift_state)); + let _ = ToUnicodeEx( + vk as UINT, + 0, + key_state.as_ptr(), + uni_chars.as_mut_ptr(), + uni_chars.len() as _, + 0, + self.hkl, + ); + } + } + } + } + } + + fn get_base_key(&self, vk: VkCode, modifiers: Modifiers) -> Key { + let mut shift_state = 0; + if modifiers.contains(Modifiers::SHIFT) { + shift_state |= SHIFT_STATE_SHIFT; + } + if modifiers.contains(Modifiers::ALT_GRAPH) { + shift_state |= SHIFT_STATE_ALTGR; + } + if let Some(s) = self.key_vals.get(&(vk, shift_state)) { + Key::Character(s.clone()) + } else { + let mapped = self.map_vk(vk); + if mapped >= (1 << 31) { + Key::Dead + } else { + code_unit_to_key(mapped) + } + } + } + + /// Map a virtual key code to a code unit, also indicate if dead key. + /// + /// Bit 31 is set if the mapping is to a dead key. The bottom bits contain the code unit. + fn map_vk(&self, vk: VkCode) -> u32 { + unsafe { MapVirtualKeyExW(vk as _, MAPVK_VK_TO_CHAR, self.hkl) } + } + + /// Refine a virtual key code to distinguish left and right. + /// + /// This only does the mapping if the original code is ambiguous, as otherwise the + /// virtual key code reported in `wparam` is more reliable. + fn refine_vk(&self, vk: VkCode, mut scan_code: u32) -> VkCode { + match vk as INT { + 0 | VK_SHIFT | VK_CONTROL | VK_MENU => { + if scan_code >= 0x100 { + scan_code += 0xE000 - 0x100; + } + unsafe { MapVirtualKeyExW(scan_code, MAPVK_VSC_TO_VK_EX, self.hkl) as u8 } + } + _ => vk, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 3777a69..3a62ea1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,14 @@ //! Window creation for Windows. mod error; +#[cfg(feature = "kb")] +mod keyboard; mod runloop; mod window; pub use error::Error; pub use runloop::runloop; pub use window::{WindowBuilder, WindowClass, WindowClassBuilder, WindowProc}; + +#[cfg(feature = "kb")] +pub use keyboard::{KeyboardState}; From 8d97ce956df4b794f289248f3b31523bd41c9924 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Mon, 29 Jun 2020 14:18:59 -0700 Subject: [PATCH 2/2] Update based on druid-shell changes This is now very similar to the implementation in druid-shell, but with the main exception that it's the upstream keyboard-types KeyboardEvent rather than a new type. --- src/keyboard.rs | 380 +++++++++++++++++++++++++++++++----------------- src/lib.rs | 2 +- 2 files changed, 249 insertions(+), 133 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 5ea3147..1a76db3 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -1,39 +1,35 @@ -//! Keyboard handling - -// issues: -// * AltGr - -use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Location, Modifiers}; +//! Key event handling. +use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; +use std::convert::TryInto; use std::mem; use std::ops::RangeInclusive; +use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Location, Modifiers}; + use winapi::shared::minwindef::{HKL, INT, LPARAM, UINT, WPARAM}; use winapi::shared::ntdef::SHORT; use winapi::shared::windef::HWND; use winapi::um::winuser::{ - GetKeyState, GetKeyboardLayout, MapVirtualKeyExW, PeekMessageW, ToUnicodeEx, MAPVK_VK_TO_CHAR, - MAPVK_VSC_TO_VK_EX, PM_NOREMOVE, VK_CAPITAL, WM_CHAR, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, + GetKeyState, GetKeyboardLayout, MapVirtualKeyExW, PeekMessageW, ToUnicodeEx, VkKeyScanW, + MAPVK_VK_TO_CHAR, MAPVK_VSC_TO_VK_EX, PM_NOREMOVE, VK_ACCEPT, VK_ADD, VK_APPS, VK_ATTN, + VK_BACK, VK_BROWSER_BACK, VK_BROWSER_FAVORITES, VK_BROWSER_FORWARD, VK_BROWSER_HOME, + VK_BROWSER_REFRESH, VK_BROWSER_SEARCH, VK_BROWSER_STOP, VK_CANCEL, VK_CAPITAL, VK_CLEAR, + VK_CONTROL, VK_CONVERT, VK_CRSEL, VK_DECIMAL, VK_DELETE, VK_DIVIDE, VK_DOWN, VK_END, VK_EREOF, + VK_ESCAPE, VK_EXECUTE, VK_EXSEL, VK_F1, VK_F10, VK_F11, VK_F12, VK_F2, VK_F3, VK_F4, VK_F5, + VK_F6, VK_F7, VK_F8, VK_F9, VK_FINAL, VK_HELP, VK_HOME, VK_INSERT, VK_JUNJA, VK_KANA, VK_KANJI, + VK_LAUNCH_APP1, VK_LAUNCH_APP2, VK_LAUNCH_MAIL, VK_LAUNCH_MEDIA_SELECT, VK_LCONTROL, VK_LEFT, + VK_LMENU, VK_LSHIFT, VK_LWIN, VK_MEDIA_NEXT_TRACK, VK_MEDIA_PLAY_PAUSE, VK_MEDIA_PREV_TRACK, + VK_MEDIA_STOP, VK_MENU, VK_MODECHANGE, VK_MULTIPLY, VK_NEXT, VK_NONCONVERT, VK_NUMLOCK, + VK_NUMPAD0, VK_NUMPAD1, VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, VK_NUMPAD7, + VK_NUMPAD8, VK_NUMPAD9, VK_OEM_ATTN, VK_OEM_CLEAR, VK_PAUSE, VK_PLAY, VK_PRINT, VK_PRIOR, + VK_PROCESSKEY, VK_RCONTROL, VK_RETURN, VK_RIGHT, VK_RMENU, VK_RSHIFT, VK_RWIN, VK_SCROLL, + VK_SELECT, VK_SHIFT, VK_SLEEP, VK_SNAPSHOT, VK_SUBTRACT, VK_TAB, VK_UP, VK_VOLUME_DOWN, + VK_VOLUME_MUTE, VK_VOLUME_UP, VK_ZOOM, WM_CHAR, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, }; -use winapi::um::winuser::{ - VK_ACCEPT, VK_ADD, VK_APPS, VK_ATTN, VK_BACK, VK_BROWSER_BACK, VK_BROWSER_FAVORITES, - VK_BROWSER_FORWARD, VK_BROWSER_HOME, VK_BROWSER_REFRESH, VK_BROWSER_SEARCH, VK_BROWSER_STOP, - VK_CANCEL, VK_CLEAR, VK_CONTROL, VK_CONVERT, VK_CRSEL, VK_DECIMAL, VK_DELETE, VK_DIVIDE, - VK_DOWN, VK_END, VK_EREOF, VK_ESCAPE, VK_EXECUTE, VK_EXSEL, VK_F1, VK_F10, VK_F11, VK_F12, - VK_F2, VK_F3, VK_F4, VK_F5, VK_F6, VK_F7, VK_F8, VK_F9, VK_FINAL, VK_HELP, VK_HOME, VK_INSERT, - VK_JUNJA, VK_KANA, VK_KANJI, VK_LAUNCH_APP1, VK_LAUNCH_APP2, VK_LAUNCH_MAIL, - VK_LAUNCH_MEDIA_SELECT, VK_LCONTROL, VK_LEFT, VK_LMENU, VK_LSHIFT, VK_LWIN, - VK_MEDIA_NEXT_TRACK, VK_MEDIA_PLAY_PAUSE, VK_MEDIA_PREV_TRACK, VK_MEDIA_STOP, VK_MENU, - VK_MODECHANGE, VK_MULTIPLY, VK_NEXT, VK_NONCONVERT, VK_NUMLOCK, VK_NUMPAD0, VK_NUMPAD1, - VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, VK_NUMPAD7, VK_NUMPAD8, VK_NUMPAD9, - VK_OEM_ATTN, VK_OEM_CLEAR, VK_PAUSE, VK_PLAY, VK_PRINT, VK_PRIOR, VK_PROCESSKEY, VK_RCONTROL, - VK_RETURN, VK_RIGHT, VK_RMENU, VK_RSHIFT, VK_RWIN, VK_SCROLL, VK_SELECT, VK_SHIFT, VK_SLEEP, - VK_SNAPSHOT, VK_SUBTRACT, VK_TAB, VK_UP, VK_VOLUME_DOWN, VK_VOLUME_MUTE, VK_VOLUME_UP, VK_ZOOM, -}; - const VK_ABNT_C2: INT = 0xc2; /// A (non-extended) virtual key code. @@ -247,87 +243,190 @@ fn scan_to_code(scan_code: u32) -> Code { } fn vk_to_key(vk: VkCode) -> Option { - use Key::*; Some(match vk as INT { - VK_CANCEL => Cancel, - VK_BACK => Backspace, - VK_TAB => Tab, - VK_CLEAR => Clear, - VK_RETURN => Enter, - VK_SHIFT | VK_LSHIFT | VK_RSHIFT => Shift, - VK_CONTROL | VK_LCONTROL | VK_RCONTROL => Control, - VK_MENU | VK_LMENU | VK_RMENU => Alt, - VK_PAUSE => Pause, - VK_CAPITAL => CapsLock, + VK_CANCEL => Key::Cancel, + VK_BACK => Key::Backspace, + VK_TAB => Key::Tab, + VK_CLEAR => Key::Clear, + VK_RETURN => Key::Enter, + VK_SHIFT | VK_LSHIFT | VK_RSHIFT => Key::Shift, + VK_CONTROL | VK_LCONTROL | VK_RCONTROL => Key::Control, + VK_MENU | VK_LMENU | VK_RMENU => Key::Alt, + VK_PAUSE => Key::Pause, + VK_CAPITAL => Key::CapsLock, + // TODO: disambiguate kana and hangul? same vk + VK_KANA => Key::KanaMode, + VK_JUNJA => Key::JunjaMode, + VK_FINAL => Key::FinalMode, + VK_KANJI => Key::KanjiMode, + VK_ESCAPE => Key::Escape, + VK_NONCONVERT => Key::NonConvert, + VK_ACCEPT => Key::Accept, + VK_PRIOR => Key::PageUp, + VK_NEXT => Key::PageDown, + VK_END => Key::End, + VK_HOME => Key::Home, + VK_LEFT => Key::ArrowLeft, + VK_UP => Key::ArrowUp, + VK_RIGHT => Key::ArrowRight, + VK_DOWN => Key::ArrowDown, + VK_SELECT => Key::Select, + VK_PRINT => Key::Print, + VK_EXECUTE => Key::Execute, + VK_SNAPSHOT => Key::PrintScreen, + VK_INSERT => Key::Insert, + VK_DELETE => Key::Delete, + VK_HELP => Key::Help, + VK_LWIN | VK_RWIN => Key::Meta, + VK_APPS => Key::ContextMenu, + VK_SLEEP => Key::Standby, + VK_F1 => Key::F1, + VK_F2 => Key::F2, + VK_F3 => Key::F3, + VK_F4 => Key::F4, + VK_F5 => Key::F5, + VK_F6 => Key::F6, + VK_F7 => Key::F7, + VK_F8 => Key::F8, + VK_F9 => Key::F9, + VK_F10 => Key::F10, + VK_F11 => Key::F11, + VK_F12 => Key::F12, + VK_NUMLOCK => Key::NumLock, + VK_SCROLL => Key::ScrollLock, + VK_BROWSER_BACK => Key::BrowserBack, + VK_BROWSER_FORWARD => Key::BrowserForward, + VK_BROWSER_REFRESH => Key::BrowserRefresh, + VK_BROWSER_STOP => Key::BrowserStop, + VK_BROWSER_SEARCH => Key::BrowserSearch, + VK_BROWSER_FAVORITES => Key::BrowserFavorites, + VK_BROWSER_HOME => Key::BrowserHome, + VK_VOLUME_MUTE => Key::AudioVolumeMute, + VK_VOLUME_DOWN => Key::AudioVolumeDown, + VK_VOLUME_UP => Key::AudioVolumeUp, + VK_MEDIA_NEXT_TRACK => Key::MediaTrackNext, + VK_MEDIA_PREV_TRACK => Key::MediaTrackPrevious, + VK_MEDIA_STOP => Key::MediaStop, + VK_MEDIA_PLAY_PAUSE => Key::MediaPlayPause, + VK_LAUNCH_MAIL => Key::LaunchMail, + VK_LAUNCH_MEDIA_SELECT => Key::LaunchMediaPlayer, + VK_LAUNCH_APP1 => Key::LaunchApplication1, + VK_LAUNCH_APP2 => Key::LaunchApplication2, + VK_OEM_ATTN => Key::Alphanumeric, + VK_CONVERT => Key::Convert, + VK_MODECHANGE => Key::ModeChange, + VK_PROCESSKEY => Key::Process, + VK_ATTN => Key::Attn, + VK_CRSEL => Key::CrSel, + VK_EXSEL => Key::ExSel, + VK_EREOF => Key::EraseEof, + VK_PLAY => Key::Play, + VK_ZOOM => Key::ZoomToggle, + VK_OEM_CLEAR => Key::Clear, + _ => return None, + }) +} + +/// Convert a key to a virtual key code. +/// +/// The virtual key code is needed in various winapi interfaces, including +/// accelerators. This provides the virtual key code in the current keyboard +/// map. +/// +/// The virtual key code can have modifiers in the higher order byte when the +/// argument is a `Character` variant. See: +/// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscanw +pub fn key_to_vk(key: &Key) -> Option { + Some(match key { + Key::Character(s) => { + if let Some(code_point) = s.chars().next() { + if let Ok(wchar) = (code_point as u32).try_into() { + unsafe { VkKeyScanW(wchar) as i32 } + } else { + return None; + } + } else { + return None; + } + } + Key::Cancel => VK_CANCEL, + Key::Backspace => VK_BACK, + Key::Tab => VK_TAB, + Key::Clear => VK_CLEAR, + Key::Enter => VK_RETURN, + Key::Shift => VK_SHIFT, + Key::Control => VK_CONTROL, + Key::Alt => VK_MENU, + Key::Pause => VK_PAUSE, + Key::CapsLock => VK_CAPITAL, // TODO: disambiguate kana and hangul? same vk - VK_KANA => KanaMode, - VK_JUNJA => JunjaMode, - VK_FINAL => FinalMode, - VK_KANJI => KanjiMode, - VK_ESCAPE => Escape, - VK_NONCONVERT => NonConvert, - VK_ACCEPT => Accept, - VK_PRIOR => PageUp, - VK_NEXT => PageDown, - VK_END => End, - VK_HOME => Home, - VK_LEFT => ArrowLeft, - VK_UP => ArrowUp, - VK_RIGHT => ArrowRight, - VK_DOWN => ArrowDown, - VK_SELECT => Select, - VK_PRINT => Print, - VK_EXECUTE => Execute, - VK_SNAPSHOT => PrintScreen, - VK_INSERT => Insert, - VK_DELETE => Delete, - VK_HELP => Help, - VK_LWIN | VK_RWIN => Meta, - VK_APPS => ContextMenu, - VK_SLEEP => Standby, - 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_NUMLOCK => NumLock, - VK_SCROLL => ScrollLock, - VK_BROWSER_BACK => BrowserBack, - VK_BROWSER_FORWARD => BrowserForward, - VK_BROWSER_REFRESH => BrowserRefresh, - VK_BROWSER_STOP => BrowserStop, - VK_BROWSER_SEARCH => BrowserSearch, - VK_BROWSER_FAVORITES => BrowserFavorites, - VK_BROWSER_HOME => BrowserHome, - VK_VOLUME_MUTE => AudioVolumeMute, - VK_VOLUME_DOWN => AudioVolumeDown, - VK_VOLUME_UP => AudioVolumeUp, - VK_MEDIA_NEXT_TRACK => MediaTrackNext, - VK_MEDIA_PREV_TRACK => MediaTrackPrevious, - VK_MEDIA_STOP => MediaStop, - VK_MEDIA_PLAY_PAUSE => MediaPlayPause, - VK_LAUNCH_MAIL => LaunchMail, - VK_LAUNCH_MEDIA_SELECT => LaunchMediaPlayer, - VK_LAUNCH_APP1 => LaunchApplication1, - VK_LAUNCH_APP2 => LaunchApplication2, - VK_OEM_ATTN => Alphanumeric, - VK_CONVERT => Convert, - VK_MODECHANGE => ModeChange, - VK_PROCESSKEY => Process, - VK_ATTN => Attn, - VK_CRSEL => CrSel, - VK_EXSEL => ExSel, - VK_EREOF => EraseEof, - VK_PLAY => Play, - VK_ZOOM => ZoomToggle, - VK_OEM_CLEAR => Clear, + Key::KanaMode => VK_KANA, + Key::JunjaMode => VK_JUNJA, + Key::FinalMode => VK_FINAL, + Key::KanjiMode => VK_KANJI, + Key::Escape => VK_ESCAPE, + Key::NonConvert => VK_NONCONVERT, + Key::Accept => VK_ACCEPT, + Key::PageUp => VK_PRIOR, + Key::PageDown => VK_NEXT, + Key::End => VK_END, + Key::Home => VK_HOME, + Key::ArrowLeft => VK_LEFT, + Key::ArrowUp => VK_UP, + Key::ArrowRight => VK_RIGHT, + Key::ArrowDown => VK_DOWN, + Key::Select => VK_SELECT, + Key::Print => VK_PRINT, + Key::Execute => VK_EXECUTE, + Key::PrintScreen => VK_SNAPSHOT, + Key::Insert => VK_INSERT, + Key::Delete => VK_DELETE, + Key::Help => VK_HELP, + Key::Meta => VK_LWIN, + Key::ContextMenu => VK_APPS, + Key::Standby => VK_SLEEP, + Key::F1 => VK_F1, + Key::F2 => VK_F2, + Key::F3 => VK_F3, + Key::F4 => VK_F4, + Key::F5 => VK_F5, + Key::F6 => VK_F6, + Key::F7 => VK_F7, + Key::F8 => VK_F8, + Key::F9 => VK_F9, + Key::F10 => VK_F10, + Key::F11 => VK_F11, + Key::F12 => VK_F12, + Key::NumLock => VK_NUMLOCK, + Key::ScrollLock => VK_SCROLL, + Key::BrowserBack => VK_BROWSER_BACK, + Key::BrowserForward => VK_BROWSER_FORWARD, + Key::BrowserRefresh => VK_BROWSER_REFRESH, + Key::BrowserStop => VK_BROWSER_STOP, + Key::BrowserSearch => VK_BROWSER_SEARCH, + Key::BrowserFavorites => VK_BROWSER_FAVORITES, + Key::BrowserHome => VK_BROWSER_HOME, + Key::AudioVolumeMute => VK_VOLUME_MUTE, + Key::AudioVolumeDown => VK_VOLUME_DOWN, + Key::AudioVolumeUp => VK_VOLUME_UP, + Key::MediaTrackNext => VK_MEDIA_NEXT_TRACK, + Key::MediaTrackPrevious => VK_MEDIA_PREV_TRACK, + Key::MediaStop => VK_MEDIA_STOP, + Key::MediaPlayPause => VK_MEDIA_PLAY_PAUSE, + Key::LaunchMail => VK_LAUNCH_MAIL, + Key::LaunchMediaPlayer => VK_LAUNCH_MEDIA_SELECT, + Key::LaunchApplication1 => VK_LAUNCH_APP1, + Key::LaunchApplication2 => VK_LAUNCH_APP2, + Key::Alphanumeric => VK_OEM_ATTN, + Key::Convert => VK_CONVERT, + Key::ModeChange => VK_MODECHANGE, + Key::Process => VK_PROCESSKEY, + Key::Attn => VK_ATTN, + Key::CrSel => VK_CRSEL, + Key::ExSel => VK_EXSEL, + Key::EraseEof => VK_EREOF, + Key::Play => VK_PLAY, + Key::ZoomToggle => VK_ZOOM, _ => return None, }) } @@ -413,9 +512,22 @@ impl KeyboardState { /// do the processing on the first message, fetching the subsequent messages /// from the queue. We believe our handling is simpler and more robust. /// + /// A simple example of a multi-message sequence is the key "=". In a US layout, + /// we'd expect `WM_KEYDOWN` with `wparam = VK_OEM_PLUS` and lparam encoding the + /// keycode that translates into `Code::Equal`, followed by a `WM_CHAR` with + /// `wparam = b"="` and the same scancode. + /// + /// A more complex example of a multi-message sequence is the second press of + /// that key in a German layout, where it's mapped to the dead key for accent + /// acute. Then we expect `WM_KEYDOWN` with `wparam = VK_OEM_6` followed by + /// two `WM_CHAR` with `wparam = 0xB4` (corresponding to U+00B4 = acute accent). + /// In this case, the result (produced on the final message in the sequence) is + /// a key event with `key = Key::Character("ยดยด")`, which also matches browser + /// behavior. + /// /// # Safety /// - /// The `hwnd` argument must be a valid `HWND`. Similarly, the `lparam` must + /// The `hwnd` argument must be a valid `HWND`. Similarly, the `lparam` must be /// a valid `HKL` reference in the `WM_INPUTLANGCHANGE` message. Actual danger /// is likely low, though. pub unsafe fn process_message( @@ -427,7 +539,7 @@ impl KeyboardState { ) -> Option { match msg { WM_KEYDOWN | WM_SYSKEYDOWN => { - println!("keydown wparam {:x} lparam {:x}", wparam, lparam); + //println!("keydown wparam {:x} lparam {:x}", wparam, lparam); let scan_code = ((lparam & SCAN_MASK) >> 16) as u32; let vk = self.refine_vk(wparam as u8, scan_code); if is_last_message(hwnd, msg, lparam) { @@ -475,7 +587,7 @@ impl KeyboardState { Some(event) } WM_CHAR | WM_SYSCHAR => { - println!("char wparam {:x} lparam {:x}", wparam, lparam); + //println!("char wparam {:x} lparam {:x}", wparam, lparam); if is_last_message(hwnd, msg, lparam) { let stash_vk = self.stash_vk.take(); let modifiers = self.get_modifiers(); @@ -540,7 +652,7 @@ impl KeyboardState { modifiers |= modifier; } } - if self.has_altgr && GetKeyState(VK_RMENU) & 0x80 != 0 { + if self.has_altgr && GetKeyState(VK_RMENU) & 0x80 != 0 { modifiers |= Modifiers::ALT_GRAPH; modifiers &= !(Modifiers::CONTROL | Modifiers::ALT); } @@ -583,33 +695,37 @@ impl KeyboardState { 0, self.hkl, ); - if ret > 0 { - let utf16_slice = &uni_chars[..ret as usize]; - if let Ok(strval) = String::from_utf16(utf16_slice) { - self.key_vals.insert((vk, shift_state), strval); + match ret.cmp(&0) { + Ordering::Greater => { + let utf16_slice = &uni_chars[..ret as usize]; + if let Ok(strval) = String::from_utf16(utf16_slice) { + self.key_vals.insert((vk, shift_state), strval); + } + // If the AltGr version of the key has a different string than + // the base, then the layout has AltGr. Note that Mozilla also + // checks dead keys for change. + if has_altgr + && !self.has_altgr + && self.key_vals.get(&(vk, shift_state)) + != self.key_vals.get(&(vk, shift_state & !SHIFT_STATE_ALTGR)) + { + self.has_altgr = true; + } } - // If the AltGr version of the key has a different string than - // the base, then the layout has AltGr. Note that Mozilla also - // checks dead keys for change. - if has_altgr - && !self.has_altgr - && self.key_vals.get(&(vk, shift_state)) - != self.key_vals.get(&(vk, shift_state & !SHIFT_STATE_ALTGR)) - { - self.has_altgr = true; + Ordering::Less => { + // It's a dead key, press it again to reset the state. + self.dead_keys.insert((vk, shift_state)); + let _ = ToUnicodeEx( + vk as UINT, + 0, + key_state.as_ptr(), + uni_chars.as_mut_ptr(), + uni_chars.len() as _, + 0, + self.hkl, + ); } - } else if ret < 0 { - // It's a dead key, press it again to reset the state. - self.dead_keys.insert((vk, shift_state)); - let _ = ToUnicodeEx( - vk as UINT, - 0, - key_state.as_ptr(), - uni_chars.as_mut_ptr(), - uni_chars.len() as _, - 0, - self.hkl, - ); + _ => (), } } } diff --git a/src/lib.rs b/src/lib.rs index 3a62ea1..d51149e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,4 +11,4 @@ pub use runloop::runloop; pub use window::{WindowBuilder, WindowClass, WindowClassBuilder, WindowProc}; #[cfg(feature = "kb")] -pub use keyboard::{KeyboardState}; +pub use keyboard::{key_to_vk, KeyboardState};