diff --git a/examples/kbd_ev_print.rs b/examples/kbd_ev_print.rs new file mode 100644 index 0000000000..f1b4727016 --- /dev/null +++ b/examples/kbd_ev_print.rs @@ -0,0 +1,322 @@ +//! Simple winit window example that prints keyboard events: +//! [KeyboardInput](https://docs.rs/winit/latest/winit/event/enum.WindowEvent.html#variant.KeyboardInput) +//! [ModifiersChanged](https://docs.rs/winit/latest/winit/event/enum.WindowEvent.html#variant.ModifiersChanged).) + +use std::error::Error; + +use winit::application::ApplicationHandler; +use winit::event::WindowEvent; +use winit::event_loop::{ActiveEventLoop, EventLoop}; +#[cfg(web_platform)] +use winit::platform::web::WindowAttributesWeb; +use winit::window::{Window, WindowAttributes, WindowId}; + +#[path = "util/fill.rs"] +mod fill; +#[path = "util/tracing.rs"] +mod tracing; + +#[derive(Default, Debug)] +struct App { + window: Option>, +} + +use winit::event::{KeyEvent, Modifiers}; +// struct Modifiers +// state : ModifiersState, +// pressed_mods: ModifiersKeys , +// https://docs.rs/winit/latest/winit/keyboard/struct.ModifiersState.html +pub fn mod_state_side_agnostic_s(state: &ModifiersState) -> String { + let mut s = String::new(); + if state.contains(ModifiersState::SHIFT) { + s.push_str(" ⇧ ") + } else { + s.push_str(" ") + }; + if state.contains(ModifiersState::CONTROL) { + s.push_str(" ⎈ ") + } else { + s.push_str(" ") + }; + if state.contains(ModifiersState::META) { + s.push_str(" ◆ ") + } else { + s.push_str(" ") + }; + if state.contains(ModifiersState::ALT) { + s.push_str(" ⎇ ") + } else { + s.push_str(" ") + }; + if state.contains(ModifiersState::ALT_GRAPH) { + s.push_str("⎇Gr") + } else { + s.push_str(" ") + }; + s.push(' '); + if state.contains(ModifiersState::CAPS_LOCK) { + s.push('⇪') + } else { + s.push(' ') + }; + s.push(' '); + if state.contains(ModifiersState::NUM_LOCK) { + s.push('⇭') //🔢 + } else { + s.push(' ') + }; + s.push(' '); + if state.contains(ModifiersState::SCROLL_LOCK) { + s.push_str("⇳🔒") + } else { + s.push_str(" ") + }; + s.push(' '); + + if state.contains(ModifiersState::FN) { + s.push('ƒ') + } else { + s.push(' ') + }; + s.push(' '); + if state.contains(ModifiersState::FN_LOCK) { + s.push_str("ƒ🔒") + } else { + s.push_str(" ") + }; + s.push(' '); + if state.contains(ModifiersState::KANA_LOCK) { + s.push_str("カナ🔒") + } else { + s.push_str(" ") + }; + s.push(' '); + if state.contains(ModifiersState::LOYA) { + s.push_str("‹👍") + } else { + s.push_str(" ") + }; + s.push(' '); + if state.contains(ModifiersState::ROYA) { + s.push_str("👍›") + } else { + s.push_str(" ") + }; + s.push(' '); + if state.contains(ModifiersState::SYMBOL) { + s.push('🔣') + } else { + s.push(' ') + }; + s.push(' '); + if state.contains(ModifiersState::SYMBOL_LOCK) { + s.push_str("🔣🔒") + } else { + s.push_str(" ") + }; + s.push(' '); + s +} +// https://docs.rs/winit/latest/winit/event/struct.Modifiers.html +pub fn mod_state_side_aware_s(mods: &Modifiers) -> String { + let mut s = String::new(); + if let ModifiersKeyState::Pressed = mods.lshift_state() { + s.push_str("‹⇧"); + if let ModifiersKeyState::Pressed = mods.rshift_state() { + s.push('›') + } else { + s.push(' ') + }; + } else if let ModifiersKeyState::Pressed = mods.rshift_state() { + s.push_str(" ⇧›") + } else { + s.push_str(" ") + } + if let ModifiersKeyState::Pressed = mods.lcontrol_state() { + s.push_str("‹⎈"); + if let ModifiersKeyState::Pressed = mods.rcontrol_state() { + s.push('›') + } else { + s.push(' ') + }; + } else if let ModifiersKeyState::Pressed = mods.rcontrol_state() { + s.push_str(" ⎈›") + } else { + s.push_str(" ") + } + if let ModifiersKeyState::Pressed = mods.lsuper_state() { + s.push_str("‹◆"); + if let ModifiersKeyState::Pressed = mods.rsuper_state() { + s.push('›') + } else { + s.push(' ') + }; + } else if let ModifiersKeyState::Pressed = mods.rsuper_state() { + s.push_str(" ◆›") + } else { + s.push_str(" ") + } + if let ModifiersKeyState::Pressed = mods.lalt_state() { + s.push_str("‹⎇"); + if let ModifiersKeyState::Pressed = mods.ralt_state() { + s.push('›') + } else { + s.push(' ') + }; + } else if let ModifiersKeyState::Pressed = mods.ralt_state() { + s.push_str(" ⎇›") + } else { + s.push_str(" ") + } + s.push_str(" "); + s +} +// pub struct KeyEvent +// physical_key: PhysicalKey, enum PhysicalKey +// Code ( KeyCode) +// �Unidentified(NativeKeyCode) +// logical_key: Key, enum Key +// Named(NamedKey) +// Character(Str) +// �Unidentified(NativeKey) +// 🕱Dead(Option) +// text : Option +// location: KeyLocation, enum KeyLocation Standard,Left,Right,Numpad +// state : ElementState, pressed/released +//🔁repeat : bool +use winit::event::ElementState; +use winit::keyboard::{Key, KeyLocation, ModifiersKeyState, ModifiersState, PhysicalKey}; +pub fn ev_key_s(key: &KeyEvent) -> String { + let mut s = String::new(); + match &key.state { + ElementState::Pressed => s.push('↓'), + ElementState::Released => s.push('↑'), + } + if key.repeat { + s.push('🔁') + } else { + s.push(' ') + }; //𜱣⚛ + s.push(' '); + match &key.physical_key { + PhysicalKey::Code(key_code) => s.push_str(&format!("{:?}", key_code)), + PhysicalKey::Unidentified(key_code_native) => { + s.push_str(&format!("�{:?}", key_code_native)) + }, + }; + s.push(' '); + match &key.logical_key { + Key::Named(key_named) => s.push_str(&format!("{:?}", key_named)), + Key::Character(key_char) => s.push_str(&format!("{}", key_char)), + Key::Unidentified(key_native) => s.push_str(&format!("�{:?}", key_native)), + Key::Dead(maybe_char) => s.push_str(&format!("🕱{:?}", maybe_char)), + }; + s.push_str(" "); + if let Some(txt) = &key.text { + s.push_str(&format!("{}", txt)); + } else { + s.push(' '); + } + s.push(' '); + if let Some(txt) = &key.text_with_all_modifiers { + s.push_str(&format!("{}", txt)); + } else { + s.push(' '); + } + s.push(' '); + match &key.key_without_modifiers { + Key::Named(key_named) => s.push_str(&format!("{:?}", key_named)), + Key::Character(key_char) => s.push_str(&format!("{}", key_char)), + Key::Unidentified(key_native) => s.push_str(&format!("�{:?}", key_native)), + Key::Dead(maybe_char) => s.push_str(&format!("🕱{:?}", maybe_char)), + }; + s.push_str(" "); + match &key.location { + KeyLocation::Standard => s.push('≝'), + KeyLocation::Left => s.push('←'), + KeyLocation::Right => s.push('→'), + KeyLocation::Numpad => s.push('🔢'), + } + s +} + +impl ApplicationHandler for App { + fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { + #[cfg(not(web_platform))] + let window_attributes = WindowAttributes::default(); + #[cfg(web_platform)] + let window_attributes = WindowAttributes::default() + .with_platform_attributes(Box::new(WindowAttributesWeb::default().with_append(true))); + self.window = match event_loop.create_window(window_attributes) { + Ok(window) => Some(window), + Err(err) => { + eprintln!("error creating window: {err}"); + event_loop.exit(); + return; + }, + } + } + + fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _: WindowId, event: WindowEvent) { + match event { + WindowEvent::ModifiersChanged(mods) => { + let state = mods.state(); + let state_s = mod_state_side_agnostic_s(&state); + let pressed_mods_s = mod_state_side_aware_s(&mods); + println!("Δ {}\tside-agnostic (mostly)\n {}\tside-aware", state_s, pressed_mods_s); + }, + WindowEvent::KeyboardInput { event, is_synthetic, .. } => { + let is_synthetic_s = if is_synthetic { "⚗" } else { " " }; + let key_event_s = ev_key_s(&event); + println!("🖮 {}{}", is_synthetic_s, key_event_s); + }, + WindowEvent::CloseRequested => { + event_loop.exit(); + }, + WindowEvent::SurfaceResized(_) => { + self.window.as_ref().expect("resize event without a window").request_redraw(); + }, + WindowEvent::RedrawRequested => { + // Redraw the application. + // + // It's preferable for applications that do not render continuously to render in + // this event rather than in AboutToWait, since rendering in here allows + // the program to gracefully handle redraws requested by the OS. + + let window = self.window.as_ref().expect("redraw request without a window"); + + // Notify that you're about to draw. + window.pre_present_notify(); + + // Draw. + fill::fill_window(window.as_ref()); + + // For contiguous redraw loop you can request a redraw from here. + // window.request_redraw(); + }, + _ => (), + } + } +} + +fn main() -> Result<(), Box> { + #[cfg(web_platform)] + console_error_panic_hook::set_once(); + + tracing::init(); + + let event_loop = EventLoop::new()?; + + println!( + "Δ is ModifiersChanged event, showing (line #1) side-agnostic modifier state as well as \ + (#2) side-aware one.\n ⇧ Shift ⎈ Control ◆ Meta ⎇ Alt ⎇Gr AltGraph ⇪ CapsLock ⇭ \ + NumLock ⇳🔒 ScrollLock\n ƒ Fn ƒ🔒 FnLock カナ🔒 KanaLock ‹👍 Loya 👍› Roya 🔣 \ + Symbol 🔣🔒 SymbolLock\n🖮 is KeyboardInput: ⚗ synthetic, ↓↑ pressed/unknown, 🔁 \ + repeat\n phys logic txt +mod −mod location" + ); + + // For alternative loop run options see `pump_events` and `run_on_demand` examples. + event_loop.run_app(App::default())?; + + Ok(()) +} diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 67ea934912..d169889702 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -81,6 +81,10 @@ changelog entry. - `keyboard::ModifiersKey` to track which modifier is exactly pressed. - `ActivationToken::as_raw` to get a ref to raw token. - Each platform now has corresponding `WindowAttributes` struct instead of trait extension. +- Added support for using AltGr, CapsLock,NumLock, ScrollLock, Fn, FnLock, KanaLock, Loya, Roya, Symbol, SymbolLock as separate modifiers. +- On Windows, update side-aware `event::Modifiers` information on state change. +- On Windows, added AltGr as a separate modifier (though currently AltGr+LCtrl can't be differentiated from just AltGr). +- On Windows, added CapsLock,NumLock, ScrollLock, KanaLock, Loya, Roya as separate modifiers. ### Changed diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 6d0648174c..a4d6e88020 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -934,17 +934,24 @@ fn update_modifiers(window: HWND, userdata: &WindowData) { let modifiers = { let mut layouts = LAYOUT_CACHE.lock().unwrap(); - layouts.get_agnostic_mods() + layouts.get_mods() }; + let mut send_event = false; let mut window_state = userdata.window_state.lock().unwrap(); - if window_state.modifiers_state != modifiers { - window_state.modifiers_state = modifiers; - + if window_state.modifiers_keys != modifiers.pressed_mods() { + window_state.modifiers_keys = modifiers.pressed_mods(); + send_event = true; + } + if window_state.modifiers_state != modifiers.state() { + window_state.modifiers_state = modifiers.state(); + send_event = true; + } + if send_event { // Drop lock drop(window_state); - userdata.send_window_event(window, ModifiersChanged(modifiers.into())); + userdata.send_window_event(window, ModifiersChanged(modifiers)); } } @@ -1040,6 +1047,9 @@ unsafe fn public_window_callback_inner( let mut result = ProcResult::DefWindowProc(wparam); // Send new modifiers before sending key events. + // NOTE: Some modifier key presses are not reported as KeyDown/Up events when the same + // alternative side modifier is being held, e.g., in a sequence of ↓LShift ↓RShift ↑RShift the + // last event is not reported. let mods_changed_callback = || match msg { WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP => { update_modifiers(window, userdata); diff --git a/src/platform_impl/windows/keyboard_layout.rs b/src/platform_impl/windows/keyboard_layout.rs index a31362f386..cc7009a695 100644 --- a/src/platform_impl/windows/keyboard_layout.rs +++ b/src/platform_impl/windows/keyboard_layout.rs @@ -40,7 +40,10 @@ use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ VK_SCROLL, VK_SELECT, VK_SEPARATOR, VK_SHIFT, VK_SLEEP, VK_SNAPSHOT, VK_SPACE, VK_SUBTRACT, VK_TAB, VK_UP, VK_VOLUME_DOWN, VK_VOLUME_MUTE, VK_VOLUME_UP, VK_XBUTTON1, VK_XBUTTON2, VK_ZOOM, }; -use winit_core::keyboard::{Key, KeyCode, ModifiersState, NamedKey, NativeKey, PhysicalKey}; +use winit_core::event::Modifiers; +use winit_core::keyboard::{ + Key, KeyCode, ModifiersKeys, ModifiersState, NamedKey, NativeKey, PhysicalKey, +}; use crate::platform_impl::{loword, primarylangid, scancode_to_physicalkey}; @@ -50,6 +53,11 @@ pub(crate) static LAYOUT_CACHE: LazyLock> = fn key_pressed(vkey: VIRTUAL_KEY) -> bool { unsafe { (GetKeyState(vkey as i32) & (1 << 15)) == (1 << 15) } } +fn key_toggled(vkey: VIRTUAL_KEY) -> bool { + // learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getkeystate + // If the low-order bit is 1, the key is toggled. GetKeyState is SHORT = 16bit + unsafe { (GetKeyState(vkey as i32) & (1 << 0)) == (1 << 0) } +} const NUMPAD_VKEYS: [VIRTUAL_KEY; 16] = [ VK_NUMPAD0, @@ -271,15 +279,62 @@ impl LayoutCache { } } - pub fn get_agnostic_mods(&mut self) -> ModifiersState { + pub fn get_mods(&mut self) -> Modifiers { let (_, layout) = self.get_current_layout(); - let filter_out_altgr = layout.has_alt_graph && key_pressed(VK_RMENU); - let mut mods = ModifiersState::empty(); - mods.set(ModifiersState::SHIFT, key_pressed(VK_SHIFT)); - mods.set(ModifiersState::CONTROL, key_pressed(VK_CONTROL) && !filter_out_altgr); - mods.set(ModifiersState::ALT, key_pressed(VK_MENU) && !filter_out_altgr); - mods.set(ModifiersState::META, key_pressed(VK_LWIN) || key_pressed(VK_RWIN)); - mods + let mut state = ModifiersState::empty(); + let mut pressed_mods = ModifiersKeys::empty(); + + state.set(ModifiersState::SHIFT, key_pressed(VK_SHIFT)); + pressed_mods.set( + ModifiersKeys::LSHIFT, + state.contains(ModifiersState::SHIFT) && key_pressed(VK_LSHIFT), + ); + pressed_mods.set( + ModifiersKeys::RSHIFT, + state.contains(ModifiersState::SHIFT) && key_pressed(VK_RSHIFT), + ); + + pressed_mods.set(ModifiersKeys::LALT, key_pressed(VK_LMENU)); + let is_ralt = key_pressed(VK_RMENU); + let is_altgr = layout.has_alt_graph && is_ralt; + pressed_mods.set(ModifiersKeys::RALT, is_ralt && !is_altgr); + state.set( + ModifiersState::ALT, + pressed_mods.contains(ModifiersKeys::LALT) + || pressed_mods.contains(ModifiersKeys::RALT), + ); + state.set(ModifiersState::ALT_GRAPH, is_altgr); + + // On Windows AltGr = RAlt + LCtrl, and OS sends artificial LCtrl key event, which needs to + // be filtered out without touching "real" LCtrl events to allow separate bindings of + // LCtrl+AltGr+X and AltGr+X. TODO: this is likely only possible by tracking the + // physical LCtrl state via raw keyboard events as the message loop isn't capable of + // excluding artificial LCtrl events? + pressed_mods.set(ModifiersKeys::RCONTROL, key_pressed(VK_RCONTROL)); + pressed_mods.set(ModifiersKeys::LCONTROL, key_pressed(VK_LCONTROL) && !is_altgr); + state.set( + ModifiersState::CONTROL, + pressed_mods.contains(ModifiersKeys::LCONTROL) + || pressed_mods.contains(ModifiersKeys::RCONTROL), + ); + + pressed_mods.set(ModifiersKeys::LMETA, key_pressed(VK_LWIN)); + pressed_mods.set(ModifiersKeys::RMETA, key_pressed(VK_RWIN)); + state.set( + ModifiersState::META, + pressed_mods.contains(ModifiersKeys::LMETA) + || pressed_mods.contains(ModifiersKeys::RMETA), + ); + + state.set(ModifiersState::CAPS_LOCK, key_toggled(VK_CAPITAL)); + state.set(ModifiersState::NUM_LOCK, key_toggled(VK_NUMLOCK)); + state.set(ModifiersState::SCROLL_LOCK, key_toggled(VK_SCROLL)); + + state.set(ModifiersState::KANA_LOCK, key_toggled(VK_KANA)); + state.set(ModifiersState::LOYA, key_pressed(VK_OEM_FJ_LOYA)); + state.set(ModifiersState::ROYA, key_pressed(VK_OEM_FJ_ROYA)); + + Modifiers::new(state, pressed_mods) } fn prepare_layout(locale_id: u64) -> Layout { diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index a0ca3a539e..52ad4f8c59 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -17,7 +17,7 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ WS_MINIMIZEBOX, WS_OVERLAPPEDWINDOW, WS_POPUP, WS_SIZEBOX, WS_SYSMENU, WS_VISIBLE, }; use winit_core::icon::Icon; -use winit_core::keyboard::ModifiersState; +use winit_core::keyboard::{ModifiersKeys, ModifiersState}; use winit_core::monitor::Fullscreen; use winit_core::window::{Theme, WindowAttributes}; @@ -41,6 +41,7 @@ pub(crate) struct WindowState { pub scale_factor: f64, pub modifiers_state: ModifiersState, + pub modifiers_keys: ModifiersKeys, pub fullscreen: Option, pub current_theme: Theme, pub preferred_theme: Option, @@ -172,6 +173,7 @@ impl WindowState { scale_factor, modifiers_state: ModifiersState::default(), + modifiers_keys: ModifiersKeys::default(), fullscreen: None, current_theme, preferred_theme, diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index 416723749c..68de2f63bb 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -825,6 +825,11 @@ impl Modifiers { self.state } + /// The logical state of the modifier keys. + pub fn pressed_mods(&self) -> ModifiersKeys { + self.pressed_mods + } + /// The logical state of the left shift key. pub fn lshift_state(&self) -> ModifiersKeyState { self.mod_state(ModifiersKeys::LSHIFT) diff --git a/winit-core/src/keyboard.rs b/winit-core/src/keyboard.rs index 156a32d4ad..d51d245a41 100644 --- a/winit-core/src/keyboard.rs +++ b/winit-core/src/keyboard.rs @@ -1705,6 +1705,29 @@ bitflags! { const ALT = 0b100 << 6; /// This is the "windows" key on PC and "command" key on Mac. const META = 0b100 << 9; + /// The "AltGraph" key, usually used to insert symbols. + const ALT_GRAPH = 0b100 << 10; + /// The "Caps Lock" key. + const CAPS_LOCK = 0b100 << 11; + /// The "Num Lock" key. + const NUM_LOCK = 0b100 << 12; + /// The "Scroll Lock" key. + const SCROLL_LOCK = 0b100 << 13; + /// The "Function" switch key. Often handled directly in the keyboard hardware and does not generate key events. + /// - **macOS**: Generates `ModifiersChanged` events on Apple hardware. + const FN = 0b100 << 14; + /// The "Function-Lock" key. + const FN_LOCK = 0b100 << 15; + /// The "Kana Mode" ("Kana Lock") key, typically used to enter hiragana mode (typically from romaji mode). + const KANA_LOCK = 0b100 << 16; + /// The "Left OYAYUBI" key (OEM-specific). + const LOYA = 0b100 << 17; + /// The "Right OYAYUBI" key (OEM-specific). + const ROYA = 0b100 << 18; + /// The "Symbol" modifier key used on some virtual keyboards. + const SYMBOL = 0b100 << 19; + /// The "Symbol" Lock key. + const SYMBOL_LOCK = 0b100 << 20; #[deprecated = "use META instead"] const SUPER = Self::META.bits(); } @@ -1730,6 +1753,61 @@ impl ModifiersState { pub fn meta_key(&self) -> bool { self.intersects(Self::META) } + + /// Returns `true` if the AltGraph modifier is active. + pub fn alt_graph_key(&self) -> bool { + self.contains(Self::ALT_GRAPH) + } + + /// Returns `true` if the CapsLock modifier is active. + pub fn caps_lock_key(&self) -> bool { + self.contains(Self::CAPS_LOCK) + } + + /// Returns `true` if the NumLock modifier is active. + pub fn num_lock_key(&self) -> bool { + self.contains(Self::NUM_LOCK) + } + + /// Returns `true` if the ScrollLock modifier is active. + pub fn scroll_lock_key(&self) -> bool { + self.contains(Self::SCROLL_LOCK) + } + + /// Returns `true` if the Fn modifier is active. + pub fn fn_key(&self) -> bool { + self.contains(Self::FN) + } + + /// Returns `true` if the FnLock modifier is active. + pub fn fn_lock_key(&self) -> bool { + self.contains(Self::FN_LOCK) + } + + /// Returns `true` if the KanaLock modifier is active. + pub fn kana_lock_key(&self) -> bool { + self.contains(Self::KANA_LOCK) + } + + /// Returns `true` if the Loya modifier is active. + pub fn loya_key(&self) -> bool { + self.contains(Self::LOYA) + } + + /// Returns `true` if the Roya modifier is active. + pub fn roya_key(&self) -> bool { + self.contains(Self::ROYA) + } + + /// Returns `true` if the Symbol modifier is active. + pub fn symbol_key(&self) -> bool { + self.contains(Self::SYMBOL) + } + + /// Returns `true` if the SymbolLock modifier is active. + pub fn symbol_lock_key(&self) -> bool { + self.contains(Self::SYMBOL_LOCK) + } } /// The logical state of the particular modifiers key.