diff --git a/.changes/keyboard-event.md b/.changes/keyboard-event.md new file mode 100644 index 000000000..9fcd4811f --- /dev/null +++ b/.changes/keyboard-event.md @@ -0,0 +1,15 @@ +--- +"tao": minor +--- + +**Breaking change**: New keyboard API, including `Accelerator` and `GlobalShortcut`. + +`WindowEvent::ModifiersChanged` is emitted when a new keyboard modifier is pressed. This is your responsibility to keep a local state. When the modifier is released, `ModifiersState::empty()` is emitted. + +`WindowEvent::KeyboardInput` as been refactored and is exposing the event `KeyEvent`. + +All menus (`ContextMenu` and `MenuBar`), now includes `Accelerator` support on Windows, macOS and Linux. + +New modules available: `keyboard`, `accelerator` and `platform::global_shortcut`. + +_Please refer to the docs and examples for more details._ diff --git a/Cargo.toml b/Cargo.toml index 25e6f0770..bc8086b3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,17 +61,21 @@ core-graphics = "0.22" dispatch = "0.2" scopeguard = "1.1" - [target."cfg(target_os = \"macos\")".dependencies.core-video-sys] - version = "0.1" - default_features = false - features = [ "display_link" ] +[target."cfg(target_os = \"macos\")".dependencies.core-video-sys] +version = "0.1" +default_features = false +features = [ "display_link" ] + +[target."cfg(target_os = \"macos\")".build-dependencies] +cc = "1" [target."cfg(target_os = \"windows\")".dependencies] parking_lot = "0.11" +unicode-segmentation = "1.7.1" - [target."cfg(target_os = \"windows\")".dependencies.winapi] - version = "0.3" - features = [ +[target."cfg(target_os = \"windows\")".dependencies.winapi] +version = "0.3" +features = [ "combaseapi", "commctrl", "dwmapi", @@ -91,6 +95,7 @@ parking_lot = "0.11" "winerror", "wingdi", "winnt", + "winnls", "winuser", "impl-default" ] @@ -99,8 +104,11 @@ parking_lot = "0.11" cairo-rs = "0.9" gio = "0.9" glib = "0.10" +glib-sys = "0.10" gtk = { version = "0.9", features = [ "v3_16" ] } gdk = "0.13" +gdk-sys = "0.10" gdk-pixbuf = { version = "0.9", features = [ "v2_36_8" ] } sourceview = { version = "0.9", optional = true } libappindicator = { version = "0.5", optional = true } +x11-dl = "2.18" diff --git a/build.rs b/build.rs index 354b16bd6..3c0bc2ab1 100644 --- a/build.rs +++ b/build.rs @@ -10,4 +10,14 @@ fn main() { { println!("cargo:rustc-cfg=use_colorsync_cgdisplaycreateuuidfromdisplayid"); } + // link carbon hotkey on macOS + #[cfg(target_os = "macos")] + { + if std::env::var("CARGO_CFG_TARGET_OS").map_or(false, |os| os == "macos") { + println!("cargo:rustc-link-lib=framework=Carbon"); + cc::Build::new() + .file("src/platform_impl/macos/carbon_hotkey/carbon_hotkey_binding.c") + .compile("carbon_hotkey_binding.a"); + } + } } diff --git a/examples/accelerator.rs b/examples/accelerator.rs new file mode 100644 index 000000000..feab62cad --- /dev/null +++ b/examples/accelerator.rs @@ -0,0 +1,55 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +fn main() { + use simple_logger::SimpleLogger; + use tao::{ + accelerator::{Accelerator, RawMods}, + dpi::LogicalSize, + event::{ElementState, Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::{Key, KeyCode, ModifiersState}, + window::WindowBuilder, + }; + + SimpleLogger::new().init().unwrap(); + + // create a sample hotkey + let hotkey = Accelerator::new(RawMods::Shift, KeyCode::Digit1); + // create local modifier state + let mut modifiers = ModifiersState::default(); + + let event_loop = EventLoop::new(); + + let _window = WindowBuilder::new() + .with_inner_size(LogicalSize::new(400.0, 200.0)) + .build(&event_loop) + .unwrap(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + if let Event::WindowEvent { event, .. } = event { + match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::ModifiersChanged(new_state) => { + // update our local modifier state + modifiers = new_state; + } + // catch only pressed event + WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => { + if hotkey.matches(&modifiers, &event.physical_key) { + println!( + "KeyEvent: `Shift` + `1` | logical_key: {:?}", + &event.logical_key + ); + // we can match manually without `Accelerator` + } else if event.key_without_modifiers() == Key::Character("1") && modifiers.is_empty() { + println!("KeyEvent: `1`"); + } + } + _ => (), + } + } + }); +} diff --git a/examples/control_flow.rs b/examples/control_flow.rs index d9561be67..147fde41a 100644 --- a/examples/control_flow.rs +++ b/examples/control_flow.rs @@ -5,8 +5,9 @@ use std::{thread, time}; use simple_logger::SimpleLogger; use tao::{ - event::{Event, KeyboardInput, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::Key, window::WindowBuilder, }; @@ -42,7 +43,7 @@ fn main() { let mut close_requested = false; event_loop.run(move |event, _, control_flow| { - use tao::event::{ElementState, StartCause, VirtualKeyCode}; + use tao::event::StartCause; println!("{:?}", event); match event { Event::NewEvents(start_cause) => { @@ -56,36 +57,37 @@ fn main() { close_requested = true; } WindowEvent::KeyboardInput { - input: - KeyboardInput { - virtual_keycode: Some(virtual_code), + event: + KeyEvent { + logical_key, state: ElementState::Pressed, .. }, .. - } => match virtual_code { - VirtualKeyCode::Key1 => { + } => { + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + if Key::Character("1") == logical_key { mode = Mode::Wait; println!("\nmode: {:?}\n", mode); } - VirtualKeyCode::Key2 => { + if Key::Character("2") == logical_key { mode = Mode::WaitUntil; println!("\nmode: {:?}\n", mode); } - VirtualKeyCode::Key3 => { + if Key::Character("3") == logical_key { mode = Mode::Poll; println!("\nmode: {:?}\n", mode); } - VirtualKeyCode::R => { + if Key::Character("r") == logical_key { request_redraw = !request_redraw; println!("\nrequest_redraw: {}\n", request_redraw); } - VirtualKeyCode::Escape => { + if Key::Escape == logical_key { close_requested = true; } - _ => (), - }, - _ => (), + } + _ => {} }, Event::MainEventsCleared => { if request_redraw && !wait_cancelled && !close_requested { diff --git a/examples/cursor.rs b/examples/cursor.rs index da12a4774..4f606e223 100644 --- a/examples/cursor.rs +++ b/examples/cursor.rs @@ -3,7 +3,7 @@ use simple_logger::SimpleLogger; use tao::{ - event::{ElementState, Event, KeyboardInput, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::{CursorIcon, WindowBuilder}, }; @@ -24,8 +24,8 @@ fn main() { Event::WindowEvent { event: WindowEvent::KeyboardInput { - input: - KeyboardInput { + event: + KeyEvent { state: ElementState::Pressed, .. }, @@ -44,9 +44,7 @@ fn main() { Event::WindowEvent { event: WindowEvent::CloseRequested, .. - } => { - *control_flow = ControlFlow::Exit; - } + } => *control_flow = ControlFlow::Exit, _ => (), } }); diff --git a/examples/cursor_grab.rs b/examples/cursor_grab.rs index 353ee6093..5675d549a 100644 --- a/examples/cursor_grab.rs +++ b/examples/cursor_grab.rs @@ -3,8 +3,9 @@ use simple_logger::SimpleLogger; use tao::{ - event::{DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, WindowEvent}, + event::{DeviceEvent, ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::{Key, ModifiersState}, window::WindowBuilder, }; @@ -27,19 +28,23 @@ fn main() { Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::KeyboardInput { - input: - KeyboardInput { + event: + KeyEvent { + logical_key: key, state: ElementState::Released, - virtual_keycode: Some(key), .. }, .. } => { - use tao::event::VirtualKeyCode::*; + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example match key { - Escape => *control_flow = ControlFlow::Exit, - G => window.set_cursor_grab(!modifiers.shift()).unwrap(), - H => window.set_cursor_visible(modifiers.shift()), + Key::Escape => *control_flow = ControlFlow::Exit, + Key::Character(ch) => match ch.to_lowercase().as_str() { + "g" => window.set_cursor_grab(!modifiers.shift_key()).unwrap(), + "h" => window.set_cursor_visible(modifiers.shift_key()), + _ => (), + }, _ => (), } } diff --git a/examples/custom_menu.rs b/examples/custom_menu.rs index 1dbd1c5c9..5c9466868 100644 --- a/examples/custom_menu.rs +++ b/examples/custom_menu.rs @@ -5,9 +5,11 @@ use simple_logger::SimpleLogger; #[cfg(target_os = "macos")] use tao::platform::macos::{CustomMenuItemExtMacOS, NativeImage}; use tao::{ + accelerator::{Accelerator, SysMods}, clipboard::Clipboard, event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::KeyCode, menu::{MenuBar as Menu, MenuItem, MenuItemAttributes, MenuType}, window::WindowBuilder, }; @@ -25,15 +27,15 @@ fn main() { // create `first_menu` let mut first_menu = Menu::new(); - // create an empty menu to be used as submenu - let mut my_sub_menu = Menu::new(); - // create second menu let mut second_menu = Menu::new(); + // create an empty menu to be used as submenu + let mut my_sub_menu = Menu::new(); - // create custom item `Disable menu` children of `my_sub_menu` - let mut test_menu_item = - my_sub_menu.add_item(MenuItemAttributes::new("Disable menu").with_accelerators("d")); + let mut test_menu_item = my_sub_menu.add_item( + MenuItemAttributes::new("Disable menu") + .with_accelerators(&Accelerator::new(SysMods::Cmd, KeyCode::KeyD)), + ); // add native `Copy` to `first_menu` menu // in macOS native item are required to get keyboard shortcut @@ -46,6 +48,7 @@ fn main() { // add `my_sub_menu` children of `first_menu` with `Sub menu` title first_menu.add_submenu("Sub menu", true, my_sub_menu); + first_menu.add_native_item(MenuItem::Quit); // create custom item `Selected and disabled` children of `second_menu` second_menu.add_item( diff --git a/examples/drag_window.rs b/examples/drag_window.rs index 39b48bf8c..73c9b0e38 100644 --- a/examples/drag_window.rs +++ b/examples/drag_window.rs @@ -3,10 +3,9 @@ use simple_logger::SimpleLogger; use tao::{ - event::{ - ElementState, Event, KeyboardInput, MouseButton, StartCause, VirtualKeyCode, WindowEvent, - }, + event::{ElementState, Event, KeyEvent, MouseButton, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::Key, window::{Window, WindowBuilder, WindowId}, }; @@ -47,14 +46,14 @@ fn main() { name_windows(entered_id, switched, &window_1, &window_2) } WindowEvent::KeyboardInput { - input: - KeyboardInput { + event: + KeyEvent { state: ElementState::Released, - virtual_keycode: Some(VirtualKeyCode::X), + logical_key, .. }, .. - } => { + } if logical_key == Key::Character("x") => { switched = !switched; name_windows(entered_id, switched, &window_1, &window_2); println!("Switched!") diff --git a/examples/fullscreen.rs b/examples/fullscreen.rs index 10140ca90..ce317d4e5 100644 --- a/examples/fullscreen.rs +++ b/examples/fullscreen.rs @@ -5,8 +5,9 @@ use std::io::{stdin, stdout, Write}; use simple_logger::SimpleLogger; use tao::{ - event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::Key, monitor::{MonitorHandle, VideoMode}, window::{Fullscreen, WindowBuilder}, }; @@ -44,35 +45,38 @@ fn main() { Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::KeyboardInput { - input: - KeyboardInput { - virtual_keycode: Some(virtual_code), - state, + event: + KeyEvent { + logical_key, + state: ElementState::Pressed, .. }, .. - } => match (virtual_code, state) { - (VirtualKeyCode::Escape, _) => *control_flow = ControlFlow::Exit, - (VirtualKeyCode::F, ElementState::Pressed) => { + } => { + if Key::Escape == logical_key { + *control_flow = ControlFlow::Exit + } + + if Key::Character("f") == logical_key { if window.fullscreen().is_some() { window.set_fullscreen(None); } else { window.set_fullscreen(fullscreen.clone()); } } - (VirtualKeyCode::S, ElementState::Pressed) => { + + if Key::Character("s") == logical_key { println!("window.fullscreen {:?}", window.fullscreen()); } - (VirtualKeyCode::M, ElementState::Pressed) => { + if Key::Character("m") == logical_key { let is_maximized = window.is_maximized(); window.set_maximized(!is_maximized); } - (VirtualKeyCode::D, ElementState::Pressed) => { + if Key::Character("d") == logical_key { decorations = !decorations; window.set_decorations(decorations); } - _ => (), - }, + } _ => (), }, _ => {} @@ -81,7 +85,6 @@ fn main() { } // Enumerate monitors and prompt user to choose one -#[allow(clippy::ok_expect)] fn prompt_for_monitor(event_loop: &EventLoop<()>) -> MonitorHandle { for (num, monitor) in event_loop.available_monitors().enumerate() { println!("Monitor #{}: {:?}", num, monitor.name()); @@ -92,7 +95,7 @@ fn prompt_for_monitor(event_loop: &EventLoop<()>) -> MonitorHandle { let mut num = String::new(); stdin().read_line(&mut num).unwrap(); - let num = num.trim().parse().ok().expect("Please enter a number"); + let num = num.trim().parse().expect("Please enter a number"); let monitor = event_loop .available_monitors() .nth(num) @@ -103,7 +106,6 @@ fn prompt_for_monitor(event_loop: &EventLoop<()>) -> MonitorHandle { monitor } -#[allow(clippy::ok_expect)] fn prompt_for_video_mode(monitor: &MonitorHandle) -> VideoMode { for (i, video_mode) in monitor.video_modes().enumerate() { println!("Video mode #{}: {}", i, video_mode); @@ -114,7 +116,7 @@ fn prompt_for_video_mode(monitor: &MonitorHandle) -> VideoMode { let mut num = String::new(); stdin().read_line(&mut num).unwrap(); - let num = num.trim().parse().ok().expect("Please enter a number"); + let num = num.trim().parse().expect("Please enter a number"); let video_mode = monitor .video_modes() .nth(num) diff --git a/examples/global_shortcut.rs b/examples/global_shortcut.rs new file mode 100644 index 000000000..1d433c613 --- /dev/null +++ b/examples/global_shortcut.rs @@ -0,0 +1,86 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +fn main() { + use simple_logger::SimpleLogger; + use std::str::FromStr; + use tao::{ + accelerator::{Accelerator, AcceleratorId, RawMods, SysMods}, + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::KeyCode, + platform::global_shortcut::ShortcutManager, + window::WindowBuilder, + }; + + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new(); + + // create new shortcut manager instance + let mut hotkey_manager = ShortcutManager::new(&event_loop); + + // create our accelerators + let shortcut_1 = Accelerator::new(SysMods::Shift, KeyCode::ArrowUp); + let shortcut_2 = Accelerator::new(RawMods::AltCtrlMeta, KeyCode::KeyB); + // use string parser to generate accelerator (require `std::str::FromStr`) + let shortcut_3 = Accelerator::from_str("COMMANDORCONTROL+SHIFT+3").unwrap(); + let shortcut_4 = Accelerator::from_str("COMMANDORCONTROL+shIfT+DOWN").unwrap(); + + // save a reference to unregister it later + let global_shortcut_1 = hotkey_manager.register(shortcut_1.clone()).unwrap(); + // register other accelerator's + hotkey_manager.register(shortcut_2.clone()).unwrap(); + hotkey_manager.register(shortcut_3).unwrap(); + hotkey_manager.register(shortcut_4.clone()).unwrap(); + + let window = WindowBuilder::new() + .with_title("A fantastic window!") + .build(&event_loop) + .unwrap(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + window_id, + } if window_id == window.id() => *control_flow = ControlFlow::Exit, + Event::MainEventsCleared => { + window.request_redraw(); + } + Event::GlobalShortcutEvent(hotkey_id) if hotkey_id == shortcut_1.clone().id() => { + println!("Pressed `shortcut_1` -- unregister for future use"); + // unregister key + hotkey_manager + .unregister(global_shortcut_1.clone()) + .unwrap(); + } + Event::GlobalShortcutEvent(hotkey_id) if hotkey_id == shortcut_2.clone().id() => { + println!("Pressed on `shortcut_2`"); + } + // you can match hotkey_id with accelerator_string only if you used `from_str` + // by example `shortcut_1` will NOT match AcceleratorId::new("SHIFT+UP") as it's + // been created with a struct and the ID is generated automatically + Event::GlobalShortcutEvent(hotkey_id) + if hotkey_id == AcceleratorId::new("COMMANDORCONTROL+SHIFT+3") => + { + println!("Pressed on `shortcut_3`"); + } + Event::GlobalShortcutEvent(hotkey_id) if hotkey_id == shortcut_4.clone().id() => { + println!("Pressed on `shortcut_4`"); + } + Event::GlobalShortcutEvent(hotkey_id) => { + println!("hotkey_id {:?}", hotkey_id); + } + _ => (), + } + }); +} + +// Global shortcut isn't supported on other's platforms. +#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] +fn main() { + println!("This platform doesn't support global_shortcut."); +} diff --git a/examples/handling_close.rs b/examples/handling_close.rs index e657fdb97..04d5cc5c9 100644 --- a/examples/handling_close.rs +++ b/examples/handling_close.rs @@ -3,8 +3,9 @@ use simple_logger::SimpleLogger; use tao::{ - event::{Event, KeyboardInput, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::Key, window::WindowBuilder, }; @@ -21,10 +22,6 @@ fn main() { let mut close_requested = false; event_loop.run(move |event, _, control_flow| { - use tao::event::{ - ElementState::Released, - VirtualKeyCode::{N, Y}, - }; *control_flow = ControlFlow::Wait; match event { @@ -48,16 +45,18 @@ fn main() { // the Y key. } WindowEvent::KeyboardInput { - input: - KeyboardInput { - virtual_keycode: Some(virtual_code), - state: Released, + event: + KeyEvent { + logical_key: Key::Character(char), + state: ElementState::Released, .. }, .. } => { - match virtual_code { - Y => { + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + match char { + "y" => { if close_requested { // This is where you'll want to do any cleanup you need. println!("Buh-bye!"); @@ -70,7 +69,7 @@ fn main() { *control_flow = ControlFlow::Exit; } } - N => { + "n" => { if close_requested { println!("Your window will continue to stay by your side."); close_requested = false; diff --git a/examples/minimize.rs b/examples/minimize.rs index 498a3c476..5bf98b400 100644 --- a/examples/minimize.rs +++ b/examples/minimize.rs @@ -5,8 +5,9 @@ extern crate tao; use simple_logger::SimpleLogger; use tao::{ - event::{Event, VirtualKeyCode, WindowEvent}, + event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::Key, window::WindowBuilder, }; @@ -31,15 +32,13 @@ fn main() { // Keyboard input event to handle minimize via a hotkey Event::WindowEvent { - event: WindowEvent::KeyboardInput { input, .. }, + event: WindowEvent::KeyboardInput { event, .. }, window_id, - } => { - if window_id == window.id() { - // Pressing the 'M' key will minimize the window - if input.virtual_keycode == Some(VirtualKeyCode::M) { - window.set_minimized(true); - } - } + } if window_id == window.id() && Key::Character("m") == event.logical_key => { + // Pressing the 'm' key will minimize the window + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + window.set_minimized(true); } _ => (), } diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs index cc62aa678..893ec8a05 100644 --- a/examples/multithreaded.rs +++ b/examples/multithreaded.rs @@ -9,8 +9,9 @@ fn main() { use simple_logger::SimpleLogger; use tao::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, - event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::{Key, ModifiersState}, window::{CursorIcon, Fullscreen, WindowBuilder}, }; @@ -31,6 +32,7 @@ fn main() { let (tx, rx) = mpsc::channel(); window_senders.insert(window.id(), tx); + let mut modifiers = ModifiersState::default(); thread::spawn(move || { while let Ok(event) = rx.recv() { match event { @@ -53,32 +55,87 @@ fn main() { ); } } - #[allow(deprecated)] + WindowEvent::ModifiersChanged(mod_state) => { + modifiers = mod_state; + } WindowEvent::KeyboardInput { - input: - KeyboardInput { + event: + KeyEvent { state: ElementState::Released, - virtual_keycode: Some(key), - modifiers, + logical_key: key, .. }, .. } => { + use Key::{ArrowLeft, ArrowRight, Character}; window.set_title(&format!("{:?}", key)); - let state = !modifiers.shift(); - use VirtualKeyCode::*; - match key { - A => window.set_always_on_top(state), - C => window.set_cursor_icon(match state { - true => CursorIcon::Progress, - false => CursorIcon::Default, - }), - D => window.set_decorations(!state), - // Cycle through video modes - Right | Left => { - video_mode_id = match key { - Left => video_mode_id.saturating_sub(1), - Right => (video_modes.len() - 1).min(video_mode_id + 1), + let state = !modifiers.shift_key(); + match &key { + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + Character(string) => match string.to_lowercase().as_str() { + "a" => window.set_always_on_top(state), + "c" => window.set_cursor_icon(match state { + true => CursorIcon::Progress, + false => CursorIcon::Default, + }), + "d" => window.set_decorations(!state), + "f" => window.set_fullscreen(match (state, modifiers.alt_key()) { + (true, false) => Some(Fullscreen::Borderless(None)), + (true, true) => Some(Fullscreen::Exclusive( + video_modes.iter().nth(video_mode_id).unwrap().clone(), + )), + (false, _) => None, + }), + "g" => window.set_cursor_grab(state).unwrap(), + "h" => window.set_cursor_visible(!state), + "i" => { + println!("Info:"); + println!("-> outer_position : {:?}", window.outer_position()); + println!("-> inner_position : {:?}", window.inner_position()); + println!("-> outer_size : {:?}", window.outer_size()); + println!("-> inner_size : {:?}", window.inner_size()); + println!("-> fullscreen : {:?}", window.fullscreen()); + } + "l" => window.set_min_inner_size(match state { + true => Some(WINDOW_SIZE), + false => None, + }), + "m" => window.set_maximized(state), + "p" => window.set_outer_position({ + let mut position = window.outer_position().unwrap(); + let sign = if state { 1 } else { -1 }; + position.x += 10 * sign; + position.y += 10 * sign; + position + }), + "q" => window.request_redraw(), + "r" => window.set_resizable(state), + "s" => window.set_inner_size(match state { + true => PhysicalSize::new(WINDOW_SIZE.width + 100, WINDOW_SIZE.height + 100), + false => WINDOW_SIZE, + }), + "w" => { + if let Size::Physical(size) = WINDOW_SIZE.into() { + window + .set_cursor_position(Position::Physical(PhysicalPosition::new( + size.width as i32 / 2, + size.height as i32 / 2, + ))) + .unwrap() + } + } + "z" => { + window.set_visible(false); + thread::sleep(Duration::from_secs(1)); + window.set_visible(true); + } + _ => (), + }, + ArrowRight | ArrowLeft => { + video_mode_id = match &key { + ArrowLeft => video_mode_id.saturating_sub(1), + ArrowRight => (video_modes.len() - 1).min(video_mode_id + 1), _ => unreachable!(), }; println!( @@ -86,56 +143,6 @@ fn main() { video_modes.iter().nth(video_mode_id).unwrap() ); } - F => window.set_fullscreen(match (state, modifiers.alt()) { - (true, false) => Some(Fullscreen::Borderless(None)), - (true, true) => Some(Fullscreen::Exclusive( - video_modes.iter().nth(video_mode_id).unwrap().clone(), - )), - (false, _) => None, - }), - G => window.set_cursor_grab(state).unwrap(), - H => window.set_cursor_visible(!state), - I => { - println!("Info:"); - println!("-> outer_position : {:?}", window.outer_position()); - println!("-> inner_position : {:?}", window.inner_position()); - println!("-> outer_size : {:?}", window.outer_size()); - println!("-> inner_size : {:?}", window.inner_size()); - println!("-> fullscreen : {:?}", window.fullscreen()); - } - L => window.set_min_inner_size(match state { - true => Some(WINDOW_SIZE), - false => None, - }), - M => window.set_maximized(state), - P => window.set_outer_position({ - let mut position = window.outer_position().unwrap(); - let sign = if state { 1 } else { -1 }; - position.x += 10 * sign; - position.y += 10 * sign; - position - }), - Q => window.request_redraw(), - R => window.set_resizable(state), - S => window.set_inner_size(match state { - true => PhysicalSize::new(WINDOW_SIZE.width + 100, WINDOW_SIZE.height + 100), - false => WINDOW_SIZE, - }), - W => { - if let Size::Physical(size) = WINDOW_SIZE.into() { - window - .set_cursor_position(Position::Physical(PhysicalPosition::new( - size.width as i32 / 2, - size.height as i32 / 2, - ))) - .unwrap() - } - } - Z => { - window.set_visible(false); - thread::sleep(Duration::from_secs(1)); - window.set_visible(true); - } _ => (), } } @@ -154,10 +161,10 @@ fn main() { WindowEvent::CloseRequested | WindowEvent::Destroyed | WindowEvent::KeyboardInput { - input: - KeyboardInput { + event: + KeyEvent { state: ElementState::Released, - virtual_keycode: Some(VirtualKeyCode::Escape), + logical_key: Key::Escape, .. }, .. diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs index 6ccad55c0..5ea78df57 100644 --- a/examples/multiwindow.rs +++ b/examples/multiwindow.rs @@ -5,12 +5,11 @@ use std::collections::HashMap; use simple_logger::SimpleLogger; use tao::{ - event::{ElementState, Event, KeyboardInput, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::Window, }; -#[allow(clippy::single_match)] fn main() { SimpleLogger::new().init().unwrap(); let event_loop = EventLoop::new(); @@ -24,34 +23,30 @@ fn main() { event_loop.run(move |event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; - match event { - Event::WindowEvent { event, window_id } => { - match event { - WindowEvent::CloseRequested => { - println!("Window {:?} has received the signal to close", window_id); + if let Event::WindowEvent { event, window_id } = event { + match event { + WindowEvent::CloseRequested => { + println!("Window {:?} has received the signal to close", window_id); - // This drops the window, causing it to close. - windows.remove(&window_id); + // This drops the window, causing it to close. + windows.remove(&window_id); - if windows.is_empty() { - *control_flow = ControlFlow::Exit; - } + if windows.is_empty() { + *control_flow = ControlFlow::Exit; } - WindowEvent::KeyboardInput { - input: - KeyboardInput { - state: ElementState::Pressed, - .. - }, + } + WindowEvent::KeyboardInput { + event: KeyEvent { + state: ElementState::Released, .. - } => { - let window = Window::new(event_loop).unwrap(); - windows.insert(window.id(), window); - } - _ => (), + }, + .. + } => { + let window = Window::new(event_loop).unwrap(); + windows.insert(window.id(), window); } + _ => (), } - _ => (), } }) } diff --git a/examples/resizable.rs b/examples/resizable.rs index d5799bad1..045ccd813 100644 --- a/examples/resizable.rs +++ b/examples/resizable.rs @@ -4,8 +4,9 @@ use simple_logger::SimpleLogger; use tao::{ dpi::LogicalSize, - event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::KeyCode, window::WindowBuilder, }; @@ -30,9 +31,9 @@ fn main() { Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::KeyboardInput { - input: - KeyboardInput { - virtual_keycode: Some(VirtualKeyCode::Space), + event: + KeyEvent { + physical_key: KeyCode::Space, state: ElementState::Released, .. }, diff --git a/examples/system_tray_no_menu.rs b/examples/system_tray_no_menu.rs index aacb99720..157f7ec24 100644 --- a/examples/system_tray_no_menu.rs +++ b/examples/system_tray_no_menu.rs @@ -49,7 +49,7 @@ fn main() { // position Y axis (Windows only) #[cfg(target_os = "windows")] { - rectangle.position.y = rectangle.position.y - window_size.height; + rectangle.position.y = rectangle.position.y - window_size.height - rectangle.size.height; } *rectangle diff --git a/examples/window_debug.rs b/examples/window_debug.rs index 173c57a6d..085eb25be 100644 --- a/examples/window_debug.rs +++ b/examples/window_debug.rs @@ -6,8 +6,9 @@ use simple_logger::SimpleLogger; use tao::{ dpi::{LogicalSize, PhysicalSize}, - event::{DeviceEvent, ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, + event::{DeviceEvent, ElementState, Event, KeyEvent, RawKeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::{Key, KeyCode}, window::{Fullscreen, WindowBuilder}, }; @@ -39,22 +40,24 @@ fn main() { *control_flow = ControlFlow::Wait; match event { + // This used to use the virtual key, but the new API + // only provides the `physical_key` (`Code`). Event::DeviceEvent { event: - DeviceEvent::Key(KeyboardInput { - virtual_keycode: Some(key), - state: ElementState::Pressed, + DeviceEvent::Key(RawKeyEvent { + physical_key, + state: ElementState::Released, .. }), .. - } => match key { - VirtualKeyCode::M => { + } => match physical_key { + KeyCode::KeyM => { if minimized { minimized = !minimized; window.set_minimized(minimized); } } - VirtualKeyCode::V => { + KeyCode::KeyV => { if !visible { visible = !visible; window.set_visible(visible); @@ -63,61 +66,65 @@ fn main() { _ => (), }, Event::WindowEvent { - event: WindowEvent::KeyboardInput { input, .. }, + event: + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: Key::Character(key_str), + state: ElementState::Released, + .. + }, + .. + }, .. - } => match input { - KeyboardInput { - virtual_keycode: Some(key), - state: ElementState::Pressed, - .. - } => match key { - VirtualKeyCode::E => { - fn area(size: PhysicalSize) -> u32 { - size.width * size.height - } - - let monitor = window.current_monitor().unwrap(); - if let Some(mode) = monitor - .video_modes() - .max_by(|a, b| area(a.size()).cmp(&area(b.size()))) - { - window.set_fullscreen(Some(Fullscreen::Exclusive(mode))); - } else { - eprintln!("no video modes available"); - } - } - VirtualKeyCode::F => { - if window.fullscreen().is_some() { - window.set_fullscreen(None); - } else { - let monitor = window.current_monitor(); - window.set_fullscreen(Some(Fullscreen::Borderless(monitor))); - } - } - VirtualKeyCode::P => { - if window.fullscreen().is_some() { - window.set_fullscreen(None); - } else { - window.set_fullscreen(Some(Fullscreen::Borderless(None))); - } + } => match key_str { + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + "e" => { + fn area(size: PhysicalSize) -> u32 { + size.width * size.height } - VirtualKeyCode::M => { - minimized = !minimized; - window.set_minimized(minimized); - } - VirtualKeyCode::Q => { - *control_flow = ControlFlow::Exit; + + let monitor = window.current_monitor().unwrap(); + if let Some(mode) = monitor + .video_modes() + .max_by(|a, b| area(a.size()).cmp(&area(b.size()))) + { + window.set_fullscreen(Some(Fullscreen::Exclusive(mode))); + } else { + eprintln!("no video modes available"); } - VirtualKeyCode::V => { - visible = !visible; - window.set_visible(visible); + } + "f" => { + if window.fullscreen().is_some() { + window.set_fullscreen(None); + } else { + let monitor = window.current_monitor(); + window.set_fullscreen(Some(Fullscreen::Borderless(monitor))); } - VirtualKeyCode::X => { - let is_maximized = window.is_maximized(); - window.set_maximized(!is_maximized); + } + "p" => { + if window.fullscreen().is_some() { + window.set_fullscreen(None); + } else { + window.set_fullscreen(Some(Fullscreen::Borderless(None))); } - _ => (), - }, + } + "m" => { + minimized = !minimized; + window.set_minimized(minimized); + } + "q" => { + *control_flow = ControlFlow::Exit; + } + "v" => { + visible = !visible; + window.set_visible(visible); + } + "x" => { + let is_maximized = window.is_maximized(); + window.set_maximized(!is_maximized); + } _ => (), }, Event::WindowEvent { diff --git a/src/accelerator.rs b/src/accelerator.rs new file mode 100644 index 000000000..70a061863 --- /dev/null +++ b/src/accelerator.rs @@ -0,0 +1,427 @@ +//! The Accelerator struct and associated types. + +use crate::{ + error::OsError, + keyboard::{KeyCode, ModifiersState, NativeKeyCode}, +}; +use std::{ + borrow::Borrow, + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + str::FromStr, +}; + +/// Base `Accelerator` functions. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct Accelerator { + id: Option, + pub(crate) mods: ModifiersState, + pub(crate) key: KeyCode, +} + +impl Accelerator { + /// Creates a new accelerator to define keyboard shortcuts throughout your application. + pub fn new(mods: impl Into>, key: KeyCode) -> Self { + Self { + id: None, + mods: mods.into().unwrap_or_else(ModifiersState::empty), + key, + } + } + + /// Assign a custom accelerator id. + pub fn with_id(mut self, id: AcceleratorId) -> Self { + self.id = Some(id); + self + } + + /// Returns an identifier unique to the accelerator. + pub fn id(self) -> AcceleratorId { + if let Some(id) = self.id { + return id; + } + + AcceleratorId(hash_accelerator_to_u16(self)) + } + + /// Returns `true` if this [`KeyCode`] and [`ModifiersState`] matches this `Accelerator`. + /// + /// [`KeyCode`]: KeyCode + /// [`ModifiersState`]: crate::keyboard::ModifiersState + pub fn matches(&self, modifiers: impl Borrow, key: impl Borrow) -> bool { + // Should be a const but const bit_or doesn't work here. + let base_mods = + ModifiersState::SHIFT | ModifiersState::CONTROL | ModifiersState::ALT | ModifiersState::SUPER; + let modifiers = modifiers.borrow(); + let key = key.borrow(); + self.mods == *modifiers & base_mods && self.key == *key + } +} + +// Accelerator::from_str is available to be backward +// compatible with tauri and it also open the option +// to generate accelerator from string +impl FromStr for Accelerator { + type Err = OsError; + fn from_str(accelerator_string: &str) -> Result { + Ok(parse_accelerator(accelerator_string)) + } +} + +/// Represents the platform-agnostic keyboard modifiers, for command handling. +/// +/// **This does one thing: it allows specifying accelerators that use the Command key +/// on macOS, but use the Ctrl key on other platforms.** +#[derive(Debug, Clone, Copy)] +pub enum SysMods { + None, + Shift, + /// Command on macOS, and Ctrl on windows/linux + Cmd, + /// Command + Alt on macOS, Ctrl + Alt on windows/linux + AltCmd, + /// Command + Shift on macOS, Ctrl + Shift on windows/linux + CmdShift, + /// Command + Alt + Shift on macOS, Ctrl + Alt + Shift on windows/linux + AltCmdShift, +} + +/// Represents the active modifier keys. +/// +/// This is intended to be clearer than [`ModifiersState`], when describing accelerators. +/// +#[derive(Debug, Clone, Copy, PartialEq, Hash)] +pub enum RawMods { + None, + Alt, + Ctrl, + Meta, + Shift, + AltCtrl, + AltMeta, + AltShift, + CtrlShift, + CtrlMeta, + MetaShift, + AltCtrlMeta, + AltCtrlShift, + AltMetaShift, + CtrlMetaShift, + AltCtrlMetaShift, +} + +// we do this so that Accelerator::new can accept `None` as an initial argument. +impl From for Option { + fn from(src: SysMods) -> Option { + Some(src.into()) + } +} + +impl From for Option { + fn from(src: RawMods) -> Option { + Some(src.into()) + } +} + +impl From for ModifiersState { + fn from(src: RawMods) -> ModifiersState { + let (alt, ctrl, meta, shift) = match src { + RawMods::None => (false, false, false, false), + RawMods::Alt => (true, false, false, false), + RawMods::Ctrl => (false, true, false, false), + RawMods::Meta => (false, false, true, false), + RawMods::Shift => (false, false, false, true), + RawMods::AltCtrl => (true, true, false, false), + RawMods::AltMeta => (true, false, true, false), + RawMods::AltShift => (true, false, false, true), + RawMods::CtrlMeta => (false, true, true, false), + RawMods::CtrlShift => (false, true, false, true), + RawMods::MetaShift => (false, false, true, true), + RawMods::AltCtrlMeta => (true, true, true, false), + RawMods::AltMetaShift => (true, false, true, true), + RawMods::AltCtrlShift => (true, true, false, true), + RawMods::CtrlMetaShift => (false, true, true, true), + RawMods::AltCtrlMetaShift => (true, true, true, true), + }; + let mut mods = ModifiersState::empty(); + mods.set(ModifiersState::ALT, alt); + mods.set(ModifiersState::CONTROL, ctrl); + mods.set(ModifiersState::SUPER, meta); + mods.set(ModifiersState::SHIFT, shift); + mods + } +} + +impl From for ModifiersState { + fn from(src: SysMods) -> ModifiersState { + let (alt, ctrl, meta, shift) = match src { + SysMods::None => (false, false, false, false), + SysMods::Shift => (false, false, false, true), + + #[cfg(target_os = "macos")] + SysMods::AltCmd => (true, false, true, false), + #[cfg(not(target_os = "macos"))] + SysMods::AltCmd => (true, true, false, false), + + #[cfg(target_os = "macos")] + SysMods::AltCmdShift => (true, false, true, true), + #[cfg(not(target_os = "macos"))] + SysMods::AltCmdShift => (true, true, false, true), + + #[cfg(target_os = "macos")] + SysMods::Cmd => (false, false, true, false), + #[cfg(not(target_os = "macos"))] + SysMods::Cmd => (false, true, false, false), + + #[cfg(target_os = "macos")] + SysMods::CmdShift => (false, false, true, true), + #[cfg(not(target_os = "macos"))] + SysMods::CmdShift => (false, true, false, true), + }; + let mut mods = ModifiersState::empty(); + mods.set(ModifiersState::ALT, alt); + mods.set(ModifiersState::CONTROL, ctrl); + mods.set(ModifiersState::SUPER, meta); + mods.set(ModifiersState::SHIFT, shift); + mods + } +} + +impl From for RawMods { + fn from(src: SysMods) -> RawMods { + #[cfg(target_os = "macos")] + match src { + SysMods::None => RawMods::None, + SysMods::Shift => RawMods::Shift, + SysMods::Cmd => RawMods::Meta, + SysMods::AltCmd => RawMods::AltMeta, + SysMods::CmdShift => RawMods::MetaShift, + SysMods::AltCmdShift => RawMods::AltMetaShift, + } + #[cfg(not(target_os = "macos"))] + match src { + SysMods::None => RawMods::None, + SysMods::Shift => RawMods::Shift, + SysMods::Cmd => RawMods::Ctrl, + SysMods::AltCmd => RawMods::AltCtrl, + SysMods::CmdShift => RawMods::CtrlShift, + SysMods::AltCmdShift => RawMods::AltCtrlShift, + } + } +} + +/// Identifier of an Accelerator. +/// +/// Whenever you receive an event arising from a [GlobalShortcutEvent], [MenuEvent] +/// or [KeyboardInput] , this event contains a `AcceleratorId` which identifies its origin. +/// +/// [MenuEvent]: crate::event::Event::MenuEvent +/// [KeyboardInput]: crate::event::WindowEvent::KeyboardInput +/// [GlobalShortcutEvent]: crate::event::Event::GlobalShortcutEvent +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct AcceleratorId(pub u16); + +impl From for u16 { + fn from(s: AcceleratorId) -> u16 { + s.0 + } +} + +impl From for u32 { + fn from(s: AcceleratorId) -> u32 { + s.0 as u32 + } +} + +impl From for i32 { + fn from(s: AcceleratorId) -> i32 { + s.0 as i32 + } +} + +impl AcceleratorId { + /// Return an empty `AcceleratorId`. + pub const EMPTY: AcceleratorId = AcceleratorId(0); + + /// Create new `AcceleratorId` from a String. + pub fn new(accelerator_string: &str) -> AcceleratorId { + AcceleratorId(hash_string_to_u16(accelerator_string)) + } + + /// Whenever this menu is empty. + pub fn is_empty(self) -> bool { + Self::EMPTY == self + } +} + +fn parse_accelerator(accelerator_string: &str) -> Accelerator { + let mut mods = ModifiersState::empty(); + let mut key = KeyCode::Unidentified(NativeKeyCode::Unidentified); + + for raw in accelerator_string.to_uppercase().split('+') { + let token = raw.trim().to_string(); + if token.is_empty() { + continue; + } + + match token.as_str() { + "OPTION" | "ALT" => { + mods.set(ModifiersState::ALT, true); + continue; + } + "CONTROL" | "CTRL" => { + mods.set(ModifiersState::CONTROL, true); + continue; + } + "COMMAND" | "CMD" | "SUPER" => { + mods.set(ModifiersState::SUPER, true); + continue; + } + "SHIFT" => { + mods.set(ModifiersState::SHIFT, true); + continue; + } + "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { + #[cfg(target_os = "macos")] + mods.set(ModifiersState::SUPER, true); + #[cfg(not(target_os = "macos"))] + mods.set(ModifiersState::CONTROL, true); + continue; + } + _ => {} + } + + if let Ok(keycode) = KeyCode::from_str(token.to_uppercase().as_str()) { + key = keycode; + } + } + + Accelerator { + // use the accelerator string as id + id: Some(AcceleratorId(hash_string_to_u16(accelerator_string))), + key, + mods, + } +} + +fn hash_string_to_u16(title: &str) -> u16 { + let mut s = DefaultHasher::new(); + // we transform to uppercase to make sure + // if we write Shift instead of SHIFT it return + // the same ID + title.to_uppercase().hash(&mut s); + s.finish() as u16 +} + +fn hash_accelerator_to_u16(hotkey: Accelerator) -> u16 { + let mut s = DefaultHasher::new(); + hotkey.hash(&mut s); + s.finish() as u16 +} + +#[test] +fn test_parse_accelerator() { + assert_eq!( + parse_accelerator("CTRL+X"), + Accelerator { + id: Some(AcceleratorId::new("CTRL+X")), + mods: ModifiersState::CONTROL, + key: KeyCode::KeyX, + } + ); + assert_eq!( + parse_accelerator("SHIFT+C"), + Accelerator { + id: Some(AcceleratorId::new("SHIFT+C")), + mods: ModifiersState::SHIFT, + key: KeyCode::KeyC, + } + ); + assert_eq!( + parse_accelerator("CTRL+Z"), + Accelerator { + id: Some(AcceleratorId::new("CTRL+Z")), + mods: ModifiersState::CONTROL, + key: KeyCode::KeyZ, + } + ); + assert_eq!( + parse_accelerator("super+ctrl+SHIFT+alt+Up"), + Accelerator { + id: Some(AcceleratorId::new("super+ctrl+SHIFT+alt+Up")), + mods: ModifiersState::SUPER + | ModifiersState::CONTROL + | ModifiersState::SHIFT + | ModifiersState::ALT, + key: KeyCode::ArrowUp, + } + ); + assert_eq!( + parse_accelerator("5"), + Accelerator { + id: Some(AcceleratorId::new("5")), + mods: ModifiersState::empty(), + key: KeyCode::Digit5, + } + ); + assert_eq!( + parse_accelerator("G"), + Accelerator { + id: Some(AcceleratorId::new("G")), + mods: ModifiersState::empty(), + key: KeyCode::KeyG, + } + ); + assert_eq!( + parse_accelerator("G"), + Accelerator { + // id not with same uppercase should work + id: Some(AcceleratorId::new("g")), + mods: ModifiersState::empty(), + key: KeyCode::KeyG, + } + ); + assert_eq!( + parse_accelerator("+G"), + Accelerator { + id: Some(AcceleratorId::new("+G")), + mods: ModifiersState::empty(), + key: KeyCode::KeyG, + } + ); + assert_eq!( + parse_accelerator("SHGSH+G"), + Accelerator { + id: Some(AcceleratorId::new("SHGSH+G")), + mods: ModifiersState::empty(), + key: KeyCode::KeyG, + } + ); + assert_eq!( + parse_accelerator("SHiFT+F12"), + Accelerator { + id: Some(AcceleratorId::new("SHIFT+F12")), + mods: ModifiersState::SHIFT, + key: KeyCode::F12, + } + ); + assert_eq!( + parse_accelerator("CmdOrCtrl+Space"), + Accelerator { + id: Some(AcceleratorId::new("CmdOrCtrl+Space")), + #[cfg(target_os = "macos")] + mods: ModifiersState::SUPER, + #[cfg(not(target_os = "macos"))] + mods: ModifiersState::CONTROL, + key: KeyCode::Space, + } + ); + assert_eq!( + parse_accelerator("CTRL+"), + Accelerator { + id: Some(AcceleratorId::new("CTRL+")), + mods: ModifiersState::CONTROL, + key: KeyCode::Unidentified(NativeKeyCode::Unidentified), + } + ); +} diff --git a/src/event.rs b/src/event.rs index c9ed0c2fb..6608037e1 100644 --- a/src/event.rs +++ b/src/event.rs @@ -40,7 +40,9 @@ use instant::Instant; use std::path::PathBuf; use crate::{ + accelerator::AcceleratorId, dpi::{PhysicalPosition, PhysicalSize}, + keyboard::{self, ModifiersState}, menu::{MenuId, MenuType}, platform_impl, window::{Theme, WindowId}, @@ -89,6 +91,13 @@ pub enum Event<'a, T: 'static> { position: PhysicalPosition, }, + /// Emitted when a global shortcut is triggered. + /// + /// ## Platform-specific + /// + /// - **iOS / Android:** Unsupported. + GlobalShortcutEvent(AcceleratorId), + /// Emitted when the application has been suspended. Suspended, @@ -170,6 +179,7 @@ impl Clone for Event<'static, T> { event: *event, position: *position, }, + GlobalShortcutEvent(accelerator_id) => GlobalShortcutEvent(*accelerator_id), } } } @@ -198,6 +208,7 @@ impl<'a, T> Event<'a, T> { event, position, }), + GlobalShortcutEvent(accelerator_id) => Ok(GlobalShortcutEvent(accelerator_id)), } } @@ -228,6 +239,7 @@ impl<'a, T> Event<'a, T> { event, position, }), + GlobalShortcutEvent(accelerator_id) => Some(GlobalShortcutEvent(accelerator_id)), } } } @@ -292,7 +304,7 @@ pub enum WindowEvent<'a> { HoveredFileCancelled, /// The window received a unicode character. - ReceivedCharacter(char), + ReceivedImeText(String), /// The window gained or lost focus. /// @@ -300,9 +312,15 @@ pub enum WindowEvent<'a> { Focused(bool), /// An event from the keyboard has been received. + /// + /// ## Platform-specific + /// - **Windows:** The shift key overrides NumLock. In other words, while shift is held down, + /// numpad keys act as if NumLock wasn't active. When this is used, the OS sends fake key + /// events which are not marked as `is_synthetic`. KeyboardInput { device_id: DeviceId, - input: KeyboardInput, + event: KeyEvent, + /// If `true`, the event was generated synthetically by tao /// in one of the following circumstances: /// @@ -413,15 +431,15 @@ impl Clone for WindowEvent<'static> { DroppedFile(file) => DroppedFile(file.clone()), HoveredFile(file) => HoveredFile(file.clone()), HoveredFileCancelled => HoveredFileCancelled, - ReceivedCharacter(c) => ReceivedCharacter(*c), + ReceivedImeText(c) => ReceivedImeText(c.clone()), Focused(f) => Focused(*f), KeyboardInput { device_id, - input, + event, is_synthetic, } => KeyboardInput { device_id: *device_id, - input: *input, + event: event.clone(), is_synthetic: *is_synthetic, }, @@ -504,15 +522,15 @@ impl<'a> WindowEvent<'a> { DroppedFile(file) => Some(DroppedFile(file)), HoveredFile(file) => Some(HoveredFile(file)), HoveredFileCancelled => Some(HoveredFileCancelled), - ReceivedCharacter(c) => Some(ReceivedCharacter(c)), + ReceivedImeText(c) => Some(ReceivedImeText(c)), Focused(focused) => Some(Focused(focused)), KeyboardInput { device_id, - input, + event, is_synthetic, } => Some(KeyboardInput { device_id, - input, + event, is_synthetic, }), ModifiersChanged(modifiers) => Some(ModifiersChanged(modifiers)), @@ -638,38 +656,118 @@ pub enum DeviceEvent { state: ElementState, }, - Key(KeyboardInput), + Key(RawKeyEvent), Text { codepoint: char, }, } -/// Describes a keyboard input event. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// Describes a keyboard input as a raw device event. +/// +/// Note that holding down a key may produce repeated `RawKeyEvent`s. The +/// operating system doesn't provide information whether such an event is a +/// repeat or the initial keypress. An application may emulate this by, for +/// example keeping a Map/Set of pressed keys and determining whether a keypress +/// corresponds to an already pressed key. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct KeyboardInput { - /// Identifies the physical key pressed +pub struct RawKeyEvent { + pub physical_key: keyboard::KeyCode, + pub state: ElementState, +} + +/// Describes a keyboard input targeting a window. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEvent { + /// Represents the position of a key independent of the currently active layout. + /// + /// It also uniquely identifies the physical key (i.e. it's mostly synonymous with a scancode). + /// The most prevalent use case for this is games. For example the default keys for the player + /// to move around might be the W, A, S, and D keys on a US layout. The position of these keys + /// is more important than their label, so they should map to Z, Q, S, and D on an "AZERTY" + /// layout. (This value is `KeyCode::KeyW` for the Z key on an AZERTY layout.) /// - /// This should not change if the user adjusts the host's keyboard map. Use when the physical location of the - /// key is more important than the key's host GUI semantics, such as for movement controls in a first-person - /// game. - pub scancode: ScanCode, + /// Note that `Fn` and `FnLock` key events are not guaranteed to be emitted by `tao`. These + /// keys are usually handled at the hardware or OS level. + pub physical_key: keyboard::KeyCode, + /// This value is affected by all modifiers except Ctrl. + /// + /// This has two use cases: + /// - Allows querying whether the current input is a Dead key. + /// - Allows handling key-bindings on platforms which don't + /// support `key_without_modifiers`. + /// + /// ## Platform-specific + /// - **Web:** Dead keys might be reported as the real key instead + /// of `Dead` depending on the browser/OS. + /// + pub logical_key: keyboard::Key<'static>, + + /// Contains the text produced by this keypress. + /// + /// In most cases this is identical to the content + /// of the `Character` variant of `logical_key`. + /// However, on Windows when a dead key was pressed earlier + /// but cannot be combined with the character from this + /// keypress, the produced text will consist of two characters: + /// the dead-key-character followed by the character resulting + /// from this keypress. + /// + /// An additional difference from `logical_key` is that + /// this field stores the text representation of any key + /// that has such a representation. For example when + /// `logical_key` is `Key::Enter`, this field is `Some("\r")`. + /// + /// This is `None` if the current keypress cannot + /// be interpreted as text. + /// + /// See also: `text_with_all_modifiers()` + pub text: Option<&'static str>, + + pub location: keyboard::KeyLocation, pub state: ElementState, + pub repeat: bool, + + pub(crate) platform_specific: platform_impl::KeyEventExtra, +} - /// Identifies the semantic meaning of the key +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +impl KeyEvent { + /// Identical to `KeyEvent::text` but this is affected by Ctrl. /// - /// Use when the semantics of the key are more important than the physical location of the key, such as when - /// implementing appropriate behavior for "page up." - pub virtual_keycode: Option, + /// For example, pressing Ctrl+a produces `Some("\x01")`. + pub fn text_with_all_modifiers(&self) -> Option<&str> { + self.platform_specific.text_with_all_modifiers + } - /// Modifier keys active at the time of this input. + /// This value ignores all modifiers including, + /// but not limited to Shift, Caps Lock, + /// and Ctrl. In most cases this means that the + /// unicode character in the resulting string is lowercase. + /// + /// This is useful for key-bindings / shortcut key combinations. /// - /// This is tracked internally to avoid tracking errors arising from modifier key state changes when events from - /// this device are not being delivered to the application, e.g. due to keyboard focus being elsewhere. - #[deprecated = "Deprecated in favor of WindowEvent::ModifiersChanged"] - pub modifiers: ModifiersState, + /// In case `logical_key` reports `Dead`, this will still report the + /// key as `Character` according to the current keyboard layout. This value + /// cannot be `Dead`. + pub fn key_without_modifiers(&self) -> keyboard::Key<'static> { + self.platform_specific.key_without_modifiers.clone() + } +} + +#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] +impl KeyEvent { + /// Identical to `KeyEvent::text`. + pub fn text_with_all_modifiers(&self) -> Option<&str> { + self.text.clone() + } + + /// Identical to `KeyEvent::logical_key`. + pub fn key_without_modifiers(&self) -> keyboard::Key<'static> { + self.logical_key.clone() + } } /// Describes touch-screen input state. @@ -806,9 +904,6 @@ impl Force { } } -/// Hardware-dependent keyboard scan code. -pub type ScanCode = u32; - /// Identifier for a specific analog axis on some device. pub type AxisId = u32; @@ -851,303 +946,3 @@ pub enum MouseScrollDelta { /// platform. PixelDelta(PhysicalPosition), } - -/// Symbolic name for a keyboard key. -#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)] -#[repr(u32)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum VirtualKeyCode { - /// The '1' key over the letters. - Key1, - /// The '2' key over the letters. - Key2, - /// The '3' key over the letters. - Key3, - /// The '4' key over the letters. - Key4, - /// The '5' key over the letters. - Key5, - /// The '6' key over the letters. - Key6, - /// The '7' key over the letters. - Key7, - /// The '8' key over the letters. - Key8, - /// The '9' key over the letters. - Key9, - /// The '0' key over the 'O' and 'P' keys. - Key0, - - A, - B, - C, - D, - E, - F, - G, - H, - I, - J, - K, - L, - M, - N, - O, - P, - Q, - R, - S, - T, - U, - V, - W, - X, - Y, - Z, - - /// The Escape key, next to F1. - Escape, - - F1, - F2, - F3, - F4, - F5, - F6, - F7, - F8, - F9, - F10, - F11, - F12, - F13, - F14, - F15, - F16, - F17, - F18, - F19, - F20, - F21, - F22, - F23, - F24, - - /// Print Screen/SysRq. - Snapshot, - /// Scroll Lock. - Scroll, - /// Pause/Break key, next to Scroll lock. - Pause, - - /// `Insert`, next to Backspace. - Insert, - Home, - Delete, - End, - PageDown, - PageUp, - - Left, - Up, - Right, - Down, - - /// The Backspace key, right over Enter. - // TODO: rename - Back, - /// The Enter key. - Return, - /// The space bar. - Space, - - /// The "Compose" key on Linux. - Compose, - - Caret, - - Numlock, - Numpad0, - Numpad1, - Numpad2, - Numpad3, - Numpad4, - Numpad5, - Numpad6, - Numpad7, - Numpad8, - Numpad9, - NumpadAdd, - NumpadDivide, - NumpadDecimal, - NumpadComma, - NumpadEnter, - NumpadEquals, - NumpadMultiply, - NumpadSubtract, - - AbntC1, - AbntC2, - Apostrophe, - Apps, - Asterisk, - At, - Ax, - Backslash, - Calculator, - Capital, - Colon, - Comma, - Convert, - Equals, - Grave, - Kana, - Kanji, - LAlt, - LBracket, - LControl, - LShift, - LWin, - Mail, - MediaSelect, - MediaStop, - Minus, - Mute, - MyComputer, - // also called "Next" - NavigateForward, - // also called "Prior" - NavigateBackward, - NextTrack, - NoConvert, - OEM102, - Period, - PlayPause, - Plus, - Power, - PrevTrack, - RAlt, - RBracket, - RControl, - RShift, - RWin, - Semicolon, - Slash, - Sleep, - Stop, - Sysrq, - Tab, - Underline, - Unlabeled, - VolumeDown, - VolumeUp, - Wake, - WebBack, - WebFavorites, - WebForward, - WebHome, - WebRefresh, - WebSearch, - WebStop, - Yen, - Copy, - Paste, - Cut, -} - -impl ModifiersState { - /// Returns `true` if the shift key is pressed. - pub fn shift(&self) -> bool { - self.intersects(Self::SHIFT) - } - /// Returns `true` if the control key is pressed. - pub fn ctrl(&self) -> bool { - self.intersects(Self::CTRL) - } - /// Returns `true` if the alt key is pressed. - pub fn alt(&self) -> bool { - self.intersects(Self::ALT) - } - /// Returns `true` if the logo key is pressed. - pub fn logo(&self) -> bool { - self.intersects(Self::LOGO) - } -} - -bitflags! { - /// Represents the current state of the keyboard modifiers - /// - /// Each flag represents a modifier and is set if this modifier is active. - #[derive(Default)] - pub struct ModifiersState: u32 { - // left and right modifiers are currently commented out, but we should be able to support - // them in a future release - /// The "shift" key. - const SHIFT = 0b100 << 0; - // const LSHIFT = 0b010 << 0; - // const RSHIFT = 0b001 << 0; - /// The "control" key. - const CTRL = 0b100 << 3; - // const LCTRL = 0b010 << 3; - // const RCTRL = 0b001 << 3; - /// The "alt" key. - const ALT = 0b100 << 6; - // const LALT = 0b010 << 6; - // const RALT = 0b001 << 6; - /// This is the "windows" key on PC and "command" key on Mac. - const LOGO = 0b100 << 9; - // const LLOGO = 0b010 << 9; - // const RLOGO = 0b001 << 9; - } -} - -#[cfg(feature = "serde")] -mod modifiers_serde { - use super::ModifiersState; - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - - #[derive(Default, Serialize, Deserialize)] - #[serde(default)] - #[serde(rename = "ModifiersState")] - pub struct ModifiersStateSerialize { - pub shift: bool, - pub ctrl: bool, - pub alt: bool, - pub logo: bool, - } - - impl Serialize for ModifiersState { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let s = ModifiersStateSerialize { - shift: self.shift(), - ctrl: self.ctrl(), - alt: self.alt(), - logo: self.logo(), - }; - s.serialize(serializer) - } - } - - impl<'de> Deserialize<'de> for ModifiersState { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let ModifiersStateSerialize { - shift, - ctrl, - alt, - logo, - } = ModifiersStateSerialize::deserialize(deserializer)?; - let mut m = ModifiersState::empty(); - m.set(ModifiersState::SHIFT, shift); - m.set(ModifiersState::CTRL, ctrl); - m.set(ModifiersState::ALT, alt); - m.set(ModifiersState::LOGO, logo); - Ok(m) - } - } -} diff --git a/src/keyboard.rs b/src/keyboard.rs new file mode 100644 index 000000000..422587da2 --- /dev/null +++ b/src/keyboard.rs @@ -0,0 +1,1588 @@ +//! **UNSTABLE** -- Types related to the keyboard. + +// This file contains a substantial portion of the UI Events Specification by the W3C. In +// particular, the variant names within `Key` and `KeyCode` and their documentation are modified +// versions of contents of the aforementioned specification. +// +// The original documents are: +// +// ### For `Key` +// UI Events KeyboardEvent key Values +// https://www.w3.org/TR/2017/CR-uievents-key-20170601/ +// Copyright © 2017 W3C® (MIT, ERCIM, Keio, Beihang). +// +// ### For `KeyCode` +// UI Events KeyboardEvent code Values +// https://www.w3.org/TR/2017/CR-uievents-code-20170601/ +// Copyright © 2017 W3C® (MIT, ERCIM, Keio, Beihang). +// +// These documents were used under the terms of the following license. This W3C license as well as +// the W3C short notice apply to the `Key` and `KeyCode` enums and their variants and the +// documentation attached to their variants. + +// --------- BEGGINING OF W3C LICENSE -------------------------------------------------------------- +// +// License +// +// By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, +// and will comply with the following terms and conditions. +// +// Permission to copy, modify, and distribute this work, with or without modification, for any +// purpose and without fee or royalty is hereby granted, provided that you include the following on +// ALL copies of the work or portions thereof, including modifications: +// +// - The full text of this NOTICE in a location viewable to users of the redistributed or derivative +// work. +// - Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none +// exist, the W3C Software and Document Short Notice should be included. +// - Notice of any changes or modifications, through a copyright statement on the new code or +// document such as "This software or document includes material copied from or derived from +// [title and URI of the W3C document]. Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." +// +// Disclaimers +// +// THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR +// ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD +// PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. +// +// COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES +// ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. +// +// The name and trademarks of copyright holders may NOT be used in advertising or publicity +// pertaining to the work without specific, written prior permission. Title to copyright in this +// work will at all times remain with copyright holders. +// +// --------- END OF W3C LICENSE -------------------------------------------------------------------- + +// --------- BEGGINING OF W3C SHORT NOTICE --------------------------------------------------------- +// +// tao: https://github.com/tauri-apps/tao +// +// Copyright © 2021 World Wide Web Consortium, (Massachusetts Institute of Technology, European +// Research Consortium for Informatics and Mathematics, Keio University, Beihang). All Rights +// Reserved. This work is distributed under the W3C® Software License [1] in the hope that it will +// be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// [1] http://www.w3.org/Consortium/Legal/copyright-software +// +// --------- END OF W3C SHORT NOTICE --------------------------------------------------------------- + +use std::{fmt, str::FromStr}; + +use crate::{ + error::OsError, + platform_impl::{ + keycode_from_scancode as platform_keycode_from_scancode, + keycode_to_scancode as platform_keycode_to_scancode, + }, +}; + +impl ModifiersState { + /// Returns `true` if the shift key is pressed. + pub fn shift_key(&self) -> bool { + self.intersects(Self::SHIFT) + } + /// Returns `true` if the control key is pressed. + pub fn control_key(&self) -> bool { + self.intersects(Self::CONTROL) + } + /// Returns `true` if the alt key is pressed. + pub fn alt_key(&self) -> bool { + self.intersects(Self::ALT) + } + /// Returns `true` if the super key is pressed. + pub fn super_key(&self) -> bool { + self.intersects(Self::SUPER) + } +} + +bitflags! { + /// Represents the current state of the keyboard modifiers + /// + /// Each flag represents a modifier and is set if this modifier is active. + #[derive(Default)] + pub struct ModifiersState: u32 { + // left and right modifiers are currently commented out, but we should be able to support + // them in a future release + /// The "shift" key. + const SHIFT = 0b100 << 0; + // const LSHIFT = 0b010 << 0; + // const RSHIFT = 0b001 << 0; + /// The "control" key. + const CONTROL = 0b100 << 3; + // const LCTRL = 0b010 << 3; + // const RCTRL = 0b001 << 3; + /// The "alt" key. + const ALT = 0b100 << 6; + // const LALT = 0b010 << 6; + // const RALT = 0b001 << 6; + /// This is the "windows" key on PC and "command" key on Mac. + const SUPER = 0b100 << 9; + // const LSUPER = 0b010 << 9; + // const RSUPER = 0b001 << 9; + } +} + +#[cfg(feature = "serde")] +mod modifiers_serde { + use super::ModifiersState; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + #[derive(Default, Serialize, Deserialize)] + #[serde(default)] + #[serde(rename = "ModifiersState")] + pub struct ModifiersStateSerialize { + pub shift_key: bool, + pub control_key: bool, + pub alt_key: bool, + pub super_key: bool, + } + + impl Serialize for ModifiersState { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let s = ModifiersStateSerialize { + shift_key: self.shift_key(), + control_key: self.control_key(), + alt_key: self.alt_key(), + super_key: self.super_key(), + }; + s.serialize(serializer) + } + } + + impl<'de> Deserialize<'de> for ModifiersState { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let ModifiersStateSerialize { + shift_key, + control_key, + alt_key, + super_key, + } = ModifiersStateSerialize::deserialize(deserializer)?; + let mut m = ModifiersState::empty(); + m.set(ModifiersState::SHIFT, shift_key); + m.set(ModifiersState::CONTROL, control_key); + m.set(ModifiersState::ALT, alt_key); + m.set(ModifiersState::SUPER, super_key); + Ok(m) + } + } +} + +/// Contains the platform-native physical key identifier (aka scancode) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum NativeKeyCode { + Unidentified, + Windows(u16), + MacOS(u16), + Gtk(u16), + + /// This is the android "key code" of the event as returned by + /// `KeyEvent.getKeyCode()` + Android(u32), +} + +/// Represents the code of a physical key. +/// +/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.code`] with a few +/// exceptions: +/// - The keys that the specification calls "MetaLeft" and "MetaRight" are named "SuperLeft" and +/// "SuperRight" here. +/// - The key that the specification calls "Super" is reported as `Unidentified` here. +/// - The `Unidentified` variant here, can still identifiy a key through it's `NativeKeyCode`. +/// +/// [`KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum KeyCode { + /// This variant is used when the key cannot be translated to any + /// other variant. + /// + /// The native scancode is provided (if available) in order + /// to allow the user to specify keybindings for keys which + /// are not defined by this API. + Unidentified(NativeKeyCode), + /// ` on a US keyboard. This is also called a backtick or grave. + /// This is the 半角/全角/漢字 + /// (hankaku/zenkaku/kanji) key on Japanese keyboards + Backquote, + /// Used for both the US \\ (on the 101-key layout) and also for the key + /// located between the " and Enter keys on row C of the 102-, + /// 104- and 106-key layouts. + /// Labeled # on a UK (102) keyboard. + Backslash, + /// [ on a US keyboard. + BracketLeft, + /// ] on a US keyboard. + BracketRight, + /// , on a US keyboard. + Comma, + /// 0 on a US keyboard. + Digit0, + /// 1 on a US keyboard. + Digit1, + /// 2 on a US keyboard. + Digit2, + /// 3 on a US keyboard. + Digit3, + /// 4 on a US keyboard. + Digit4, + /// 5 on a US keyboard. + Digit5, + /// 6 on a US keyboard. + Digit6, + /// 7 on a US keyboard. + Digit7, + /// 8 on a US keyboard. + Digit8, + /// 9 on a US keyboard. + Digit9, + /// = on a US keyboard. + Equal, + /// Located between the left Shift and Z keys. + /// Labeled \\ on a UK keyboard. + IntlBackslash, + /// Located between the / and right Shift keys. + /// Labeled \\ (ro) on a Japanese keyboard. + IntlRo, + /// Located between the = and Backspace keys. + /// Labeled ¥ (yen) on a Japanese keyboard. \\ on a + /// Russian keyboard. + IntlYen, + /// a on a US keyboard. + /// Labeled q on an AZERTY (e.g., French) keyboard. + KeyA, + /// b on a US keyboard. + KeyB, + /// c on a US keyboard. + KeyC, + /// d on a US keyboard. + KeyD, + /// e on a US keyboard. + KeyE, + /// f on a US keyboard. + KeyF, + /// g on a US keyboard. + KeyG, + /// h on a US keyboard. + KeyH, + /// i on a US keyboard. + KeyI, + /// j on a US keyboard. + KeyJ, + /// k on a US keyboard. + KeyK, + /// l on a US keyboard. + KeyL, + /// m on a US keyboard. + KeyM, + /// n on a US keyboard. + KeyN, + /// o on a US keyboard. + KeyO, + /// p on a US keyboard. + KeyP, + /// q on a US keyboard. + /// Labeled a on an AZERTY (e.g., French) keyboard. + KeyQ, + /// r on a US keyboard. + KeyR, + /// s on a US keyboard. + KeyS, + /// t on a US keyboard. + KeyT, + /// u on a US keyboard. + KeyU, + /// v on a US keyboard. + KeyV, + /// w on a US keyboard. + /// Labeled z on an AZERTY (e.g., French) keyboard. + KeyW, + /// x on a US keyboard. + KeyX, + /// y on a US keyboard. + /// Labeled z on a QWERTZ (e.g., German) keyboard. + KeyY, + /// z on a US keyboard. + /// Labeled w on an AZERTY (e.g., French) keyboard, and y on a + /// QWERTZ (e.g., German) keyboard. + KeyZ, + /// - on a US keyboard. + Minus, + /// . on a US keyboard. + Period, + /// ' on a US keyboard. + Quote, + /// ; on a US keyboard. + Semicolon, + /// / on a US keyboard. + Slash, + /// Alt, Option, or . + AltLeft, + /// Alt, Option, or . + /// This is labeled AltGr on many keyboard layouts. + AltRight, + /// Backspace or . + /// Labeled Delete on Apple keyboards. + Backspace, + /// CapsLock or + CapsLock, + /// The application context menu key, which is typically found between the right + /// Super key and the right Control key. + ContextMenu, + /// Control or + ControlLeft, + /// Control or + ControlRight, + /// Enter or . Labeled Return on Apple keyboards. + Enter, + /// The Windows, , Command, or other OS symbol key. + SuperLeft, + /// The Windows, , Command, or other OS symbol key. + SuperRight, + /// Shift or + ShiftLeft, + /// Shift or + ShiftRight, + /// (space) + Space, + /// Tab or + Tab, + /// Japanese: (henkan) + Convert, + /// Japanese: カタカナ/ひらがな/ローマ字 (katakana/hiragana/romaji) + KanaMode, + /// Korean: HangulMode 한/영 (han/yeong) + /// + /// Japanese (Mac keyboard): (kana) + Lang1, + /// Korean: Hanja (hanja) + /// + /// Japanese (Mac keyboard): (eisu) + Lang2, + /// Japanese (word-processing keyboard): Katakana + Lang3, + /// Japanese (word-processing keyboard): Hiragana + Lang4, + /// Japanese (word-processing keyboard): Zenkaku/Hankaku + Lang5, + /// Japanese: 無変換 (muhenkan) + NonConvert, + /// . The forward delete key. + /// Note that on Apple keyboards, the key labelled Delete on the main part of + /// the keyboard is encoded as [`Backspace`]. + /// + /// [`Backspace`]: Self::Backspace + Delete, + /// Page Down, End, or + End, + /// Help. Not present on standard PC keyboards. + Help, + /// Home or + Home, + /// Insert or Ins. Not present on Apple keyboards. + Insert, + /// Page Down, PgDn, or + PageDown, + /// Page Up, PgUp, or + PageUp, + /// + ArrowDown, + /// + ArrowLeft, + /// + ArrowRight, + /// + ArrowUp, + /// On the Mac, this is used for the numpad Clear key. + NumLock, + /// 0 Ins on a keyboard. 0 on a phone or remote control + Numpad0, + /// 1 End on a keyboard. 1 or 1 QZ on a phone or remote control + Numpad1, + /// 2 ↓ on a keyboard. 2 ABC on a phone or remote control + Numpad2, + /// 3 PgDn on a keyboard. 3 DEF on a phone or remote control + Numpad3, + /// 4 ← on a keyboard. 4 GHI on a phone or remote control + Numpad4, + /// 5 on a keyboard. 5 JKL on a phone or remote control + Numpad5, + /// 6 → on a keyboard. 6 MNO on a phone or remote control + Numpad6, + /// 7 Home on a keyboard. 7 PQRS or 7 PRS on a phone + /// or remote control + Numpad7, + /// 8 ↑ on a keyboard. 8 TUV on a phone or remote control + Numpad8, + /// 9 PgUp on a keyboard. 9 WXYZ or 9 WXY on a phone + /// or remote control + Numpad9, + /// + + NumpadAdd, + /// Found on the Microsoft Natural Keyboard. + NumpadBackspace, + /// C or A (All Clear). Also for use with numpads that have a + /// Clear key that is separate from the NumLock key. On the Mac, the + /// numpad Clear key is encoded as [`NumLock`]. + /// + /// [`NumLock`]: Self::NumLock + NumpadClear, + /// C (Clear Entry) + NumpadClearEntry, + /// , (thousands separator). For locales where the thousands separator + /// is a "." (e.g., Brazil), this key may generate a .. + NumpadComma, + /// . Del. For locales where the decimal separator is "," (e.g., + /// Brazil), this key may generate a ,. + NumpadDecimal, + /// / + NumpadDivide, + NumpadEnter, + /// = + NumpadEqual, + /// # on a phone or remote control device. This key is typically found + /// below the 9 key and to the right of the 0 key. + NumpadHash, + /// M Add current entry to the value stored in memory. + NumpadMemoryAdd, + /// M Clear the value stored in memory. + NumpadMemoryClear, + /// M Replace the current entry with the value stored in memory. + NumpadMemoryRecall, + /// M Replace the value stored in memory with the current entry. + NumpadMemoryStore, + /// M Subtract current entry from the value stored in memory. + NumpadMemorySubtract, + /// * on a keyboard. For use with numpads that provide mathematical + /// operations (+, - * and /). + /// + /// Use `NumpadStar` for the * key on phones and remote controls. + NumpadMultiply, + /// ( Found on the Microsoft Natural Keyboard. + NumpadParenLeft, + /// ) Found on the Microsoft Natural Keyboard. + NumpadParenRight, + /// * on a phone or remote control device. + /// + /// This key is typically found below the 7 key and to the left of + /// the 0 key. + /// + /// Use "NumpadMultiply" for the * key on + /// numeric keypads. + NumpadStar, + /// - + NumpadSubtract, + /// Esc or + Escape, + /// Fn This is typically a hardware key that does not generate a separate code. + Fn, + /// FLock or FnLock. Function Lock key. Found on the Microsoft + /// Natural Keyboard. + FnLock, + /// PrtScr SysRq or Print Screen + PrintScreen, + /// Scroll Lock + ScrollLock, + /// Pause Break + Pause, + /// Some laptops place this key to the left of the key. + /// + /// This also the "back" button (triangle) on Android. + BrowserBack, + BrowserFavorites, + /// Some laptops place this key to the right of the key. + BrowserForward, + /// The "home" button on Android. + BrowserHome, + BrowserRefresh, + BrowserSearch, + BrowserStop, + /// Eject or . This key is placed in the function section on some Apple + /// keyboards. + Eject, + /// Sometimes labelled My Computer on the keyboard + LaunchApp1, + /// Sometimes labelled Calculator on the keyboard + LaunchApp2, + LaunchMail, + MediaPlayPause, + MediaSelect, + MediaStop, + MediaTrackNext, + MediaTrackPrevious, + /// This key is placed in the function section on some Apple keyboards, replacing the + /// Eject key. + Power, + Sleep, + AudioVolumeDown, + AudioVolumeMute, + AudioVolumeUp, + WakeUp, + Hyper, + Turbo, + Abort, + Resume, + Suspend, + /// Found on Sun’s USB keyboard. + Again, + /// Found on Sun’s USB keyboard. + Copy, + /// Found on Sun’s USB keyboard. + Cut, + /// Found on Sun’s USB keyboard. + Find, + /// Found on Sun’s USB keyboard. + Open, + /// Found on Sun’s USB keyboard. + Paste, + /// Found on Sun’s USB keyboard. + Props, + /// Found on Sun’s USB keyboard. + Select, + /// Found on Sun’s USB keyboard. + Undo, + /// Use for dedicated ひらがな key found on some Japanese word processing keyboards. + Hiragana, + /// Use for dedicated カタカナ key found on some Japanese word processing keyboards. + Katakana, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F1, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F2, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F3, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F4, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F5, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F6, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F7, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F8, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F9, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F10, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F11, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F12, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F13, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F14, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F15, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F16, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F17, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F18, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F19, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F20, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F21, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F22, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F23, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F24, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, +} + +impl KeyCode { + /// Return platform specific scancode. + pub fn to_scancode(self) -> Option { + platform_keycode_to_scancode(self) + } + /// Return `KeyCode` from platform scancode. + pub fn from_scancode(scancode: u32) -> KeyCode { + platform_keycode_from_scancode(scancode) + } +} + +impl FromStr for KeyCode { + type Err = OsError; + fn from_str(accelerator_string: &str) -> Result { + let keycode = match accelerator_string.to_uppercase().as_str() { + "BACKQUOTE" => KeyCode::Backquote, + "BACKSLASH" => KeyCode::Backslash, + "BRACKETLEFT" => KeyCode::BracketLeft, + "BRACKETRIGHT" => KeyCode::BracketRight, + "COMMA" => KeyCode::Comma, + "0" => KeyCode::Digit0, + "1" => KeyCode::Digit1, + "2" => KeyCode::Digit2, + "3" => KeyCode::Digit3, + "4" => KeyCode::Digit4, + "5" => KeyCode::Digit5, + "6" => KeyCode::Digit6, + "7" => KeyCode::Digit7, + "8" => KeyCode::Digit8, + "9" => KeyCode::Digit9, + "NUMPAD0" => KeyCode::Numpad0, + "NUMPAD1" => KeyCode::Numpad1, + "NUMPAD2" => KeyCode::Numpad2, + "NUMPAD3" => KeyCode::Numpad3, + "NUMPAD4" => KeyCode::Numpad4, + "NUMPAD5" => KeyCode::Numpad5, + "NUMPAD6" => KeyCode::Numpad6, + "NUMPAD7" => KeyCode::Numpad7, + "NUMPAD8" => KeyCode::Numpad8, + "NUMPAD9" => KeyCode::Numpad9, + "=" => KeyCode::Equal, + "-" => KeyCode::Minus, + "PERIOD" => KeyCode::Period, + "QUOTE" => KeyCode::Quote, + "\\" => KeyCode::IntlBackslash, + "A" => KeyCode::KeyA, + "B" => KeyCode::KeyB, + "C" => KeyCode::KeyC, + "D" => KeyCode::KeyD, + "E" => KeyCode::KeyE, + "F" => KeyCode::KeyF, + "G" => KeyCode::KeyG, + "H" => KeyCode::KeyH, + "I" => KeyCode::KeyI, + "J" => KeyCode::KeyJ, + "K" => KeyCode::KeyK, + "L" => KeyCode::KeyL, + "M" => KeyCode::KeyM, + "N" => KeyCode::KeyN, + "O" => KeyCode::KeyO, + "P" => KeyCode::KeyP, + "Q" => KeyCode::KeyQ, + "R" => KeyCode::KeyR, + "S" => KeyCode::KeyS, + "T" => KeyCode::KeyT, + "U" => KeyCode::KeyU, + "V" => KeyCode::KeyV, + "W" => KeyCode::KeyW, + "X" => KeyCode::KeyX, + "Y" => KeyCode::KeyY, + "Z" => KeyCode::KeyZ, + + "SEMICOLON" => KeyCode::Semicolon, + "SLASH" => KeyCode::Slash, + "BACKSPACE" => KeyCode::Backspace, + "CAPSLOCK" => KeyCode::CapsLock, + "CONTEXTMENU" => KeyCode::ContextMenu, + "ENTER" => KeyCode::Enter, + "SPACE" => KeyCode::Space, + "TAB" => KeyCode::Tab, + "CONVERT" => KeyCode::Convert, + + "DELETE" => KeyCode::Delete, + "END" => KeyCode::End, + "HELP" => KeyCode::Help, + "HOME" => KeyCode::Home, + "PAGEDOWN" => KeyCode::PageDown, + "PAGEUP" => KeyCode::PageUp, + + "DOWN" => KeyCode::ArrowDown, + "UP" => KeyCode::ArrowUp, + "LEFT" => KeyCode::ArrowLeft, + "RIGHT" => KeyCode::ArrowRight, + + "NUMLOCK" => KeyCode::NumLock, + "NUMPADADD" => KeyCode::NumpadAdd, + "NUMPADBACKSPACE" => KeyCode::NumpadBackspace, + "NUMPADCLEAR" => KeyCode::NumpadClear, + "NUMPADCOMMA" => KeyCode::NumpadComma, + "NUMPADDIVIDE" => KeyCode::NumpadDivide, + "NUMPADSUBSTRACT" => KeyCode::NumpadSubtract, + "NUMPADENTER" => KeyCode::NumpadEnter, + + "ESCAPE" => KeyCode::Escape, + "ESC" => KeyCode::Escape, + "FN" => KeyCode::Fn, + "FNLOCK" => KeyCode::FnLock, + "PRINTSCREEN" => KeyCode::PrintScreen, + "SCROLLLOCK" => KeyCode::ScrollLock, + + "PAUSE" => KeyCode::Pause, + + "VOLUMEMUTE" => KeyCode::AudioVolumeMute, + "VOLUMEDOWN" => KeyCode::AudioVolumeDown, + "VOLUMEUP" => KeyCode::AudioVolumeUp, + "MEDIANEXTTRACK" => KeyCode::MediaTrackNext, + "MEDIAPREVIOUSTRACK" => KeyCode::MediaTrackPrevious, + "MEDIAPLAYPAUSE" => KeyCode::MediaPlayPause, + "LAUNCHMAIL" => KeyCode::LaunchMail, + + "SUSPEND" => KeyCode::Suspend, + "F1" => KeyCode::F1, + "F2" => KeyCode::F2, + "F3" => KeyCode::F3, + "F4" => KeyCode::F4, + "F5" => KeyCode::F5, + "F6" => KeyCode::F6, + "F7" => KeyCode::F7, + "F8" => KeyCode::F8, + "F9" => KeyCode::F9, + "F10" => KeyCode::F10, + "F11" => KeyCode::F11, + "F12" => KeyCode::F12, + "F13" => KeyCode::F13, + "F14" => KeyCode::F14, + "F15" => KeyCode::F15, + "F16" => KeyCode::F16, + "F17" => KeyCode::F17, + "F18" => KeyCode::F18, + "F19" => KeyCode::F19, + "F20" => KeyCode::F20, + "F21" => KeyCode::F21, + "F22" => KeyCode::F22, + "F23" => KeyCode::F23, + "F24" => KeyCode::F24, + "F25" => KeyCode::F25, + "F26" => KeyCode::F26, + "F27" => KeyCode::F27, + "F28" => KeyCode::F28, + "F29" => KeyCode::F29, + "F30" => KeyCode::F30, + "F31" => KeyCode::F31, + "F32" => KeyCode::F32, + "F33" => KeyCode::F33, + "F34" => KeyCode::F34, + "F35" => KeyCode::F35, + _ => KeyCode::Unidentified(NativeKeyCode::Unidentified), + }; + + Ok(keycode) + } +} + +impl fmt::Display for KeyCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + &KeyCode::Unidentified(_) => write!(f, "{:?}", "Unidentified"), + val => write!(f, "{:?}", val), + } + } +} + +/// Key represents the meaning of a keypress. +/// +/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.key`] with a few +/// exceptions: +/// - The `Super` variant here, is named `Meta` in the aforementioned specification. (There's +/// another key which the specification calls `Super`. That does not exist here.) +/// - The `Space` variant here, can be identified by the character it generates in the +/// specificaiton. +/// - The `Unidentified` variant here, can still identifiy a key through it's `NativeKeyCode`. +/// - The `Dead` variant here, can specify the character which is inserted when pressing the +/// dead-key twice. +/// +/// [`KeyboardEvent.key`]: https://w3c.github.io/uievents-key/ +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Key<'a> { + /// A key string that corresponds to the character typed by the user, taking into account the + /// user’s current locale setting, and any system-level keyboard mapping overrides that are in + /// effect. + Character(&'a str), + + /// This variant is used when the key cannot be translated to any other variant. + /// + /// The native scancode is provided (if available) in order to allow the user to specify + /// keybindings for keys which are not defined by this API. + Unidentified(NativeKeyCode), + + /// Contains the text representation of the dead-key when available. + /// + /// ## Platform-specific + /// - **Web:** Always contains `None` + Dead(Option), + + /// The `Alt` (Alternative) key. + /// + /// This key enables the alternate modifier function for interpreting concurrent or subsequent + /// keyboard input. This key value is also used for the Apple Option key. + Alt, + /// The Alternate Graphics (AltGr or AltGraph) key. + /// + /// This key is used enable the ISO Level 3 shift modifier (the standard `Shift` key is the + /// level 2 modifier). + AltGraph, + /// The `Caps Lock` (Capital) key. + /// + /// Toggle capital character lock function for interpreting subsequent keyboard input event. + CapsLock, + /// The `Control` or `Ctrl` key. + /// + /// Used to enable control modifier function for interpreting concurrent or subsequent keyboard + /// input. + Control, + /// The Function switch `Fn` key. Activating this key simultaneously with another key changes + /// that key’s value to an alternate character or function. This key is often handled directly + /// in the keyboard hardware and does not usually generate key events. + Fn, + /// The Function-Lock (`FnLock` or `F-Lock`) key. Activating this key switches the mode of the + /// keyboard to changes some keys' values to an alternate character or function. This key is + /// often handled directly in the keyboard hardware and does not usually generate key events. + FnLock, + /// The `NumLock` or Number Lock key. Used to toggle numpad mode function for interpreting + /// subsequent keyboard input. + NumLock, + /// Toggle between scrolling and cursor movement modes. + ScrollLock, + /// Used to enable shift modifier function for interpreting concurrent or subsequent keyboard + /// input. + Shift, + /// The Symbol modifier key (used on some virtual keyboards). + Symbol, + SymbolLock, + Hyper, + /// Used to enable "super" modifier function for interpreting concurrent or subsequent keyboard + /// input. This key value is used for the "Windows Logo" key and the Apple `Command` or `⌘` key. + /// + /// Note: In some contexts (e.g. the Web) this is referred to as the "Meta" key. + Super, + /// The `Enter` or `↵` key. Used to activate current selection or accept current input. This key + /// value is also used for the `Return` (Macintosh numpad) key. This key value is also used for + /// the Android `KEYCODE_DPAD_CENTER`. + Enter, + /// The Horizontal Tabulation `Tab` key. + Tab, + /// Used in text to insert a space between words. Usually located below the character keys. + Space, + /// Navigate or traverse downward. (`KEYCODE_DPAD_DOWN`) + ArrowDown, + /// Navigate or traverse leftward. (`KEYCODE_DPAD_LEFT`) + ArrowLeft, + /// Navigate or traverse rightward. (`KEYCODE_DPAD_RIGHT`) + ArrowRight, + /// Navigate or traverse upward. (`KEYCODE_DPAD_UP`) + ArrowUp, + /// The End key, used with keyboard entry to go to the end of content (`KEYCODE_MOVE_END`). + End, + /// The Home key, used with keyboard entry, to go to start of content (`KEYCODE_MOVE_HOME`). + /// For the mobile phone `Home` key (which goes to the phone’s main screen), use [`GoHome`]. + /// + /// [`GoHome`]: Self::GoHome + Home, + /// Scroll down or display next page of content. + PageDown, + /// Scroll up or display previous page of content. + PageUp, + /// Used to remove the character to the left of the cursor. This key value is also used for + /// the key labeled `Delete` on MacOS keyboards. + Backspace, + /// Remove the currently selected input. + Clear, + /// Copy the current selection. (`APPCOMMAND_COPY`) + Copy, + /// The Cursor Select key. + CrSel, + /// Cut the current selection. (`APPCOMMAND_CUT`) + Cut, + /// Used to delete the character to the right of the cursor. This key value is also used for the + /// key labeled `Delete` on MacOS keyboards when `Fn` is active. + Delete, + /// The Erase to End of Field key. This key deletes all characters from the current cursor + /// position to the end of the current field. + EraseEof, + /// The Extend Selection (Exsel) key. + ExSel, + /// Toggle between text modes for insertion or overtyping. + /// (`KEYCODE_INSERT`) + Insert, + /// The Paste key. (`APPCOMMAND_PASTE`) + Paste, + /// Redo the last action. (`APPCOMMAND_REDO`) + Redo, + /// Undo the last action. (`APPCOMMAND_UNDO`) + Undo, + /// The Accept (Commit, OK) key. Accept current option or input method sequence conversion. + Accept, + /// Redo or repeat an action. + Again, + /// The Attention (Attn) key. + Attn, + Cancel, + /// Show the application’s context menu. + /// This key is commonly found between the right `Super` key and the right `Control` key. + ContextMenu, + /// The `Esc` key. This key was originally used to initiate an escape sequence, but is + /// now more generally used to exit or "escape" the current context, such as closing a dialog + /// or exiting full screen mode. + Escape, + Execute, + /// Open the Find dialog. (`APPCOMMAND_FIND`) + Find, + /// Open a help dialog or toggle display of help information. (`APPCOMMAND_HELP`, + /// `KEYCODE_HELP`) + Help, + /// Pause the current state or application (as appropriate). + /// + /// Note: Do not use this value for the `Pause` button on media controllers. Use `"MediaPause"` + /// instead. + Pause, + /// Play or resume the current state or application (as appropriate). + /// + /// Note: Do not use this value for the `Play` button on media controllers. Use `"MediaPlay"` + /// instead. + Play, + /// The properties (Props) key. + Props, + Select, + /// The ZoomIn key. (`KEYCODE_ZOOM_IN`) + ZoomIn, + /// The ZoomOut key. (`KEYCODE_ZOOM_OUT`) + ZoomOut, + /// The Brightness Down key. Typically controls the display brightness. + /// (`KEYCODE_BRIGHTNESS_DOWN`) + BrightnessDown, + /// The Brightness Up key. Typically controls the display brightness. (`KEYCODE_BRIGHTNESS_UP`) + BrightnessUp, + /// Toggle removable media to eject (open) and insert (close) state. (`KEYCODE_MEDIA_EJECT`) + Eject, + LogOff, + /// Toggle power state. (`KEYCODE_POWER`) + /// Note: Note: Some devices might not expose this key to the operating environment. + Power, + /// The `PowerOff` key. Sometime called `PowerDown`. + PowerOff, + /// Initiate print-screen function. + PrintScreen, + /// The Hibernate key. This key saves the current state of the computer to disk so that it can + /// be restored. The computer will then shutdown. + Hibernate, + /// The Standby key. This key turns off the display and places the computer into a low-power + /// mode without completely shutting down. It is sometimes labelled `Suspend` or `Sleep` key. + /// (`KEYCODE_SLEEP`) + Standby, + /// The WakeUp key. (`KEYCODE_WAKEUP`) + WakeUp, + /// Initate the multi-candidate mode. + AllCandidates, + Alphanumeric, + /// Initiate the Code Input mode to allow characters to be entered by + /// their code points. + CodeInput, + /// The Compose key, also known as "Multi_key" on the X Window System. This key acts in a + /// manner similar to a dead key, triggering a mode where subsequent key presses are combined to + /// produce a different character. + Compose, + /// Convert the current input method sequence. + Convert, + /// The Final Mode `Final` key used on some Asian keyboards, to enable the final mode for IMEs. + FinalMode, + /// Switch to the first character group. (ISO/IEC 9995) + GroupFirst, + /// Switch to the last character group. (ISO/IEC 9995) + GroupLast, + /// Switch to the next character group. (ISO/IEC 9995) + GroupNext, + /// Switch to the previous character group. (ISO/IEC 9995) + GroupPrevious, + /// Toggle between or cycle through input modes of IMEs. + ModeChange, + NextCandidate, + /// Accept current input method sequence without + /// conversion in IMEs. + NonConvert, + PreviousCandidate, + Process, + SingleCandidate, + /// Toggle between Hangul and English modes. + HangulMode, + HanjaMode, + JunjaMode, + /// The Eisu key. This key may close the IME, but its purpose is defined by the current IME. + /// (`KEYCODE_EISU`) + Eisu, + /// The (Half-Width) Characters key. + Hankaku, + /// The Hiragana (Japanese Kana characters) key. + Hiragana, + /// The Hiragana/Katakana toggle key. (`KEYCODE_KATAKANA_HIRAGANA`) + HiraganaKatakana, + /// The Kana Mode (Kana Lock) key. This key is used to enter hiragana mode (typically from + /// romaji mode). + KanaMode, + /// The Kanji (Japanese name for ideographic characters of Chinese origin) Mode key. This key is + /// typically used to switch to a hiragana keyboard for the purpose of converting input into + /// kanji. (`KEYCODE_KANA`) + KanjiMode, + /// The Katakana (Japanese Kana characters) key. + Katakana, + /// The Roman characters function key. + Romaji, + /// The Zenkaku (Full-Width) Characters key. + Zenkaku, + /// The Zenkaku/Hankaku (full-width/half-width) toggle key. (`KEYCODE_ZENKAKU_HANKAKU`) + ZenkakuHankaku, + /// General purpose virtual function key, as index 1. + Soft1, + /// General purpose virtual function key, as index 2. + Soft2, + /// General purpose virtual function key, as index 3. + Soft3, + /// General purpose virtual function key, as index 4. + Soft4, + /// Select next (numerically or logically) lower channel. (`APPCOMMAND_MEDIA_CHANNEL_DOWN`, + /// `KEYCODE_CHANNEL_DOWN`) + ChannelDown, + /// Select next (numerically or logically) higher channel. (`APPCOMMAND_MEDIA_CHANNEL_UP`, + /// `KEYCODE_CHANNEL_UP`) + ChannelUp, + /// Close the current document or message (Note: This doesn’t close the application). + /// (`APPCOMMAND_CLOSE`) + Close, + /// Open an editor to forward the current message. (`APPCOMMAND_FORWARD_MAIL`) + MailForward, + /// Open an editor to reply to the current message. (`APPCOMMAND_REPLY_TO_MAIL`) + MailReply, + /// Send the current message. (`APPCOMMAND_SEND_MAIL`) + MailSend, + /// Close the current media, for example to close a CD or DVD tray. (`KEYCODE_MEDIA_CLOSE`) + MediaClose, + /// Initiate or continue forward playback at faster than normal speed, or increase speed if + /// already fast forwarding. (`APPCOMMAND_MEDIA_FAST_FORWARD`, `KEYCODE_MEDIA_FAST_FORWARD`) + MediaFastForward, + /// Pause the currently playing media. (`APPCOMMAND_MEDIA_PAUSE`, `KEYCODE_MEDIA_PAUSE`) + /// + /// Note: Media controller devices should use this value rather than `"Pause"` for their pause + /// keys. + MediaPause, + /// Initiate or continue media playback at normal speed, if not currently playing at normal + /// speed. (`APPCOMMAND_MEDIA_PLAY`, `KEYCODE_MEDIA_PLAY`) + MediaPlay, + /// Toggle media between play and pause states. (`APPCOMMAND_MEDIA_PLAY_PAUSE`, + /// `KEYCODE_MEDIA_PLAY_PAUSE`) + MediaPlayPause, + /// Initiate or resume recording of currently selected media. (`APPCOMMAND_MEDIA_RECORD`, + /// `KEYCODE_MEDIA_RECORD`) + MediaRecord, + /// Initiate or continue reverse playback at faster than normal speed, or increase speed if + /// already rewinding. (`APPCOMMAND_MEDIA_REWIND`, `KEYCODE_MEDIA_REWIND`) + MediaRewind, + /// Stop media playing, pausing, forwarding, rewinding, or recording, if not already stopped. + /// (`APPCOMMAND_MEDIA_STOP`, `KEYCODE_MEDIA_STOP`) + MediaStop, + /// Seek to next media or program track. (`APPCOMMAND_MEDIA_NEXTTRACK`, `KEYCODE_MEDIA_NEXT`) + MediaTrackNext, + /// Seek to previous media or program track. (`APPCOMMAND_MEDIA_PREVIOUSTRACK`, + /// `KEYCODE_MEDIA_PREVIOUS`) + MediaTrackPrevious, + /// Open a new document or message. (`APPCOMMAND_NEW`) + New, + /// Open an existing document or message. (`APPCOMMAND_OPEN`) + Open, + /// Print the current document or message. (`APPCOMMAND_PRINT`) + Print, + /// Save the current document or message. (`APPCOMMAND_SAVE`) + Save, + /// Spellcheck the current document or selection. (`APPCOMMAND_SPELL_CHECK`) + SpellCheck, + /// The `11` key found on media numpads that + /// have buttons from `1` ... `12`. + Key11, + /// The `12` key found on media numpads that + /// have buttons from `1` ... `12`. + Key12, + /// Adjust audio balance leftward. (`VK_AUDIO_BALANCE_LEFT`) + AudioBalanceLeft, + /// Adjust audio balance rightward. (`VK_AUDIO_BALANCE_RIGHT`) + AudioBalanceRight, + /// Decrease audio bass boost or cycle down through bass boost states. (`APPCOMMAND_BASS_DOWN`, + /// `VK_BASS_BOOST_DOWN`) + AudioBassBoostDown, + /// Toggle bass boost on/off. (`APPCOMMAND_BASS_BOOST`) + AudioBassBoostToggle, + /// Increase audio bass boost or cycle up through bass boost states. (`APPCOMMAND_BASS_UP`, + /// `VK_BASS_BOOST_UP`) + AudioBassBoostUp, + /// Adjust audio fader towards front. (`VK_FADER_FRONT`) + AudioFaderFront, + /// Adjust audio fader towards rear. (`VK_FADER_REAR`) + AudioFaderRear, + /// Advance surround audio mode to next available mode. (`VK_SURROUND_MODE_NEXT`) + AudioSurroundModeNext, + /// Decrease treble. (`APPCOMMAND_TREBLE_DOWN`) + AudioTrebleDown, + /// Increase treble. (`APPCOMMAND_TREBLE_UP`) + AudioTrebleUp, + /// Decrease audio volume. (`APPCOMMAND_VOLUME_DOWN`, `KEYCODE_VOLUME_DOWN`) + AudioVolumeDown, + /// Increase audio volume. (`APPCOMMAND_VOLUME_UP`, `KEYCODE_VOLUME_UP`) + AudioVolumeUp, + /// Toggle between muted state and prior volume level. (`APPCOMMAND_VOLUME_MUTE`, + /// `KEYCODE_VOLUME_MUTE`) + AudioVolumeMute, + /// Toggle the microphone on/off. (`APPCOMMAND_MIC_ON_OFF_TOGGLE`) + MicrophoneToggle, + /// Decrease microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_DOWN`) + MicrophoneVolumeDown, + /// Increase microphone volume. (`APPCOMMAND_MICROPHONE_VOLUME_UP`) + MicrophoneVolumeUp, + /// Mute the microphone. (`APPCOMMAND_MICROPHONE_VOLUME_MUTE`, `KEYCODE_MUTE`) + MicrophoneVolumeMute, + /// Show correction list when a word is incorrectly identified. (`APPCOMMAND_CORRECTION_LIST`) + SpeechCorrectionList, + /// Toggle between dictation mode and command/control mode. + /// (`APPCOMMAND_DICTATE_OR_COMMAND_CONTROL_TOGGLE`) + SpeechInputToggle, + /// The first generic "LaunchApplication" key. This is commonly associated with launching "My + /// Computer", and may have a computer symbol on the key. (`APPCOMMAND_LAUNCH_APP1`) + LaunchApplication1, + /// The second generic "LaunchApplication" key. This is commonly associated with launching + /// "Calculator", and may have a calculator symbol on the key. (`APPCOMMAND_LAUNCH_APP2`, + /// `KEYCODE_CALCULATOR`) + LaunchApplication2, + /// The "Calendar" key. (`KEYCODE_CALENDAR`) + LaunchCalendar, + /// The "Contacts" key. (`KEYCODE_CONTACTS`) + LaunchContacts, + /// The "Mail" key. (`APPCOMMAND_LAUNCH_MAIL`) + LaunchMail, + /// The "Media Player" key. (`APPCOMMAND_LAUNCH_MEDIA_SELECT`) + LaunchMediaPlayer, + LaunchMusicPlayer, + LaunchPhone, + LaunchScreenSaver, + LaunchSpreadsheet, + LaunchWebBrowser, + LaunchWebCam, + LaunchWordProcessor, + /// Navigate to previous content or page in current history. (`APPCOMMAND_BROWSER_BACKWARD`) + BrowserBack, + /// Open the list of browser favorites. (`APPCOMMAND_BROWSER_FAVORITES`) + BrowserFavorites, + /// Navigate to next content or page in current history. (`APPCOMMAND_BROWSER_FORWARD`) + BrowserForward, + /// Go to the user’s preferred home page. (`APPCOMMAND_BROWSER_HOME`) + BrowserHome, + /// Refresh the current page or content. (`APPCOMMAND_BROWSER_REFRESH`) + BrowserRefresh, + /// Call up the user’s preferred search page. (`APPCOMMAND_BROWSER_SEARCH`) + BrowserSearch, + /// Stop loading the current page or content. (`APPCOMMAND_BROWSER_STOP`) + BrowserStop, + /// The Application switch key, which provides a list of recent apps to switch between. + /// (`KEYCODE_APP_SWITCH`) + AppSwitch, + /// The Call key. (`KEYCODE_CALL`) + Call, + /// The Camera key. (`KEYCODE_CAMERA`) + Camera, + /// The Camera focus key. (`KEYCODE_FOCUS`) + CameraFocus, + /// The End Call key. (`KEYCODE_ENDCALL`) + EndCall, + /// The Back key. (`KEYCODE_BACK`) + GoBack, + /// The Home key, which goes to the phone’s main screen. (`KEYCODE_HOME`) + GoHome, + /// The Headset Hook key. (`KEYCODE_HEADSETHOOK`) + HeadsetHook, + LastNumberRedial, + /// The Notification key. (`KEYCODE_NOTIFICATION`) + Notification, + /// Toggle between manner mode state: silent, vibrate, ring, ... (`KEYCODE_MANNER_MODE`) + MannerMode, + VoiceDial, + /// Switch to viewing TV. (`KEYCODE_TV`) + TV, + /// TV 3D Mode. (`KEYCODE_3D_MODE`) + TV3DMode, + /// Toggle between antenna and cable input. (`KEYCODE_TV_ANTENNA_CABLE`) + TVAntennaCable, + /// Audio description. (`KEYCODE_TV_AUDIO_DESCRIPTION`) + TVAudioDescription, + /// Audio description mixing volume down. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN`) + TVAudioDescriptionMixDown, + /// Audio description mixing volume up. (`KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP`) + TVAudioDescriptionMixUp, + /// Contents menu. (`KEYCODE_TV_CONTENTS_MENU`) + TVContentsMenu, + /// Contents menu. (`KEYCODE_TV_DATA_SERVICE`) + TVDataService, + /// Switch the input mode on an external TV. (`KEYCODE_TV_INPUT`) + TVInput, + /// Switch to component input #1. (`KEYCODE_TV_INPUT_COMPONENT_1`) + TVInputComponent1, + /// Switch to component input #2. (`KEYCODE_TV_INPUT_COMPONENT_2`) + TVInputComponent2, + /// Switch to composite input #1. (`KEYCODE_TV_INPUT_COMPOSITE_1`) + TVInputComposite1, + /// Switch to composite input #2. (`KEYCODE_TV_INPUT_COMPOSITE_2`) + TVInputComposite2, + /// Switch to HDMI input #1. (`KEYCODE_TV_INPUT_HDMI_1`) + TVInputHDMI1, + /// Switch to HDMI input #2. (`KEYCODE_TV_INPUT_HDMI_2`) + TVInputHDMI2, + /// Switch to HDMI input #3. (`KEYCODE_TV_INPUT_HDMI_3`) + TVInputHDMI3, + /// Switch to HDMI input #4. (`KEYCODE_TV_INPUT_HDMI_4`) + TVInputHDMI4, + /// Switch to VGA input #1. (`KEYCODE_TV_INPUT_VGA_1`) + TVInputVGA1, + /// Media context menu. (`KEYCODE_TV_MEDIA_CONTEXT_MENU`) + TVMediaContext, + /// Toggle network. (`KEYCODE_TV_NETWORK`) + TVNetwork, + /// Number entry. (`KEYCODE_TV_NUMBER_ENTRY`) + TVNumberEntry, + /// Toggle the power on an external TV. (`KEYCODE_TV_POWER`) + TVPower, + /// Radio. (`KEYCODE_TV_RADIO_SERVICE`) + TVRadioService, + /// Satellite. (`KEYCODE_TV_SATELLITE`) + TVSatellite, + /// Broadcast Satellite. (`KEYCODE_TV_SATELLITE_BS`) + TVSatelliteBS, + /// Communication Satellite. (`KEYCODE_TV_SATELLITE_CS`) + TVSatelliteCS, + /// Toggle between available satellites. (`KEYCODE_TV_SATELLITE_SERVICE`) + TVSatelliteToggle, + /// Analog Terrestrial. (`KEYCODE_TV_TERRESTRIAL_ANALOG`) + TVTerrestrialAnalog, + /// Digital Terrestrial. (`KEYCODE_TV_TERRESTRIAL_DIGITAL`) + TVTerrestrialDigital, + /// Timer programming. (`KEYCODE_TV_TIMER_PROGRAMMING`) + TVTimer, + /// Switch the input mode on an external AVR (audio/video receiver). (`KEYCODE_AVR_INPUT`) + AVRInput, + /// Toggle the power on an external AVR (audio/video receiver). (`KEYCODE_AVR_POWER`) + AVRPower, + /// General purpose color-coded media function key, as index 0 (red). (`VK_COLORED_KEY_0`, + /// `KEYCODE_PROG_RED`) + ColorF0Red, + /// General purpose color-coded media function key, as index 1 (green). (`VK_COLORED_KEY_1`, + /// `KEYCODE_PROG_GREEN`) + ColorF1Green, + /// General purpose color-coded media function key, as index 2 (yellow). (`VK_COLORED_KEY_2`, + /// `KEYCODE_PROG_YELLOW`) + ColorF2Yellow, + /// General purpose color-coded media function key, as index 3 (blue). (`VK_COLORED_KEY_3`, + /// `KEYCODE_PROG_BLUE`) + ColorF3Blue, + /// General purpose color-coded media function key, as index 4 (grey). (`VK_COLORED_KEY_4`) + ColorF4Grey, + /// General purpose color-coded media function key, as index 5 (brown). (`VK_COLORED_KEY_5`) + ColorF5Brown, + /// Toggle the display of Closed Captions. (`VK_CC`, `KEYCODE_CAPTIONS`) + ClosedCaptionToggle, + /// Adjust brightness of device, by toggling between or cycling through states. (`VK_DIMMER`) + Dimmer, + /// Swap video sources. (`VK_DISPLAY_SWAP`) + DisplaySwap, + /// Select Digital Video Rrecorder. (`KEYCODE_DVR`) + DVR, + /// Exit the current application. (`VK_EXIT`) + Exit, + /// Clear program or content stored as favorite 0. (`VK_CLEAR_FAVORITE_0`) + FavoriteClear0, + /// Clear program or content stored as favorite 1. (`VK_CLEAR_FAVORITE_1`) + FavoriteClear1, + /// Clear program or content stored as favorite 2. (`VK_CLEAR_FAVORITE_2`) + FavoriteClear2, + /// Clear program or content stored as favorite 3. (`VK_CLEAR_FAVORITE_3`) + FavoriteClear3, + /// Select (recall) program or content stored as favorite 0. (`VK_RECALL_FAVORITE_0`) + FavoriteRecall0, + /// Select (recall) program or content stored as favorite 1. (`VK_RECALL_FAVORITE_1`) + FavoriteRecall1, + /// Select (recall) program or content stored as favorite 2. (`VK_RECALL_FAVORITE_2`) + FavoriteRecall2, + /// Select (recall) program or content stored as favorite 3. (`VK_RECALL_FAVORITE_3`) + FavoriteRecall3, + /// Store current program or content as favorite 0. (`VK_STORE_FAVORITE_0`) + FavoriteStore0, + /// Store current program or content as favorite 1. (`VK_STORE_FAVORITE_1`) + FavoriteStore1, + /// Store current program or content as favorite 2. (`VK_STORE_FAVORITE_2`) + FavoriteStore2, + /// Store current program or content as favorite 3. (`VK_STORE_FAVORITE_3`) + FavoriteStore3, + /// Toggle display of program or content guide. (`VK_GUIDE`, `KEYCODE_GUIDE`) + Guide, + /// If guide is active and displayed, then display next day’s content. (`VK_NEXT_DAY`) + GuideNextDay, + /// If guide is active and displayed, then display previous day’s content. (`VK_PREV_DAY`) + GuidePreviousDay, + /// Toggle display of information about currently selected context or media. (`VK_INFO`, + /// `KEYCODE_INFO`) + Info, + /// Toggle instant replay. (`VK_INSTANT_REPLAY`) + InstantReplay, + /// Launch linked content, if available and appropriate. (`VK_LINK`) + Link, + /// List the current program. (`VK_LIST`) + ListProgram, + /// Toggle display listing of currently available live content or programs. (`VK_LIVE`) + LiveContent, + /// Lock or unlock current content or program. (`VK_LOCK`) + Lock, + /// Show a list of media applications: audio/video players and image viewers. (`VK_APPS`) + /// + /// Note: Do not confuse this key value with the Windows' `VK_APPS` / `VK_CONTEXT_MENU` key, + /// which is encoded as `"ContextMenu"`. + MediaApps, + /// Audio track key. (`KEYCODE_MEDIA_AUDIO_TRACK`) + MediaAudioTrack, + /// Select previously selected channel or media. (`VK_LAST`, `KEYCODE_LAST_CHANNEL`) + MediaLast, + /// Skip backward to next content or program. (`KEYCODE_MEDIA_SKIP_BACKWARD`) + MediaSkipBackward, + /// Skip forward to next content or program. (`VK_SKIP`, `KEYCODE_MEDIA_SKIP_FORWARD`) + MediaSkipForward, + /// Step backward to next content or program. (`KEYCODE_MEDIA_STEP_BACKWARD`) + MediaStepBackward, + /// Step forward to next content or program. (`KEYCODE_MEDIA_STEP_FORWARD`) + MediaStepForward, + /// Media top menu. (`KEYCODE_MEDIA_TOP_MENU`) + MediaTopMenu, + /// Navigate in. (`KEYCODE_NAVIGATE_IN`) + NavigateIn, + /// Navigate to next key. (`KEYCODE_NAVIGATE_NEXT`) + NavigateNext, + /// Navigate out. (`KEYCODE_NAVIGATE_OUT`) + NavigateOut, + /// Navigate to previous key. (`KEYCODE_NAVIGATE_PREVIOUS`) + NavigatePrevious, + /// Cycle to next favorite channel (in favorites list). (`VK_NEXT_FAVORITE_CHANNEL`) + NextFavoriteChannel, + /// Cycle to next user profile (if there are multiple user profiles). (`VK_USER`) + NextUserProfile, + /// Access on-demand content or programs. (`VK_ON_DEMAND`) + OnDemand, + /// Pairing key to pair devices. (`KEYCODE_PAIRING`) + Pairing, + /// Move picture-in-picture window down. (`VK_PINP_DOWN`) + PinPDown, + /// Move picture-in-picture window. (`VK_PINP_MOVE`) + PinPMove, + /// Toggle display of picture-in-picture window. (`VK_PINP_TOGGLE`) + PinPToggle, + /// Move picture-in-picture window up. (`VK_PINP_UP`) + PinPUp, + /// Decrease media playback speed. (`VK_PLAY_SPEED_DOWN`) + PlaySpeedDown, + /// Reset playback to normal speed. (`VK_PLAY_SPEED_RESET`) + PlaySpeedReset, + /// Increase media playback speed. (`VK_PLAY_SPEED_UP`) + PlaySpeedUp, + /// Toggle random media or content shuffle mode. (`VK_RANDOM_TOGGLE`) + RandomToggle, + /// Not a physical key, but this key code is sent when the remote control battery is low. + /// (`VK_RC_LOW_BATTERY`) + RcLowBattery, + /// Toggle or cycle between media recording speeds. (`VK_RECORD_SPEED_NEXT`) + RecordSpeedNext, + /// Toggle RF (radio frequency) input bypass mode (pass RF input directly to the RF output). + /// (`VK_RF_BYPASS`) + RfBypass, + /// Toggle scan channels mode. (`VK_SCAN_CHANNELS_TOGGLE`) + ScanChannelsToggle, + /// Advance display screen mode to next available mode. (`VK_SCREEN_MODE_NEXT`) + ScreenModeNext, + /// Toggle display of device settings screen. (`VK_SETTINGS`, `KEYCODE_SETTINGS`) + Settings, + /// Toggle split screen mode. (`VK_SPLIT_SCREEN_TOGGLE`) + SplitScreenToggle, + /// Switch the input mode on an external STB (set top box). (`KEYCODE_STB_INPUT`) + STBInput, + /// Toggle the power on an external STB (set top box). (`KEYCODE_STB_POWER`) + STBPower, + /// Toggle display of subtitles, if available. (`VK_SUBTITLE`) + Subtitle, + /// Toggle display of teletext, if available (`VK_TELETEXT`, `KEYCODE_TV_TELETEXT`). + Teletext, + /// Advance video mode to next available mode. (`VK_VIDEO_MODE_NEXT`) + VideoModeNext, + /// Cause device to identify itself in some manner, e.g., audibly or visibly. (`VK_WINK`) + Wink, + /// Toggle between full-screen and scaled content, or alter magnification level. (`VK_ZOOM`, + /// `KEYCODE_TV_ZOOM_MODE`) + ZoomToggle, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F1, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F2, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F3, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F4, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F5, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F6, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F7, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F8, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F9, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F10, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F11, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F12, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F13, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F14, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F15, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F16, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F17, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F18, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F19, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F20, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F21, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F22, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F23, + /// General-purpose function key. + /// Usually found at the top of the keyboard. + F24, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, +} + +impl<'a> Key<'a> { + pub fn to_text(&self) -> Option<&'a str> { + match self { + Key::Character(ch) => Some(*ch), + Key::Enter => Some("\r"), + Key::Backspace => Some("\x08"), + Key::Tab => Some("\t"), + Key::Space => Some(" "), + Key::Escape => Some("\x1b"), + _ => None, + } + } +} + +impl<'a> From<&'a str> for Key<'a> { + fn from(src: &'a str) -> Key<'a> { + Key::Character(src) + } +} + +/// Represents the location of a physical key. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum KeyLocation { + Standard, + Left, + Right, + Numpad, +} diff --git a/src/lib.rs b/src/lib.rs index 2254481d0..938d2eb98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -162,9 +162,11 @@ pub mod clipboard; pub mod dpi; #[macro_use] pub mod error; +pub mod accelerator; pub mod event; pub mod event_loop; mod icon; +pub mod keyboard; pub mod menu; pub mod monitor; mod platform_impl; diff --git a/src/menu.rs b/src/menu.rs index 9da134024..5efa236b4 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -23,7 +23,10 @@ use std::{ hash::{Hash, Hasher}, }; -use crate::platform_impl::{Menu as MenuPlatform, MenuItemAttributes as CustomMenuItemPlatform}; +use crate::{ + accelerator::Accelerator, + platform_impl::{Menu as MenuPlatform, MenuItemAttributes as CustomMenuItemPlatform}, +}; /// Object that allows you to create a `ContextMenu`. pub struct ContextMenu(pub(crate) Menu); @@ -36,7 +39,7 @@ pub struct MenuBar(pub(crate) Menu); pub struct MenuItemAttributes<'a> { id: MenuId, title: &'a str, - keyboard_accelerator: Option<&'a str>, + keyboard_accelerator: Option, enabled: bool, selected: bool, } @@ -69,8 +72,8 @@ impl<'a> MenuItemAttributes<'a> { /// /// - **Windows / Android / iOS:** Unsupported /// - pub fn with_accelerators(mut self, keyboard_accelerators: &'a str) -> Self { - self.keyboard_accelerator = Some(keyboard_accelerators); + pub fn with_accelerators(mut self, keyboard_accelerators: &Accelerator) -> Self { + self.keyboard_accelerator = Some(keyboard_accelerators.to_owned()); self } @@ -371,10 +374,10 @@ impl CustomMenuItem { /// Whenever you receive an event arising from a particular menu, this event contains a `MenuId` which /// identifies its origin. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct MenuId(pub u32); +pub struct MenuId(pub u16); -impl From for u32 { - fn from(s: MenuId) -> u32 { +impl From for u16 { + fn from(s: MenuId) -> u16 { s.0 } } @@ -385,7 +388,7 @@ impl MenuId { /// Create new `MenuId` from a String. pub fn new(unique_string: &str) -> MenuId { - MenuId(hash_string_to_u32(unique_string)) + MenuId(hash_string_to_u16(unique_string)) } /// Whenever this menu is empty. @@ -403,8 +406,8 @@ pub enum MenuType { ContextMenu, } -fn hash_string_to_u32(title: &str) -> u32 { +fn hash_string_to_u16(title: &str) -> u16 { let mut s = DefaultHasher::new(); - title.hash(&mut s); - s.finish() as u32 + title.to_uppercase().hash(&mut s); + s.finish() as u16 } diff --git a/src/platform/global_shortcut.rs b/src/platform/global_shortcut.rs new file mode 100644 index 000000000..58e49b10d --- /dev/null +++ b/src/platform/global_shortcut.rs @@ -0,0 +1,106 @@ +#![cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] + +//! **UNSTABLE** -- The `GlobalShortcut` struct and associated types. +//! +//! ```rust,ignore +//! let mut hotkey_manager = ShortcutManager::new(&event_loop); +//! let accelerator = Accelerator::new(SysMods::Shift, KeyCode::ArrowUp); +//! let global_shortcut = hotkey_manager.register(accelerator)?; +//! ``` + +use crate::{ + accelerator::Accelerator, + event_loop::EventLoopWindowTarget, + platform_impl::{ + GlobalShortcut as GlobalShortcutPlatform, ShortcutManager as ShortcutManagerPlatform, + }, +}; +use std::{error, fmt}; + +/// Describes a global keyboard shortcut. +#[derive(Debug, Clone)] +pub struct GlobalShortcut(pub(crate) GlobalShortcutPlatform); + +/// Object that allows you to manage a `GlobalShortcut`. +#[derive(Debug)] +pub struct ShortcutManager { + registered_hotkeys: Vec, + p: ShortcutManagerPlatform, +} + +impl ShortcutManager { + /// Creates a new shortcut manager instance connected to the event loop. + pub fn new(event_loop: &EventLoopWindowTarget) -> ShortcutManager { + ShortcutManager { + p: ShortcutManagerPlatform::new(event_loop), + registered_hotkeys: Vec::new(), + } + } + + /// Whether the application has registered this `Accelerator`. + pub fn is_registered(&self, accelerator: &Accelerator) -> bool { + self.registered_hotkeys.contains(&Box::new(accelerator)) + } + + /// Register a global shortcut of `Accelerator` who trigger `GlobalShortcutEvent` in the event loop. + pub fn register( + &mut self, + accelerator: Accelerator, + ) -> Result { + if self.is_registered(&accelerator) { + return Err(ShortcutManagerError::AcceleratorAlreadyRegistered( + accelerator, + )); + } + self.registered_hotkeys.push(accelerator.clone()); + self.p.register(accelerator) + } + + /// Unregister all `Accelerator` registered by the manager instance. + pub fn unregister_all(&mut self) -> Result<(), ShortcutManagerError> { + self.registered_hotkeys = Vec::new(); + self.p.unregister_all() + } + + /// Unregister the provided `Accelerator`. + pub fn unregister( + &mut self, + global_shortcut: GlobalShortcut, + ) -> Result<(), ShortcutManagerError> { + self + .registered_hotkeys + .retain(|hotkey| hotkey.to_owned().id() != global_shortcut.0.id()); + self.p.unregister(global_shortcut) + } +} + +/// An error whose cause the `ShortcutManager` to fail. +#[derive(Debug)] +pub enum ShortcutManagerError { + AcceleratorAlreadyRegistered(Accelerator), + AcceleratorNotRegistered(Accelerator), + InvalidAccelerator(String), +} + +impl error::Error for ShortcutManagerError {} +impl fmt::Display for ShortcutManagerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + ShortcutManagerError::AcceleratorAlreadyRegistered(e) => { + f.pad(&format!("hotkey already registered: {:?}", e)) + } + ShortcutManagerError::AcceleratorNotRegistered(e) => { + f.pad(&format!("hotkey not registered: {:?}", e)) + } + ShortcutManagerError::InvalidAccelerator(e) => e.fmt(f), + } + } +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index f33ab00e5..9f4f630ca 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -13,11 +13,13 @@ //! //! And the following platform-specific module: //! +//! - `global_shortcut` (available on `windows`, `unix`, `macos`) //! - `run_return` (available on `windows`, `unix`, `macos`, and `android`) //! //! However only the module corresponding to the platform you're compiling to will be available. pub mod android; +pub mod global_shortcut; pub mod ios; pub mod macos; pub mod run_return; diff --git a/src/platform/unix.rs b/src/platform/unix.rs index 1648e5d4a..3a03010f9 100644 --- a/src/platform/unix.rs +++ b/src/platform/unix.rs @@ -9,9 +9,8 @@ target_os = "openbsd" ))] -use crate::window::Window; - pub use crate::platform_impl::hit_test; +use crate::window::Window; /// Additional methods on `Window` that are specific to Unix. pub trait WindowExtUnix { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index c5e9b967e..e0d0c02dd 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -102,6 +102,13 @@ pub trait WindowExtWindows { /// Returns the current window theme. fn theme(&self) -> Theme; + + /// Reset the dead key state of the keyboard. + /// + /// This is useful when a dead key is bound to trigger an action. Then + /// this function can be called to reset the dead key state so that + /// follow-up text input won't be affected by the dead key. + fn reset_dead_keys(&self); } impl WindowExtWindows for Window { @@ -131,6 +138,11 @@ impl WindowExtWindows for Window { fn theme(&self) -> Theme { self.window.theme() } + + #[inline] + fn reset_dead_keys(&self) { + self.window.reset_dead_keys(); + } } /// Additional methods on `WindowBuilder` that are specific to Windows. diff --git a/src/platform_impl/android/clipboard.rs b/src/platform_impl/android/clipboard.rs index 061013335..f1d0d6536 100644 --- a/src/platform_impl/android/clipboard.rs +++ b/src/platform_impl/android/clipboard.rs @@ -4,7 +4,7 @@ #[derive(Debug, Clone, Default)] pub struct Clipboard; impl Clipboard { - pub(crate) fn write_text(&mut self, s: impl AsRef) {} + pub(crate) fn write_text(&mut self, _s: impl AsRef) {} pub(crate) fn read_text(&self) -> Option { None } diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index b4f3fda84..f23d8c1ef 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -3,9 +3,11 @@ #![cfg(target_os = "android")] use crate::{ + accelerator::Accelerator, dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error, event, event_loop::{self, ControlFlow}, + keyboard::{Key, KeyCode, KeyLocation, NativeKeyCode}, menu::{CustomMenuItem, MenuId, MenuItem, MenuType}, monitor, window, }; @@ -15,8 +17,10 @@ use ndk::{ looper::{ForeignLooper, Poll, ThreadLooper}, }; use ndk_glue::{Event, Rect}; +use ndk_sys::AKeyEvent_getKeyCode; use std::{ collections::VecDeque, + convert::TryInto, sync::{Arc, Mutex, RwLock}, time::{Duration, Instant}, }; @@ -51,6 +55,9 @@ fn poll(poll: Poll) -> Option { #[derive(Debug, Clone)] pub struct MenuItemAttributes; +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra {} + #[derive(Debug, Clone)] pub struct Menu; @@ -71,7 +78,7 @@ impl Menu { &mut self, _menu_id: MenuId, _title: &str, - _accelerators: Option<&str>, + _accelerator: Option, _enabled: bool, _selected: bool, _menu_type: MenuType, @@ -288,16 +295,32 @@ impl EventLoop { KeyAction::Up => event::ElementState::Released, _ => event::ElementState::Released, }; - #[allow(deprecated)] + + // We use the unsafe function directly because + // we want to forward the keycode value even if it doesn't have a variant + // defined in the ndk crate. + let keycode_u32 = unsafe { AKeyEvent_getKeyCode(key.ptr().as_ptr()) as u32 }; + let keycode = keycode_u32 + .try_into() + .unwrap_or(ndk::event::Keycode::Unknown); + let physical_key = + KeyCode::Unidentified(NativeKeyCode::Android(keycode.into())); + let native = NativeKeyCode::Android(keycode_u32); + let logical_key = keycode_to_logical(keycode, native); + // TODO: maybe use getUnicodeChar to get the logical key + let event = event::Event::WindowEvent { window_id, event: event::WindowEvent::KeyboardInput { device_id, - input: event::KeyboardInput { - scancode: key.scan_code() as u32, + event: event::KeyEvent { state, - virtual_keycode: None, - modifiers: event::ModifiersState::default(), + physical_key, + logical_key, + location: keycode_to_location(keycode), + repeat: key.repeat_count() > 0, + text: None, + platform_specific: KeyEventExtra {}, }, is_synthetic: false, }, @@ -737,3 +760,386 @@ impl VideoMode { } } } + +fn keycode_to_logical(keycode: ndk::event::Keycode, native: NativeKeyCode) -> Key<'static> { + use ndk::event::Keycode::*; + + // The android `Keycode` is sort-of layout dependent. More specifically + // if I press the Z key using a US layout, then I get KEYCODE_Z, + // but if I press the same key after switching to a HUN layout, I get + // KEYCODE_Y. + // + // To prevents us from using this value to determine the `physical_key` + // (also know as winit's `KeyCode`) + // + // Unfortunately the documentation says that the scancode values + // "are not reliable and vary from device to device". Which seems to mean + // that there's no way to reliably get the physical_key on android. + + match keycode { + Unknown => Key::Unidentified(native), + + // Can be added on demand + SoftLeft => Key::Unidentified(native), + SoftRight => Key::Unidentified(native), + + // Using `BrowserHome` instead of `GoHome` according to + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + Home => Key::BrowserHome, + Back => Key::BrowserBack, + Call => Key::Call, + Endcall => Key::EndCall, + + //------------------------------------------------------------------------------- + // Reporting unidentified, because the specific character is layout dependent. + // (I'm not sure though) + Keycode0 => Key::Unidentified(native), + Keycode1 => Key::Unidentified(native), + Keycode2 => Key::Unidentified(native), + Keycode3 => Key::Unidentified(native), + Keycode4 => Key::Unidentified(native), + Keycode5 => Key::Unidentified(native), + Keycode6 => Key::Unidentified(native), + Keycode7 => Key::Unidentified(native), + Keycode8 => Key::Unidentified(native), + Keycode9 => Key::Unidentified(native), + Star => Key::Unidentified(native), + Pound => Key::Unidentified(native), + A => Key::Unidentified(native), + B => Key::Unidentified(native), + C => Key::Unidentified(native), + D => Key::Unidentified(native), + E => Key::Unidentified(native), + F => Key::Unidentified(native), + G => Key::Unidentified(native), + H => Key::Unidentified(native), + I => Key::Unidentified(native), + J => Key::Unidentified(native), + K => Key::Unidentified(native), + L => Key::Unidentified(native), + M => Key::Unidentified(native), + N => Key::Unidentified(native), + O => Key::Unidentified(native), + P => Key::Unidentified(native), + Q => Key::Unidentified(native), + R => Key::Unidentified(native), + S => Key::Unidentified(native), + T => Key::Unidentified(native), + U => Key::Unidentified(native), + V => Key::Unidentified(native), + W => Key::Unidentified(native), + X => Key::Unidentified(native), + Y => Key::Unidentified(native), + Z => Key::Unidentified(native), + Comma => Key::Unidentified(native), + Period => Key::Unidentified(native), + Grave => Key::Unidentified(native), + Minus => Key::Unidentified(native), + Equals => Key::Unidentified(native), + LeftBracket => Key::Unidentified(native), + RightBracket => Key::Unidentified(native), + Backslash => Key::Unidentified(native), + Semicolon => Key::Unidentified(native), + Apostrophe => Key::Unidentified(native), + Slash => Key::Unidentified(native), + At => Key::Unidentified(native), + Plus => Key::Unidentified(native), + //------------------------------------------------------------------------------- + DpadUp => Key::ArrowUp, + DpadDown => Key::ArrowDown, + DpadLeft => Key::ArrowLeft, + DpadRight => Key::ArrowRight, + DpadCenter => Key::Enter, + + VolumeUp => Key::AudioVolumeUp, + VolumeDown => Key::AudioVolumeDown, + Power => Key::Power, + Camera => Key::Camera, + Clear => Key::Clear, + + AltLeft => Key::Alt, + AltRight => Key::Alt, + ShiftLeft => Key::Shift, + ShiftRight => Key::Shift, + Tab => Key::Tab, + Space => Key::Space, + Sym => Key::Symbol, + Explorer => Key::LaunchWebBrowser, + Envelope => Key::LaunchMail, + Enter => Key::Enter, + Del => Key::Backspace, + + // According to https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_NUM + Num => Key::Alt, + + Headsethook => Key::HeadsetHook, + Focus => Key::CameraFocus, + + Menu => Key::Unidentified(native), + + Notification => Key::Notification, + Search => Key::BrowserSearch, + MediaPlayPause => Key::MediaPlayPause, + MediaStop => Key::MediaStop, + MediaNext => Key::MediaTrackNext, + MediaPrevious => Key::MediaTrackPrevious, + MediaRewind => Key::MediaRewind, + MediaFastForward => Key::MediaFastForward, + Mute => Key::MicrophoneVolumeMute, + PageUp => Key::PageUp, + PageDown => Key::PageDown, + Pictsymbols => Key::Unidentified(native), + SwitchCharset => Key::Unidentified(native), + + // ----------------------------------------------------------------- + // Gamepad events should be exposed through a separate API, not + // keyboard events + ButtonA => Key::Unidentified(native), + ButtonB => Key::Unidentified(native), + ButtonC => Key::Unidentified(native), + ButtonX => Key::Unidentified(native), + ButtonY => Key::Unidentified(native), + ButtonZ => Key::Unidentified(native), + ButtonL1 => Key::Unidentified(native), + ButtonR1 => Key::Unidentified(native), + ButtonL2 => Key::Unidentified(native), + ButtonR2 => Key::Unidentified(native), + ButtonThumbl => Key::Unidentified(native), + ButtonThumbr => Key::Unidentified(native), + ButtonStart => Key::Unidentified(native), + ButtonSelect => Key::Unidentified(native), + ButtonMode => Key::Unidentified(native), + // ----------------------------------------------------------------- + Escape => Key::Escape, + ForwardDel => Key::Delete, + CtrlLeft => Key::Control, + CtrlRight => Key::Control, + CapsLock => Key::CapsLock, + ScrollLock => Key::ScrollLock, + MetaLeft => Key::Super, + MetaRight => Key::Super, + Function => Key::Fn, + Sysrq => Key::PrintScreen, + Break => Key::Pause, + MoveHome => Key::Home, + MoveEnd => Key::End, + Insert => Key::Insert, + Forward => Key::BrowserForward, + MediaPlay => Key::MediaPlay, + MediaPause => Key::MediaPause, + MediaClose => Key::MediaClose, + MediaEject => Key::Eject, + MediaRecord => Key::MediaRecord, + F1 => Key::F1, + F2 => Key::F2, + F3 => Key::F3, + F4 => Key::F4, + F5 => Key::F5, + F6 => Key::F6, + F7 => Key::F7, + F8 => Key::F8, + F9 => Key::F9, + F10 => Key::F10, + F11 => Key::F11, + F12 => Key::F12, + NumLock => Key::NumLock, + Numpad0 => Key::Unidentified(native), + Numpad1 => Key::Unidentified(native), + Numpad2 => Key::Unidentified(native), + Numpad3 => Key::Unidentified(native), + Numpad4 => Key::Unidentified(native), + Numpad5 => Key::Unidentified(native), + Numpad6 => Key::Unidentified(native), + Numpad7 => Key::Unidentified(native), + Numpad8 => Key::Unidentified(native), + Numpad9 => Key::Unidentified(native), + NumpadDivide => Key::Unidentified(native), + NumpadMultiply => Key::Unidentified(native), + NumpadSubtract => Key::Unidentified(native), + NumpadAdd => Key::Unidentified(native), + NumpadDot => Key::Unidentified(native), + NumpadComma => Key::Unidentified(native), + NumpadEnter => Key::Unidentified(native), + NumpadEquals => Key::Unidentified(native), + NumpadLeftParen => Key::Unidentified(native), + NumpadRightParen => Key::Unidentified(native), + + VolumeMute => Key::AudioVolumeMute, + Info => Key::Info, + ChannelUp => Key::ChannelUp, + ChannelDown => Key::ChannelDown, + ZoomIn => Key::ZoomIn, + ZoomOut => Key::ZoomOut, + Tv => Key::TV, + Window => Key::Unidentified(native), + Guide => Key::Guide, + Dvr => Key::DVR, + Bookmark => Key::BrowserFavorites, + Captions => Key::ClosedCaptionToggle, + Settings => Key::Settings, + TvPower => Key::TVPower, + TvInput => Key::TVInput, + StbPower => Key::STBPower, + StbInput => Key::STBInput, + AvrPower => Key::AVRPower, + AvrInput => Key::AVRInput, + ProgRed => Key::ColorF0Red, + ProgGreen => Key::ColorF1Green, + ProgYellow => Key::ColorF2Yellow, + ProgBlue => Key::ColorF3Blue, + AppSwitch => Key::AppSwitch, + Button1 => Key::Unidentified(native), + Button2 => Key::Unidentified(native), + Button3 => Key::Unidentified(native), + Button4 => Key::Unidentified(native), + Button5 => Key::Unidentified(native), + Button6 => Key::Unidentified(native), + Button7 => Key::Unidentified(native), + Button8 => Key::Unidentified(native), + Button9 => Key::Unidentified(native), + Button10 => Key::Unidentified(native), + Button11 => Key::Unidentified(native), + Button12 => Key::Unidentified(native), + Button13 => Key::Unidentified(native), + Button14 => Key::Unidentified(native), + Button15 => Key::Unidentified(native), + Button16 => Key::Unidentified(native), + LanguageSwitch => Key::GroupNext, + MannerMode => Key::MannerMode, + Keycode3dMode => Key::TV3DMode, + Contacts => Key::LaunchContacts, + Calendar => Key::LaunchCalendar, + Music => Key::LaunchMusicPlayer, + Calculator => Key::LaunchApplication2, + ZenkakuHankaku => Key::ZenkakuHankaku, + Eisu => Key::Eisu, + Muhenkan => Key::NonConvert, + Henkan => Key::Convert, + KatakanaHiragana => Key::HiraganaKatakana, + Yen => Key::Unidentified(native), + Ro => Key::Unidentified(native), + Kana => Key::KanjiMode, + Assist => Key::Unidentified(native), + BrightnessDown => Key::BrightnessDown, + BrightnessUp => Key::BrightnessUp, + MediaAudioTrack => Key::MediaAudioTrack, + Sleep => Key::Standby, + Wakeup => Key::WakeUp, + Pairing => Key::Pairing, + MediaTopMenu => Key::MediaTopMenu, + Keycode11 => Key::Unidentified(native), + Keycode12 => Key::Unidentified(native), + LastChannel => Key::MediaLast, + TvDataService => Key::TVDataService, + VoiceAssist => Key::VoiceDial, + TvRadioService => Key::TVRadioService, + TvTeletext => Key::Teletext, + TvNumberEntry => Key::TVNumberEntry, + TvTerrestrialAnalog => Key::TVTerrestrialAnalog, + TvTerrestrialDigital => Key::TVTerrestrialDigital, + TvSatellite => Key::TVSatellite, + TvSatelliteBs => Key::TVSatelliteBS, + TvSatelliteCs => Key::TVSatelliteCS, + TvSatelliteService => Key::TVSatelliteToggle, + TvNetwork => Key::TVNetwork, + TvAntennaCable => Key::TVAntennaCable, + TvInputHdmi1 => Key::TVInputHDMI1, + TvInputHdmi2 => Key::TVInputHDMI2, + TvInputHdmi3 => Key::TVInputHDMI3, + TvInputHdmi4 => Key::TVInputHDMI4, + TvInputComposite1 => Key::TVInputComposite1, + TvInputComposite2 => Key::TVInputComposite2, + TvInputComponent1 => Key::TVInputComponent1, + TvInputComponent2 => Key::TVInputComponent2, + TvInputVga1 => Key::TVInputVGA1, + TvAudioDescription => Key::TVAudioDescription, + TvAudioDescriptionMixUp => Key::TVAudioDescriptionMixUp, + TvAudioDescriptionMixDown => Key::TVAudioDescriptionMixDown, + TvZoomMode => Key::ZoomToggle, + TvContentsMenu => Key::TVContentsMenu, + TvMediaContextMenu => Key::TVMediaContext, + TvTimerProgramming => Key::TVTimer, + Help => Key::Help, + NavigatePrevious => Key::NavigatePrevious, + NavigateNext => Key::NavigateNext, + NavigateIn => Key::NavigateIn, + NavigateOut => Key::NavigateOut, + StemPrimary => Key::Unidentified(native), + Stem1 => Key::Unidentified(native), + Stem2 => Key::Unidentified(native), + Stem3 => Key::Unidentified(native), + DpadUpLeft => Key::Unidentified(native), + DpadDownLeft => Key::Unidentified(native), + DpadUpRight => Key::Unidentified(native), + DpadDownRight => Key::Unidentified(native), + MediaSkipForward => Key::MediaSkipForward, + MediaSkipBackward => Key::MediaSkipBackward, + MediaStepForward => Key::MediaStepForward, + MediaStepBackward => Key::MediaStepBackward, + SoftSleep => Key::Unidentified(native), + Cut => Key::Cut, + Copy => Key::Copy, + Paste => Key::Paste, + SystemNavigationUp => Key::Unidentified(native), + SystemNavigationDown => Key::Unidentified(native), + SystemNavigationLeft => Key::Unidentified(native), + SystemNavigationRight => Key::Unidentified(native), + AllApps => Key::Unidentified(native), + Refresh => Key::BrowserRefresh, + ThumbsUp => Key::Unidentified(native), + ThumbsDown => Key::Unidentified(native), + ProfileSwitch => Key::Unidentified(native), + } +} + +fn keycode_to_location(keycode: ndk::event::Keycode) -> KeyLocation { + use ndk::event::Keycode::*; + + match keycode { + AltLeft => KeyLocation::Left, + AltRight => KeyLocation::Right, + ShiftLeft => KeyLocation::Left, + ShiftRight => KeyLocation::Right, + + // According to https://developer.android.com/reference/android/view/KeyEvent#KEYCODE_NUM + Num => KeyLocation::Left, + + CtrlLeft => KeyLocation::Left, + CtrlRight => KeyLocation::Right, + MetaLeft => KeyLocation::Left, + MetaRight => KeyLocation::Right, + + NumLock => KeyLocation::Numpad, + Numpad0 => KeyLocation::Numpad, + Numpad1 => KeyLocation::Numpad, + Numpad2 => KeyLocation::Numpad, + Numpad3 => KeyLocation::Numpad, + Numpad4 => KeyLocation::Numpad, + Numpad5 => KeyLocation::Numpad, + Numpad6 => KeyLocation::Numpad, + Numpad7 => KeyLocation::Numpad, + Numpad8 => KeyLocation::Numpad, + Numpad9 => KeyLocation::Numpad, + NumpadDivide => KeyLocation::Numpad, + NumpadMultiply => KeyLocation::Numpad, + NumpadSubtract => KeyLocation::Numpad, + NumpadAdd => KeyLocation::Numpad, + NumpadDot => KeyLocation::Numpad, + NumpadComma => KeyLocation::Numpad, + NumpadEnter => KeyLocation::Numpad, + NumpadEquals => KeyLocation::Numpad, + NumpadLeftParen => KeyLocation::Numpad, + NumpadRightParen => KeyLocation::Numpad, + + _ => KeyLocation::Standard, + } +} + +// FIXME: Implement android +pub fn keycode_to_scancode(_code: KeyCode) -> Option { + None +} + +pub fn keycode_from_scancode(_scancode: u32) -> KeyCode { + KeyCode::Unidentified(NativeKeyCode::Unidentified) +} diff --git a/src/platform_impl/ios/clipboard.rs b/src/platform_impl/ios/clipboard.rs index 061013335..f1d0d6536 100644 --- a/src/platform_impl/ios/clipboard.rs +++ b/src/platform_impl/ios/clipboard.rs @@ -4,7 +4,7 @@ #[derive(Debug, Clone, Default)] pub struct Clipboard; impl Clipboard { - pub(crate) fn write_text(&mut self, s: impl AsRef) {} + pub(crate) fn write_text(&mut self, _s: impl AsRef) {} pub(crate) fn read_text(&self) -> Option { None } diff --git a/src/platform_impl/ios/keycode.rs b/src/platform_impl/ios/keycode.rs new file mode 100644 index 000000000..29b30a4c1 --- /dev/null +++ b/src/platform_impl/ios/keycode.rs @@ -0,0 +1,9 @@ +use crate::keyboard::{KeyCode, NativeKeyCode}; + +pub fn keycode_to_scancode(_code: KeyCode) -> Option { + None +} + +pub fn keycode_from_scancode(_scancode: u32) -> KeyCode { + KeyCode::Unidentified(NativeKeyCode::Unidentified) +} diff --git a/src/platform_impl/ios/mod.rs b/src/platform_impl/ios/mod.rs index 6b75fc380..b9b6b2637 100644 --- a/src/platform_impl/ios/mod.rs +++ b/src/platform_impl/ios/mod.rs @@ -75,22 +75,31 @@ mod app_state; mod clipboard; mod event_loop; mod ffi; +mod keycode; mod monitor; mod view; mod window; -use crate::menu::{CustomMenuItem, MenuId, MenuItem, MenuType}; +use crate::{ + accelerator::Accelerator, + menu::{CustomMenuItem, MenuId, MenuItem, MenuType}, +}; use std::fmt; pub use self::{ clipboard::Clipboard, event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}, + keycode::{keycode_from_scancode, keycode_to_scancode}, monitor::{MonitorHandle, VideoMode}, window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId}, }; pub(crate) use crate::icon::NoIcon as PlatformIcon; +// todo: implement iOS keyboard event +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra {} + // todo: implement iOS menubar #[derive(Debug, Clone)] pub struct MenuItemAttributes; @@ -114,7 +123,7 @@ impl Menu { &mut self, _menu_id: MenuId, _title: &str, - _accelerators: Option<&str>, + _accelerator: Option, _enabled: bool, _selected: bool, _menu_type: MenuType, diff --git a/src/platform_impl/linux/event_loop.rs b/src/platform_impl/linux/event_loop.rs index fdcef0ec2..1cf3b813f 100644 --- a/src/platform_impl/linux/event_loop.rs +++ b/src/platform_impl/linux/event_loop.rs @@ -22,18 +22,18 @@ use glib::Cast; use gtk::{Clipboard, Entry}; use crate::{ + accelerator::AcceleratorId, dpi::{PhysicalPosition, PhysicalSize}, - event::{ - DeviceId as RootDeviceId, ElementState, Event, ModifiersState, MouseButton, StartCause, - WindowEvent, - }, + event::{DeviceId as RootDeviceId, ElementState, Event, MouseButton, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, + keyboard::ModifiersState, menu::{MenuItem, MenuType}, monitor::MonitorHandle as RootMonitorHandle, window::{CursorIcon, WindowId as RootWindowId}, }; use super::{ + keyboard, monitor::MonitorHandle, window::{WindowId, WindowRequest}, DeviceId, @@ -590,15 +590,66 @@ impl EventLoop { menubar.show_all(); } } + WindowRequest::KeyboardInput((event_key, element_state)) => { + // if we have a modifier lets send it + let mut mods = keyboard::get_modifiers(event_key.clone()); + if !mods.is_empty() { + // if we release the modifier tell the world + if ElementState::Released == element_state { + mods = ModifiersState::empty(); + } + + if let Err(e) = event_tx.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ModifiersChanged(mods), + }) { + log::warn!( + "Failed to send modifiers changed event to event channel: {}", + e + ); + } else { + // stop here we don't want to send the key event + // as we emit the `ModifiersChanged` + return Continue(true); + } + } + + // todo: implement repeat? + let event = keyboard::make_key_event(&event_key, false, None, element_state); + + if let Some(event) = event { + if let Err(e) = event_tx.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::KeyboardInput { + // FIXME: currently we use a dummy device id, find if we can get device id from gtk + device_id: RootDeviceId(DeviceId(0)), + event, + is_synthetic: false, + }, + }) { + log::warn!("Failed to send keyboard event to event channel: {}", e); + } + } + } + // we use dummy window id for `GlobalHotKey` + WindowRequest::GlobalHotKey(_hotkey_id) => {} } } else if id == WindowId::dummy() { - if let WindowRequest::Menu((None, Some(menu_id))) = request { - if let Err(e) = event_tx.send(Event::MenuEvent { - menu_id, - origin: MenuType::ContextMenu, - }) { - log::warn!("Failed to send status bar event to event channel: {}", e); + match request { + WindowRequest::GlobalHotKey(hotkey_id) => { + if let Err(e) = event_tx.send(Event::GlobalShortcutEvent(AcceleratorId(hotkey_id))) { + log::warn!("Failed to send global hotkey event to event channel: {}", e); + } + } + WindowRequest::Menu((None, Some(menu_id))) => { + if let Err(e) = event_tx.send(Event::MenuEvent { + menu_id, + origin: MenuType::ContextMenu, + }) { + log::warn!("Failed to send status bar event to event channel: {}", e); + } } + _ => {} } } Continue(true) diff --git a/src/platform_impl/linux/global_shortcut.rs b/src/platform_impl/linux/global_shortcut.rs new file mode 100644 index 000000000..9bc736a6c --- /dev/null +++ b/src/platform_impl/linux/global_shortcut.rs @@ -0,0 +1,376 @@ +use super::window::{WindowId, WindowRequest}; +use crate::{ + accelerator::{Accelerator, AcceleratorId}, + event_loop::EventLoopWindowTarget, + keyboard::KeyCode, + platform::global_shortcut::{GlobalShortcut as RootGlobalShortcut, ShortcutManagerError}, +}; +use std::{ + collections::HashMap, + ptr, + sync::{mpsc, Arc, Mutex}, +}; +use x11_dl::{keysym, xlib}; + +#[derive(Debug)] +enum HotkeyMessage { + RegisterHotkey(ListenerId, u32, u32), + RegisterHotkeyResult(Result), + UnregisterHotkey(ListenerId), + UnregisterHotkeyResult(Result<(), ShortcutManagerError>), + DropThread, +} + +#[derive(Debug)] +pub struct ShortcutManager { + shortcuts: ListenerMap, + method_sender: mpsc::Sender, + method_receiver: mpsc::Receiver, +} + +impl ShortcutManager { + pub(crate) fn new(_window_target: &EventLoopWindowTarget) -> Self { + let window_id = WindowId::dummy(); + let hotkeys = ListenerMap::default(); + let hotkey_map = hotkeys.clone(); + + let event_loop_channel = _window_target.p.window_requests_tx.clone(); + + let (method_sender, thread_receiver) = mpsc::channel(); + let (thread_sender, method_receiver) = mpsc::channel(); + + std::thread::spawn(move || { + let event_loop_channel = event_loop_channel.clone(); + let xlib = xlib::Xlib::open().unwrap(); + unsafe { + let display = (xlib.XOpenDisplay)(ptr::null()); + let root = (xlib.XDefaultRootWindow)(display); + + // Only trigger key release at end of repeated keys + let mut supported_rtrn: i32 = std::mem::MaybeUninit::uninit().assume_init(); + (xlib.XkbSetDetectableAutoRepeat)(display, 1, &mut supported_rtrn); + + (xlib.XSelectInput)(display, root, xlib::KeyReleaseMask); + let mut event: xlib::XEvent = std::mem::MaybeUninit::uninit().assume_init(); + + loop { + let event_loop_channel = event_loop_channel.clone(); + if (xlib.XPending)(display) > 0 { + (xlib.XNextEvent)(display, &mut event); + if let xlib::KeyRelease = event.get_type() { + let keycode = event.key.keycode; + let modifiers = event.key.state; + if let Some(hotkey_id) = hotkey_map.lock().unwrap().get(&(keycode as i32, modifiers)) + { + event_loop_channel + .send((window_id, WindowRequest::GlobalHotKey(*hotkey_id as u16))) + .unwrap(); + } + } + } + + match thread_receiver.try_recv() { + Ok(HotkeyMessage::RegisterHotkey(_, modifiers, key)) => { + let keycode = (xlib.XKeysymToKeycode)(display, key.into()) as i32; + + let result = (xlib.XGrabKey)( + display, + keycode, + modifiers, + root, + 0, + xlib::GrabModeAsync, + xlib::GrabModeAsync, + ); + if result == 0 { + if let Err(err) = thread_sender + .clone() + .send(HotkeyMessage::RegisterHotkeyResult(Err( + ShortcutManagerError::InvalidAccelerator( + "Unable to register accelerator".into(), + ), + ))) + { + eprintln!("hotkey: thread_sender.send error {}", err); + } + } else if let Err(err) = thread_sender.send(HotkeyMessage::RegisterHotkeyResult(Ok( + (keycode, modifiers), + ))) { + eprintln!("hotkey: thread_sender.send error {}", err); + } + } + Ok(HotkeyMessage::UnregisterHotkey(id)) => { + let result = (xlib.XUngrabKey)(display, id.0, id.1, root); + if result == 0 { + if let Err(err) = thread_sender + .clone() + .send(HotkeyMessage::UnregisterHotkeyResult(Err( + ShortcutManagerError::InvalidAccelerator( + "Unable to unregister accelerator".into(), + ), + ))) + { + eprintln!("hotkey: thread_sender.send error {}", err); + } + } + } + Ok(HotkeyMessage::DropThread) => { + (xlib.XCloseDisplay)(display); + return; + } + Err(err) => { + if let std::sync::mpsc::TryRecvError::Disconnected = err { + eprintln!("hotkey: try_recv error {}", err); + } + } + _ => unreachable!("other message should not arrive"), + }; + + std::thread::sleep(std::time::Duration::from_millis(50)); + } + } + }); + + ShortcutManager { + shortcuts: hotkeys, + method_sender, + method_receiver, + } + } + + pub(crate) fn register( + &mut self, + accelerator: Accelerator, + ) -> Result { + let keycode = get_x11_scancode_from_hotkey(accelerator.key.clone()); + + if let Some(keycode) = keycode { + let mut converted_modifiers: u32 = 0; + if accelerator.mods.shift_key() { + converted_modifiers |= xlib::ShiftMask; + } + if accelerator.mods.super_key() { + converted_modifiers |= xlib::Mod4Mask; + } + if accelerator.mods.alt_key() { + converted_modifiers |= xlib::Mod1Mask; + } + if accelerator.mods.control_key() { + converted_modifiers |= xlib::ControlMask; + } + + self + .method_sender + .send(HotkeyMessage::RegisterHotkey( + (0, 0), + converted_modifiers, + keycode, + )) + .map_err(|_| { + ShortcutManagerError::InvalidAccelerator("Unable to register global shortcut".into()) + })?; + + return match self.method_receiver.recv() { + Ok(HotkeyMessage::RegisterHotkeyResult(Ok(id))) => { + self + .shortcuts + .lock() + .unwrap() + .insert(id, accelerator.clone().id().0 as u32); + let shortcut = GlobalShortcut { accelerator }; + return Ok(RootGlobalShortcut(shortcut)); + } + Ok(HotkeyMessage::RegisterHotkeyResult(Err(err))) => Err(err), + Err(err) => Err(ShortcutManagerError::InvalidAccelerator(err.to_string())), + _ => Err(ShortcutManagerError::InvalidAccelerator( + "Unknown error".into(), + )), + }; + } + + Err(ShortcutManagerError::InvalidAccelerator( + "Invalid accelerators".into(), + )) + } + + pub(crate) fn unregister_all(&mut self) -> Result<(), ShortcutManagerError> { + for (found_id, _) in self.shortcuts.lock().unwrap().iter() { + self + .method_sender + .send(HotkeyMessage::UnregisterHotkey(*found_id)) + .map_err(|_| ShortcutManagerError::InvalidAccelerator("Channel error".into()))?; + } + self.shortcuts = ListenerMap::default(); + Ok(()) + } + + pub(crate) fn unregister( + &self, + shortcut: RootGlobalShortcut, + ) -> Result<(), ShortcutManagerError> { + let mut found_id = (-1, 0); + for (id, shortcut_id) in self.shortcuts.lock().unwrap().iter() { + if *shortcut_id == shortcut.0.id().0 as u32 { + found_id = *id; + break; + } + } + if found_id == (-1, 0) { + return Err(ShortcutManagerError::AcceleratorNotRegistered( + shortcut.0.accelerator, + )); + } + + self + .method_sender + .send(HotkeyMessage::UnregisterHotkey(found_id)) + .map_err(|_| ShortcutManagerError::InvalidAccelerator("Channel error".into()))?; + if self.shortcuts.lock().unwrap().remove(&found_id).is_none() { + panic!("hotkey should never be none") + }; + match self.method_receiver.recv() { + Ok(HotkeyMessage::UnregisterHotkeyResult(Ok(_))) => Ok(()), + Ok(HotkeyMessage::UnregisterHotkeyResult(Err(err))) => Err(err), + Err(err) => Err(ShortcutManagerError::InvalidAccelerator(err.to_string())), + _ => Err(ShortcutManagerError::InvalidAccelerator( + "Unknown error".into(), + )), + } + } +} + +impl Drop for ShortcutManager { + fn drop(&mut self) { + if let Err(err) = self.method_sender.send(HotkeyMessage::DropThread) { + eprintln!("cant send close thread message {}", err); + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct GlobalShortcut { + pub(crate) accelerator: Accelerator, +} +type ListenerId = (i32, u32); +type ListenerMap = Arc>>; + +impl GlobalShortcut { + pub fn id(&self) -> AcceleratorId { + self.accelerator.clone().id() + } +} + +// required for event but we use dummy window Id +// so it shouldn't be a problem +unsafe impl Send for WindowId {} +unsafe impl Sync for WindowId {} +// simple enum, no pointer, shouldn't be a problem +// to use send + sync +unsafe impl Send for WindowRequest {} +unsafe impl Sync for WindowRequest {} + +fn get_x11_scancode_from_hotkey(key: KeyCode) -> Option { + Some(match key { + KeyCode::KeyA => 'A' as u32, + KeyCode::KeyB => 'B' as u32, + KeyCode::KeyC => 'C' as u32, + KeyCode::KeyD => 'D' as u32, + KeyCode::KeyE => 'E' as u32, + KeyCode::KeyF => 'F' as u32, + KeyCode::KeyG => 'G' as u32, + KeyCode::KeyH => 'H' as u32, + KeyCode::KeyI => 'I' as u32, + KeyCode::KeyJ => 'J' as u32, + KeyCode::KeyK => 'K' as u32, + KeyCode::KeyL => 'L' as u32, + KeyCode::KeyM => 'M' as u32, + KeyCode::KeyN => 'N' as u32, + KeyCode::KeyO => 'O' as u32, + KeyCode::KeyP => 'P' as u32, + KeyCode::KeyQ => 'Q' as u32, + KeyCode::KeyR => 'R' as u32, + KeyCode::KeyS => 'S' as u32, + KeyCode::KeyT => 'T' as u32, + KeyCode::KeyU => 'U' as u32, + KeyCode::KeyV => 'V' as u32, + KeyCode::KeyW => 'W' as u32, + KeyCode::KeyX => 'X' as u32, + KeyCode::KeyY => 'Y' as u32, + KeyCode::KeyZ => 'Z' as u32, + KeyCode::Backslash => keysym::XK_backslash, + KeyCode::BracketLeft => keysym::XK_bracketleft, + KeyCode::BracketRight => keysym::XK_bracketright, + KeyCode::Comma => keysym::XK_comma, + KeyCode::Digit0 => '0' as u32, + KeyCode::Digit1 => '1' as u32, + KeyCode::Digit2 => '2' as u32, + KeyCode::Digit3 => '3' as u32, + KeyCode::Digit4 => '4' as u32, + KeyCode::Digit5 => '5' as u32, + KeyCode::Digit6 => '6' as u32, + KeyCode::Digit7 => '7' as u32, + KeyCode::Digit8 => '8' as u32, + KeyCode::Digit9 => '9' as u32, + KeyCode::Equal => keysym::XK_equal, + KeyCode::IntlBackslash => keysym::XK_backslash, + KeyCode::Minus => keysym::XK_minus, + KeyCode::Period => keysym::XK_period, + KeyCode::Quote => keysym::XK_leftsinglequotemark, + KeyCode::Semicolon => keysym::XK_semicolon, + KeyCode::Slash => keysym::XK_slash, + KeyCode::Backspace => keysym::XK_BackSpace, + KeyCode::CapsLock => keysym::XK_Caps_Lock, + KeyCode::Enter => keysym::XK_Return, + KeyCode::Space => keysym::XK_space, + KeyCode::Tab => keysym::XK_Tab, + KeyCode::Delete => keysym::XK_Delete, + KeyCode::End => keysym::XK_End, + KeyCode::Home => keysym::XK_Home, + KeyCode::Insert => keysym::XK_Insert, + KeyCode::PageDown => keysym::XK_Page_Down, + KeyCode::PageUp => keysym::XK_Page_Up, + KeyCode::ArrowDown => keysym::XK_Down, + KeyCode::ArrowLeft => keysym::XK_Left, + KeyCode::ArrowRight => keysym::XK_Right, + KeyCode::ArrowUp => keysym::XK_Up, + KeyCode::Numpad0 => keysym::XK_KP_0, + KeyCode::Numpad1 => keysym::XK_KP_1, + KeyCode::Numpad2 => keysym::XK_KP_2, + KeyCode::Numpad3 => keysym::XK_KP_3, + KeyCode::Numpad4 => keysym::XK_KP_4, + KeyCode::Numpad5 => keysym::XK_KP_5, + KeyCode::Numpad6 => keysym::XK_KP_6, + KeyCode::Numpad7 => keysym::XK_KP_7, + KeyCode::Numpad8 => keysym::XK_KP_8, + KeyCode::Numpad9 => keysym::XK_KP_9, + KeyCode::NumpadAdd => keysym::XK_KP_Add, + KeyCode::NumpadDecimal => keysym::XK_KP_Decimal, + KeyCode::NumpadDivide => keysym::XK_KP_Divide, + KeyCode::NumpadMultiply => keysym::XK_KP_Multiply, + KeyCode::NumpadSubtract => keysym::XK_KP_Subtract, + KeyCode::Escape => keysym::XK_Escape, + KeyCode::PrintScreen => keysym::XK_Print, + KeyCode::ScrollLock => keysym::XK_Scroll_Lock, + KeyCode::Pause => keysym::XF86XK_AudioPlay, + KeyCode::MediaStop => keysym::XF86XK_AudioStop, + KeyCode::MediaTrackNext => keysym::XF86XK_AudioNext, + KeyCode::MediaTrackPrevious => keysym::XF86XK_AudioPrev, + KeyCode::AudioVolumeDown => keysym::XF86XK_AudioLowerVolume, + KeyCode::AudioVolumeMute => keysym::XF86XK_AudioMute, + KeyCode::AudioVolumeUp => keysym::XF86XK_AudioRaiseVolume, + KeyCode::F1 => keysym::XK_F1, + KeyCode::F2 => keysym::XK_F2, + KeyCode::F3 => keysym::XK_F3, + KeyCode::F4 => keysym::XK_F4, + KeyCode::F5 => keysym::XK_F5, + KeyCode::F6 => keysym::XK_F6, + KeyCode::F7 => keysym::XK_F7, + KeyCode::F8 => keysym::XK_F8, + KeyCode::F9 => keysym::XK_F9, + KeyCode::F10 => keysym::XK_F10, + KeyCode::F11 => keysym::XK_F11, + KeyCode::F12 => keysym::XK_F12, + + _ => return None, + }) +} diff --git a/src/platform_impl/linux/keyboard.rs b/src/platform_impl/linux/keyboard.rs new file mode 100644 index 000000000..20cd5cfb0 --- /dev/null +++ b/src/platform_impl/linux/keyboard.rs @@ -0,0 +1,328 @@ +use super::KeyEventExtra; +use crate::{ + event::{ElementState, KeyEvent}, + keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NativeKeyCode}, +}; +use gdk::{keys::constants::*, EventKey}; +use std::{ + collections::HashSet, + ffi::c_void, + os::raw::{c_int, c_uint}, + ptr, slice, + sync::Mutex, +}; + +pub type RawKey = gdk::keys::Key; + +lazy_static! { + static ref KEY_STRINGS: Mutex> = Mutex::new(HashSet::new()); +} + +fn insert_or_get_key_str(string: String) -> &'static str { + let mut string_set = KEY_STRINGS.lock().unwrap(); + if let Some(contained) = string_set.get(string.as_str()) { + return contained; + } + let static_str = Box::leak(string.into_boxed_str()); + string_set.insert(static_str); + static_str +} + +#[allow(clippy::just_underscores_and_digits, non_upper_case_globals)] +pub(crate) fn raw_key_to_key(gdk_key: RawKey) -> Option> { + let key = match gdk_key { + Escape => Some(Key::Escape), + BackSpace => Some(Key::Backspace), + Tab | ISO_Left_Tab => Some(Key::Tab), + Return => Some(Key::Enter), + Control_L | Control_R => Some(Key::Control), + Alt_L | Alt_R => Some(Key::Alt), + Shift_L | Shift_R => Some(Key::Shift), + // TODO: investigate mapping. Map Meta_[LR]? + Super_L | Super_R => Some(Key::Super), + Caps_Lock => Some(Key::CapsLock), + F1 => Some(Key::F1), + F2 => Some(Key::F2), + F3 => Some(Key::F3), + F4 => Some(Key::F4), + F5 => Some(Key::F5), + F6 => Some(Key::F6), + F7 => Some(Key::F7), + F8 => Some(Key::F8), + F9 => Some(Key::F9), + F10 => Some(Key::F10), + F11 => Some(Key::F11), + F12 => Some(Key::F12), + + Print => Some(Key::PrintScreen), + Scroll_Lock => Some(Key::ScrollLock), + // Pause/Break not audio. + Pause => Some(Key::Pause), + + Insert => Some(Key::Insert), + Delete => Some(Key::Delete), + Home => Some(Key::Home), + End => Some(Key::End), + Page_Up => Some(Key::PageUp), + Page_Down => Some(Key::PageDown), + Num_Lock => Some(Key::NumLock), + + Up => Some(Key::ArrowUp), + Down => Some(Key::ArrowDown), + Left => Some(Key::ArrowLeft), + Right => Some(Key::ArrowRight), + Clear => Some(Key::Clear), + + Menu => Some(Key::ContextMenu), + WakeUp => Some(Key::WakeUp), + Launch0 => Some(Key::LaunchApplication1), + Launch1 => Some(Key::LaunchApplication2), + ISO_Level3_Shift => Some(Key::AltGraph), + + KP_Begin => Some(Key::Clear), + KP_Delete => Some(Key::Delete), + KP_Down => Some(Key::ArrowDown), + KP_End => Some(Key::End), + KP_Enter => Some(Key::Enter), + KP_F1 => Some(Key::F1), + KP_F2 => Some(Key::F2), + KP_F3 => Some(Key::F3), + KP_F4 => Some(Key::F4), + KP_Home => Some(Key::Home), + KP_Insert => Some(Key::Insert), + KP_Left => Some(Key::ArrowLeft), + KP_Page_Down => Some(Key::PageDown), + KP_Page_Up => Some(Key::PageUp), + KP_Right => Some(Key::ArrowRight), + // KP_Separator? What does it map to? + KP_Tab => Some(Key::Tab), + KP_Up => Some(Key::ArrowUp), + // TODO: more mappings (media etc) + _ => return None, + }; + + key +} + +#[allow(clippy::just_underscores_and_digits, non_upper_case_globals)] +pub(crate) fn raw_key_to_location(raw: RawKey) -> KeyLocation { + match raw { + Control_L | Shift_L | Alt_L | Super_L | Meta_L => KeyLocation::Left, + Control_R | Shift_R | Alt_R | Super_R | Meta_R => KeyLocation::Right, + KP_0 | KP_1 | KP_2 | KP_3 | KP_4 | KP_5 | KP_6 | KP_7 | KP_8 | KP_9 | KP_Add | KP_Begin + | KP_Decimal | KP_Delete | KP_Divide | KP_Down | KP_End | KP_Enter | KP_Equal | KP_F1 + | KP_F2 | KP_F3 | KP_F4 | KP_Home | KP_Insert | KP_Left | KP_Multiply | KP_Page_Down + | KP_Page_Up | KP_Right | KP_Separator | KP_Space | KP_Subtract | KP_Tab | KP_Up => { + KeyLocation::Numpad + } + _ => KeyLocation::Standard, + } +} + +const MODIFIER_MAP: &[(Key<'static>, ModifiersState)] = &[ + (Key::Shift, ModifiersState::SHIFT), + (Key::Alt, ModifiersState::ALT), + (Key::Control, ModifiersState::CONTROL), + (Key::Super, ModifiersState::SUPER), +]; + +// we use the EventKey to extract the modifier mainly because +// we need to have the modifier before the second key is entered to follow +// other os' logic -- this way we can emit the new `ModifiersState` before +// we receive the next key, if needed the developer can update his local state. +pub(crate) fn get_modifiers(key: EventKey) -> ModifiersState { + // a keycode (scancode in Windows) is a code that refers to a physical keyboard key. + let scancode = key.get_hardware_keycode(); + // a keyval (keysym in X) is a "logical" key name, such as GDK_Enter, GDK_a, GDK_space, etc. + let keyval = key.get_keyval(); + // unicode value + let unicode = gdk::keys::keyval_to_unicode(*keyval); + // translate to tao::keyboard::Key + let key_from_code = raw_key_to_key(keyval).unwrap_or_else(|| { + if let Some(key) = unicode { + if key >= ' ' && key != '\x7f' { + Key::Character(insert_or_get_key_str(key.to_string())) + } else { + Key::Unidentified(NativeKeyCode::Gtk(scancode)) + } + } else { + Key::Unidentified(NativeKeyCode::Gtk(scancode)) + } + }); + // start with empty state + let mut result = ModifiersState::empty(); + // loop trough our modifier map + for (gdk_mod, modifier) in MODIFIER_MAP { + if key_from_code == *gdk_mod { + result |= *modifier; + } + } + result +} + +pub(crate) fn make_key_event( + key: &EventKey, + is_repeat: bool, + key_override: Option, + state: ElementState, +) -> Option { + // a keycode (scancode in Windows) is a code that refers to a physical keyboard key. + let scancode = key.get_hardware_keycode(); + // a keyval (keysym in X) is a "logical" key name, such as GDK_Enter, GDK_a, GDK_space, etc. + let keyval_without_modifiers = key.get_keyval(); + let keyval_with_modifiers = + hardware_keycode_to_keyval(scancode).unwrap_or_else(|| keyval_without_modifiers.clone()); + // get unicode value, with and without modifiers + let text_without_modifiers = gdk::keys::keyval_to_unicode(*keyval_with_modifiers.clone()); + let text_with_modifiers = gdk::keys::keyval_to_unicode(*keyval_without_modifiers); + // get physical key from the scancode (keycode) + let physical_key = key_override.unwrap_or_else(|| KeyCode::from_scancode(scancode as u32)); + + // extract key without modifier + let key_without_modifiers = raw_key_to_key(keyval_with_modifiers.clone()).unwrap_or_else(|| { + if let Some(key) = text_without_modifiers { + if key >= ' ' && key != '\x7f' { + Key::Character(insert_or_get_key_str(key.to_string())) + } else { + Key::Unidentified(NativeKeyCode::Gtk(scancode)) + } + } else { + Key::Unidentified(NativeKeyCode::Gtk(scancode)) + } + }); + + // extract the logical key + let logical_key = raw_key_to_key(keyval_without_modifiers.clone()).unwrap_or_else(|| { + if let Some(key) = text_with_modifiers { + if key >= ' ' && key != '\x7f' { + Key::Character(insert_or_get_key_str(key.to_string())) + } else { + Key::Unidentified(NativeKeyCode::Gtk(scancode)) + } + } else { + Key::Unidentified(NativeKeyCode::Gtk(scancode)) + } + }); + + // make sure we have a valid key + if !matches!(key_without_modifiers, Key::Unidentified(_)) { + let location = raw_key_to_location(keyval_with_modifiers); + let text_with_all_modifiers = + text_without_modifiers.map(|text| insert_or_get_key_str(text.to_string())); + return Some(KeyEvent { + location, + logical_key, + physical_key, + repeat: is_repeat, + state, + text: text_with_all_modifiers.clone(), + platform_specific: KeyEventExtra { + text_with_all_modifiers, + key_without_modifiers, + }, + }); + } else { + eprintln!("Couldn't get key from code: {:?}", physical_key); + } + None +} + +/// Map a hardware keycode to a keyval by performing a lookup in the keymap and finding the +/// keyval with the lowest group and level +fn hardware_keycode_to_keyval(keycode: u16) -> Option { + use glib::translate::FromGlib; + unsafe { + let keymap = gdk_sys::gdk_keymap_get_default(); + + let mut nkeys = 0; + let mut keys: *mut gdk_sys::GdkKeymapKey = ptr::null_mut(); + let mut keyvals: *mut c_uint = ptr::null_mut(); + + // call into gdk to retrieve the keyvals and keymap keys + gdk_sys::gdk_keymap_get_entries_for_keycode( + keymap, + c_uint::from(keycode), + &mut keys as *mut *mut gdk_sys::GdkKeymapKey, + &mut keyvals as *mut *mut c_uint, + &mut nkeys as *mut c_int, + ); + + if nkeys > 0 { + let keyvals_slice = slice::from_raw_parts(keyvals, nkeys as usize); + let keys_slice = slice::from_raw_parts(keys, nkeys as usize); + + let resolved_keyval = keys_slice.iter().enumerate().find_map(|(id, gdk_keymap)| { + if gdk_keymap.group == 0 && gdk_keymap.level == 0 { + Some(RawKey::from_glib(keyvals_slice[id])) + } else { + None + } + }); + + // notify glib to free the allocated arrays + glib_sys::g_free(keyvals as *mut c_void); + glib_sys::g_free(keys as *mut c_void); + + return resolved_keyval; + } + } + None +} + +#[allow(non_upper_case_globals)] +pub fn key_to_raw_key(src: &KeyCode) -> Option { + Some(match src { + KeyCode::Escape => Escape, + KeyCode::Backspace => BackSpace, + + KeyCode::Tab => Tab, + KeyCode::Enter => Return, + + KeyCode::ControlLeft => Control_L, + KeyCode::AltLeft => Alt_L, + KeyCode::ShiftLeft => Shift_L, + KeyCode::SuperLeft => Super_L, + + KeyCode::ControlRight => Control_R, + KeyCode::AltRight => Alt_R, + KeyCode::ShiftRight => Shift_R, + KeyCode::SuperRight => Super_R, + + KeyCode::CapsLock => Caps_Lock, + KeyCode::F1 => F1, + KeyCode::F2 => F2, + KeyCode::F3 => F3, + KeyCode::F4 => F4, + KeyCode::F5 => F5, + KeyCode::F6 => F6, + KeyCode::F7 => F7, + KeyCode::F8 => F8, + KeyCode::F9 => F9, + KeyCode::F10 => F10, + KeyCode::F11 => F11, + KeyCode::F12 => F12, + + KeyCode::PrintScreen => Print, + KeyCode::ScrollLock => Scroll_Lock, + // Pause/Break not audio. + KeyCode::Pause => Pause, + + KeyCode::Insert => Insert, + KeyCode::Delete => Delete, + KeyCode::Home => Home, + KeyCode::End => End, + KeyCode::PageUp => Page_Up, + KeyCode::PageDown => Page_Down, + + KeyCode::NumLock => Num_Lock, + + KeyCode::ArrowUp => Up, + KeyCode::ArrowDown => Down, + KeyCode::ArrowLeft => Left, + KeyCode::ArrowRight => Right, + + KeyCode::ContextMenu => Menu, + KeyCode::WakeUp => WakeUp, + _ => return None, + }) +} diff --git a/src/platform_impl/linux/keycode.rs b/src/platform_impl/linux/keycode.rs new file mode 100644 index 000000000..442c0fba8 --- /dev/null +++ b/src/platform_impl/linux/keycode.rs @@ -0,0 +1,297 @@ +use crate::keyboard::{KeyCode, NativeKeyCode}; + +pub fn keycode_to_scancode(code: KeyCode) -> Option { + // See `from_scancode` for more info + match code { + KeyCode::Backquote => Some(0x0031), + KeyCode::Backslash => Some(0x0033), + KeyCode::Backspace => Some(0x0016), + KeyCode::BracketLeft => Some(0x0022), + KeyCode::BracketRight => Some(0x001B), + KeyCode::Comma => Some(0x003B), + KeyCode::Digit0 => Some(0x0013), + KeyCode::Digit1 => Some(0x000A), + KeyCode::Digit2 => Some(0x000B), + KeyCode::Digit3 => Some(0x000C), + KeyCode::Digit4 => Some(0x000D), + KeyCode::Digit5 => Some(0x000E), + KeyCode::Digit6 => Some(0x000F), + KeyCode::Digit7 => Some(0x0010), + KeyCode::Digit8 => Some(0x0011), + KeyCode::Digit9 => Some(0x0012), + KeyCode::Equal => Some(0x0015), + KeyCode::IntlBackslash => Some(0x005E), + KeyCode::IntlRo => Some(0x0061), + KeyCode::IntlYen => Some(0x0084), + KeyCode::KeyA => Some(0x0026), + KeyCode::KeyB => Some(0x0038), + KeyCode::KeyC => Some(0x0036), + KeyCode::KeyD => Some(0x0028), + KeyCode::KeyE => Some(0x001A), + KeyCode::KeyF => Some(0x0029), + KeyCode::KeyG => Some(0x002A), + KeyCode::KeyH => Some(0x002B), + KeyCode::KeyI => Some(0x001F), + KeyCode::KeyJ => Some(0x002C), + KeyCode::KeyK => Some(0x002D), + KeyCode::KeyL => Some(0x002E), + KeyCode::KeyM => Some(0x002E), + KeyCode::KeyN => Some(0x0039), + KeyCode::KeyO => Some(0x0020), + KeyCode::KeyP => Some(0x0021), + KeyCode::KeyQ => Some(0x0018), + KeyCode::KeyR => Some(0x001B), + KeyCode::KeyS => Some(0x0027), + KeyCode::KeyT => Some(0x001C), + KeyCode::KeyU => Some(0x001E), + KeyCode::KeyV => Some(0x0037), + KeyCode::KeyW => Some(0x0019), + KeyCode::KeyX => Some(0x0035), + KeyCode::KeyY => Some(0x001D), + KeyCode::KeyZ => Some(0x0034), + KeyCode::Minus => Some(0x0014), + KeyCode::Period => Some(0x003C), + KeyCode::Quote => Some(0x0030), + KeyCode::Semicolon => Some(0x002F), + KeyCode::Slash => Some(0x003D), + KeyCode::AltLeft => Some(0x0040), + KeyCode::AltRight => Some(0x006C), + KeyCode::CapsLock => Some(0x0042), + KeyCode::ContextMenu => Some(0x0087), + KeyCode::ControlLeft => Some(0x0025), + KeyCode::ControlRight => Some(0x0069), + KeyCode::Enter => Some(0x0024), + KeyCode::SuperLeft => Some(0x0085), + KeyCode::SuperRight => Some(0x0086), + KeyCode::ShiftLeft => Some(0x0032), + KeyCode::ShiftRight => Some(0x003E), + KeyCode::Space => Some(0x0041), + KeyCode::Tab => Some(0x0017), + KeyCode::Convert => Some(0x0064), + KeyCode::Lang1 => Some(0x0082), + KeyCode::Lang2 => Some(0x0083), + KeyCode::KanaMode => Some(0x0065), + KeyCode::NonConvert => Some(0x0066), + KeyCode::Delete => Some(0x0077), + KeyCode::End => Some(0x0073), + KeyCode::Home => Some(0x006E), + KeyCode::Insert => Some(0x0076), + KeyCode::PageDown => Some(0x0075), + KeyCode::PageUp => Some(0x0070), + KeyCode::ArrowDown => Some(0x0074), + KeyCode::ArrowLeft => Some(0x0071), + KeyCode::ArrowRight => Some(0x0072), + KeyCode::ArrowUp => Some(0x006F), + KeyCode::NumLock => Some(0x004D), + KeyCode::Numpad0 => Some(0x005A), + KeyCode::Numpad1 => Some(0x0057), + KeyCode::Numpad2 => Some(0x0058), + KeyCode::Numpad3 => Some(0x0059), + KeyCode::Numpad4 => Some(0x0058), + KeyCode::Numpad5 => Some(0x0053), + KeyCode::Numpad6 => Some(0x0054), + KeyCode::Numpad7 => Some(0x0055), + KeyCode::Numpad8 => Some(0x0050), + KeyCode::Numpad9 => Some(0x0051), + KeyCode::NumpadAdd => Some(0x0056), + KeyCode::NumpadComma => Some(0x0081), + KeyCode::NumpadDecimal => Some(0x005B), + KeyCode::NumpadDivide => Some(0x006A), + KeyCode::NumpadEnter => Some(0x0068), + KeyCode::NumpadEqual => Some(0x007D), + KeyCode::NumpadMultiply => Some(0x003F), + KeyCode::NumpadSubtract => Some(0x0052), + KeyCode::Escape => Some(0x0009), + KeyCode::F1 => Some(0x0043), + KeyCode::F2 => Some(0x0044), + KeyCode::F3 => Some(0x0045), + KeyCode::F4 => Some(0x0046), + KeyCode::F5 => Some(0x0047), + KeyCode::F6 => Some(0x0048), + KeyCode::F7 => Some(0x0049), + KeyCode::F8 => Some(0x004A), + KeyCode::F9 => Some(0x004B), + KeyCode::F10 => Some(0x004C), + KeyCode::F11 => Some(0x005F), + KeyCode::F12 => Some(0x0060), + KeyCode::PrintScreen => Some(0x006B), + KeyCode::ScrollLock => Some(0x004E), + KeyCode::Pause => Some(0x007F), + KeyCode::BrowserBack => Some(0x00A6), + KeyCode::BrowserFavorites => Some(0x00A4), + KeyCode::BrowserForward => Some(0x00A7), + KeyCode::BrowserHome => Some(0x00B4), + KeyCode::BrowserRefresh => Some(0x00B5), + KeyCode::BrowserSearch => Some(0x00E1), + KeyCode::BrowserStop => Some(0x0088), + + KeyCode::LaunchApp1 => Some(0x0098), + KeyCode::LaunchApp2 => Some(0x0094), + KeyCode::LaunchMail => Some(0x00A3), + KeyCode::MediaPlayPause => Some(0x00AC), + KeyCode::MediaSelect => Some(0x00B3), + KeyCode::MediaStop => Some(0x00AE), + KeyCode::MediaTrackNext => Some(0x00AB), + KeyCode::MediaTrackPrevious => Some(0x00AD), + KeyCode::AudioVolumeDown => Some(0x007A), + KeyCode::AudioVolumeMute => Some(0x0079), + KeyCode::AudioVolumeUp => Some(0x007B), + KeyCode::Unidentified(NativeKeyCode::Gtk(scancode)) => Some(scancode as u32), + _ => None, + } +} + +pub fn keycode_from_scancode(scancode: u32) -> KeyCode { + // See: https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html + // and: https://www.w3.org/TR/uievents-code/ + // and: The widget/NativeKeyToDOMCodeName.h file in the firefox source + match scancode { + 0x0009 => KeyCode::Escape, + 0x000A => KeyCode::Digit1, + 0x000B => KeyCode::Digit2, + 0x000C => KeyCode::Digit3, + 0x000D => KeyCode::Digit4, + 0x000E => KeyCode::Digit5, + 0x000F => KeyCode::Digit6, + 0x0010 => KeyCode::Digit7, + 0x0011 => KeyCode::Digit8, + 0x0012 => KeyCode::Digit9, + 0x0013 => KeyCode::Digit0, + 0x0014 => KeyCode::Minus, + 0x0015 => KeyCode::Equal, + 0x0016 => KeyCode::Backspace, + 0x0017 => KeyCode::Tab, + 0x0018 => KeyCode::KeyQ, + 0x0019 => KeyCode::KeyW, + 0x001A => KeyCode::KeyE, + 0x001B => KeyCode::KeyR, + 0x001C => KeyCode::KeyT, + 0x001D => KeyCode::KeyY, + 0x001E => KeyCode::KeyU, + 0x001F => KeyCode::KeyI, + 0x0020 => KeyCode::KeyO, + 0x0021 => KeyCode::KeyP, + 0x0022 => KeyCode::BracketLeft, + 0x0023 => KeyCode::BracketRight, + 0x0024 => KeyCode::Enter, + 0x0025 => KeyCode::ControlLeft, + 0x0026 => KeyCode::KeyA, + 0x0027 => KeyCode::KeyS, + 0x0028 => KeyCode::KeyD, + 0x0029 => KeyCode::KeyF, + 0x002A => KeyCode::KeyG, + 0x002B => KeyCode::KeyH, + 0x002C => KeyCode::KeyJ, + 0x002D => KeyCode::KeyK, + 0x002E => KeyCode::KeyL, + 0x002F => KeyCode::Semicolon, + 0x0030 => KeyCode::Quote, + 0x0031 => KeyCode::Backquote, + 0x0032 => KeyCode::ShiftLeft, + 0x0033 => KeyCode::Backslash, + 0x0034 => KeyCode::KeyZ, + 0x0035 => KeyCode::KeyX, + 0x0036 => KeyCode::KeyC, + 0x0037 => KeyCode::KeyV, + 0x0038 => KeyCode::KeyB, + 0x0039 => KeyCode::KeyN, + 0x003A => KeyCode::KeyM, + 0x003B => KeyCode::Comma, + 0x003C => KeyCode::Period, + 0x003D => KeyCode::Slash, + 0x003E => KeyCode::ShiftRight, + 0x003F => KeyCode::NumpadMultiply, + 0x0040 => KeyCode::AltLeft, + 0x0041 => KeyCode::Space, + 0x0042 => KeyCode::CapsLock, + 0x0043 => KeyCode::F1, + 0x0044 => KeyCode::F2, + 0x0045 => KeyCode::F3, + 0x0046 => KeyCode::F4, + 0x0047 => KeyCode::F5, + 0x0048 => KeyCode::F6, + 0x0049 => KeyCode::F7, + 0x004A => KeyCode::F8, + 0x004B => KeyCode::F9, + 0x004C => KeyCode::F10, + 0x004D => KeyCode::NumLock, + 0x004E => KeyCode::ScrollLock, + 0x004F => KeyCode::Numpad7, + 0x0050 => KeyCode::Numpad8, + 0x0051 => KeyCode::Numpad9, + 0x0052 => KeyCode::NumpadSubtract, + 0x0053 => KeyCode::Numpad4, + 0x0054 => KeyCode::Numpad5, + 0x0055 => KeyCode::Numpad6, + 0x0056 => KeyCode::NumpadAdd, + 0x0057 => KeyCode::Numpad1, + 0x0058 => KeyCode::Numpad2, + 0x0059 => KeyCode::Numpad3, + 0x005A => KeyCode::Numpad0, + 0x005B => KeyCode::NumpadDecimal, + 0x005E => KeyCode::IntlBackslash, + 0x005F => KeyCode::F11, + 0x0060 => KeyCode::F12, + 0x0061 => KeyCode::IntlRo, + 0x0064 => KeyCode::Convert, + 0x0065 => KeyCode::KanaMode, + 0x0066 => KeyCode::NonConvert, + 0x0068 => KeyCode::NumpadEnter, + 0x0069 => KeyCode::ControlRight, + 0x006A => KeyCode::NumpadDivide, + 0x006B => KeyCode::PrintScreen, + 0x006C => KeyCode::AltRight, + 0x006E => KeyCode::Home, + 0x006F => KeyCode::ArrowUp, + 0x0070 => KeyCode::PageUp, + 0x0071 => KeyCode::ArrowLeft, + 0x0072 => KeyCode::ArrowRight, + 0x0073 => KeyCode::End, + 0x0074 => KeyCode::ArrowDown, + 0x0075 => KeyCode::PageDown, + 0x0076 => KeyCode::Insert, + 0x0077 => KeyCode::Delete, + 0x0079 => KeyCode::AudioVolumeMute, + 0x007A => KeyCode::AudioVolumeDown, + 0x007B => KeyCode::AudioVolumeUp, + 0x007D => KeyCode::NumpadEqual, + 0x007F => KeyCode::Pause, + 0x0081 => KeyCode::NumpadComma, + 0x0082 => KeyCode::Lang1, + 0x0083 => KeyCode::Lang2, + 0x0084 => KeyCode::IntlYen, + 0x0085 => KeyCode::SuperLeft, + 0x0086 => KeyCode::SuperRight, + 0x0087 => KeyCode::ContextMenu, + 0x0088 => KeyCode::BrowserStop, + 0x0089 => KeyCode::Again, + 0x008A => KeyCode::Props, + 0x008B => KeyCode::Undo, + 0x008C => KeyCode::Select, + 0x008D => KeyCode::Copy, + 0x008E => KeyCode::Open, + 0x008F => KeyCode::Paste, + 0x0090 => KeyCode::Find, + 0x0091 => KeyCode::Cut, + 0x0092 => KeyCode::Help, + 0x0094 => KeyCode::LaunchApp2, + 0x0097 => KeyCode::WakeUp, + 0x0098 => KeyCode::LaunchApp1, + // key to right of volume controls on T430s produces 0x9C + // but no documentation of what it should map to :/ + 0x00A3 => KeyCode::LaunchMail, + 0x00A4 => KeyCode::BrowserFavorites, + 0x00A6 => KeyCode::BrowserBack, + 0x00A7 => KeyCode::BrowserForward, + 0x00A9 => KeyCode::Eject, + 0x00AB => KeyCode::MediaTrackNext, + 0x00AC => KeyCode::MediaPlayPause, + 0x00AD => KeyCode::MediaTrackPrevious, + 0x00AE => KeyCode::MediaStop, + 0x00B3 => KeyCode::MediaSelect, + 0x00B4 => KeyCode::BrowserHome, + 0x00B5 => KeyCode::BrowserRefresh, + 0x00E1 => KeyCode::BrowserSearch, + _ => KeyCode::Unidentified(NativeKeyCode::Gtk(scancode as u16)), + } +} diff --git a/src/platform_impl/linux/menu.rs b/src/platform_impl/linux/menu.rs index 26a14ff71..383070c02 100644 --- a/src/platform_impl/linux/menu.rs +++ b/src/platform_impl/linux/menu.rs @@ -7,8 +7,15 @@ use gtk::{ SeparatorMenuItem, }; -use super::window::{WindowId, WindowRequest}; -use crate::menu::{CustomMenuItem, MenuId, MenuItem, MenuType}; +use super::{ + keyboard::key_to_raw_key, + window::{WindowId, WindowRequest}, +}; +use crate::{ + accelerator::Accelerator, + keyboard::{KeyCode, ModifiersState}, + menu::{CustomMenuItem, MenuId, MenuItem, MenuType}, +}; macro_rules! menuitem { ( $description:expr, $key:expr, $accel_group:ident ) => {{ @@ -52,7 +59,7 @@ unsafe impl Sync for Menu {} #[derive(Debug, Clone)] pub struct MenuItemAttributes { id: MenuId, - key: Option, + key: Option, selected: bool, enabled: bool, menu_type: MenuType, @@ -100,7 +107,7 @@ impl Menu { &mut self, menu_id: MenuId, title: &str, - accelerators: Option<&str>, + accelerators: Option, enabled: bool, selected: bool, menu_type: MenuType, @@ -113,7 +120,7 @@ impl Menu { }; let custom_menu = MenuItemAttributes { id: menu_id, - key: accelerators.map(|a| a.to_string()), + key: accelerators, enabled, selected, menu_type, @@ -201,8 +208,7 @@ impl Menu { .. } => { if let Some(key) = key { - let (key, mods) = gtk::accelerator_parse(&key); - gtk_item.add_accelerator("activate", accel_group, key, mods, AccelFlags::VISIBLE); + register_accelerator(>k_item, accel_group, key); } gtk_item.set_sensitive(enabled); @@ -277,3 +283,71 @@ impl Menu { } } } + +fn register_accelerator(item: &GtkMenuItem, accel_group: &AccelGroup, menu_key: Accelerator) { + let accel_key = match &menu_key.key { + KeyCode::KeyA => 'A' as u32, + KeyCode::KeyB => 'B' as u32, + KeyCode::KeyC => 'C' as u32, + KeyCode::KeyD => 'D' as u32, + KeyCode::KeyE => 'E' as u32, + KeyCode::KeyF => 'F' as u32, + KeyCode::KeyG => 'G' as u32, + KeyCode::KeyH => 'H' as u32, + KeyCode::KeyI => 'I' as u32, + KeyCode::KeyJ => 'J' as u32, + KeyCode::KeyK => 'K' as u32, + KeyCode::KeyL => 'L' as u32, + KeyCode::KeyM => 'M' as u32, + KeyCode::KeyN => 'N' as u32, + KeyCode::KeyO => 'O' as u32, + KeyCode::KeyP => 'P' as u32, + KeyCode::KeyQ => 'Q' as u32, + KeyCode::KeyR => 'R' as u32, + KeyCode::KeyS => 'S' as u32, + KeyCode::KeyT => 'T' as u32, + KeyCode::KeyU => 'U' as u32, + KeyCode::KeyV => 'V' as u32, + KeyCode::KeyW => 'W' as u32, + KeyCode::KeyX => 'X' as u32, + KeyCode::KeyY => 'Y' as u32, + KeyCode::KeyZ => 'Z' as u32, + KeyCode::Digit0 => '0' as u32, + KeyCode::Digit1 => '1' as u32, + KeyCode::Digit2 => '2' as u32, + KeyCode::Digit3 => '3' as u32, + KeyCode::Digit4 => '4' as u32, + KeyCode::Digit5 => '5' as u32, + KeyCode::Digit6 => '6' as u32, + KeyCode::Digit7 => '7' as u32, + KeyCode::Digit8 => '8' as u32, + KeyCode::Digit9 => '9' as u32, + k => { + if let Some(gdk_key) = key_to_raw_key(k) { + *gdk_key + } else { + dbg!("Cannot map key {:?}", k); + return; + } + } + }; + + item.add_accelerator( + "activate", + accel_group, + accel_key, + modifiers_to_gdk_modifier_type(menu_key.mods), + gtk::AccelFlags::VISIBLE, + ); +} + +fn modifiers_to_gdk_modifier_type(modifiers: ModifiersState) -> gdk::ModifierType { + let mut result = gdk::ModifierType::empty(); + + result.set(gdk::ModifierType::MOD1_MASK, modifiers.alt_key()); + result.set(gdk::ModifierType::CONTROL_MASK, modifiers.control_key()); + result.set(gdk::ModifierType::SHIFT_MASK, modifiers.shift_key()); + result.set(gdk::ModifierType::META_MASK, modifiers.super_key()); + + result +} diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 71c8a8c08..17e9da549 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -11,6 +11,9 @@ mod clipboard; mod event_loop; +mod global_shortcut; +mod keyboard; +mod keycode; mod menu; mod monitor; #[cfg(feature = "tray")] @@ -21,6 +24,8 @@ mod window; pub use self::system_tray::{SystemTray, SystemTrayBuilder}; pub use self::{ clipboard::Clipboard, + global_shortcut::{GlobalShortcut, ShortcutManager}, + keycode::{keycode_from_scancode, keycode_to_scancode}, menu::{Menu, MenuItemAttributes}, }; pub use event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}; @@ -29,6 +34,14 @@ pub use window::{ hit_test, PlatformIcon, PlatformSpecificWindowBuilderAttributes, Window, WindowId, }; +use crate::keyboard::Key; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option<&'static str>, + pub key_without_modifiers: Key<'static>, +} + #[derive(Debug, Clone)] pub struct OsError; diff --git a/src/platform_impl/linux/window.rs b/src/platform_impl/linux/window.rs index 8e56eb310..438c3b7e9 100644 --- a/src/platform_impl/linux/window.rs +++ b/src/platform_impl/linux/window.rs @@ -8,13 +8,14 @@ use std::{ sync::atomic::{AtomicBool, AtomicI32, Ordering}, }; -use gdk::{Cursor, EventMask, WindowEdge, WindowExt, WindowState}; +use gdk::{Cursor, EventKey, EventMask, WindowEdge, WindowExt, WindowState}; use gdk_pixbuf::{Colorspace, Pixbuf}; use gtk::{prelude::*, AccelGroup, ApplicationWindow, Orientation}; use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, + event::ElementState, icon::{BadIcon, Icon}, menu::{MenuId, MenuItem}, monitor::MonitorHandle as RootMonitorHandle, @@ -290,10 +291,37 @@ impl Window { scale_factor_clone.store(window.get_scale_factor(), Ordering::Release); }); - if let Err(e) = window_requests_tx.send((window_id, WindowRequest::WireUpEvents)) { + if let Err(e) = window_requests_tx + .clone() + .send((window_id, WindowRequest::WireUpEvents)) + { log::warn!("Fail to send wire up events request: {}", e); } + let window_requests_tx_clone = window_requests_tx.clone(); + window.connect_key_press_event(move |_window, event_key| { + if let Err(e) = window_requests_tx_clone.send(( + window_id, + WindowRequest::KeyboardInput((event_key.to_owned(), ElementState::Pressed)), + )) { + log::warn!("Fail to send user attention request: {}", e); + } + + Inhibit(false) + }); + + let window_requests_tx_clone = window_requests_tx.clone(); + window.connect_key_release_event(move |_window, event_key| { + if let Err(e) = window_requests_tx_clone.send(( + window_id, + WindowRequest::KeyboardInput((event_key.to_owned(), ElementState::Released)), + )) { + log::warn!("Fail to send user attention request: {}", e); + } + + Inhibit(false) + }); + window.queue_draw(); Ok(Self { window_id, @@ -651,6 +679,8 @@ pub enum WindowRequest { Redraw, Menu((Option, Option)), SetMenu((Option, AccelGroup, gtk::MenuBar)), + KeyboardInput((EventKey, ElementState)), + GlobalHotKey(u16), } pub fn hit_test(window: &gdk::Window, cx: f64, cy: f64) -> WindowEdge { diff --git a/src/platform_impl/macos/carbon_hotkey/carbon_hotkey_binding.c b/src/platform_impl/macos/carbon_hotkey/carbon_hotkey_binding.c new file mode 100644 index 000000000..800872365 --- /dev/null +++ b/src/platform_impl/macos/carbon_hotkey/carbon_hotkey_binding.c @@ -0,0 +1,66 @@ + +#include "carbon_hotkey_binding.h" + +#include + +HotkeyCallback saved_callback = NULL; +void *saved_closure = NULL; + +int hotkey_handler( + EventHandlerCallRef next_handler, EventRef event, void *user_data) +{ + (void)(next_handler); + (void)(user_data); + EventHotKeyID event_hotkey; + + int result = GetEventParameter(event, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(event_hotkey), NULL, &event_hotkey); + if (result == noErr && saved_callback && saved_closure) + { + saved_callback(event_hotkey.id, saved_closure); + } + return noErr; +} + +void *install_event_handler(HotkeyCallback callback, void *data) +{ + if (!callback || !data) + return NULL; + saved_callback = callback; + saved_closure = data; + EventTypeSpec event_type; + event_type.eventClass = kEventClassKeyboard; + event_type.eventKind = kEventHotKeyPressed; + EventHandlerRef handler_ref; + int result = InstallEventHandler(GetApplicationEventTarget(), &hotkey_handler, 1, &event_type, data, &handler_ref); + + if (result == noErr) + { + return handler_ref; + } + + return NULL; +} + +int uninstall_event_handler(void *handler_ref) +{ + return RemoveEventHandler(handler_ref); +} + +void *register_hotkey(int id, int modifier, int key) +{ + EventHotKeyRef hotkey_ref; + EventHotKeyID hotkey_id; + hotkey_id.signature = 'htrs'; + hotkey_id.id = id; + int result = RegisterEventHotKey(key, modifier, hotkey_id, + GetApplicationEventTarget(), 0, &hotkey_ref); + if (result == noErr) + return hotkey_ref; + + return NULL; +} + +int unregister_hotkey(void *hotkey_ref) +{ + return UnregisterEventHotKey(hotkey_ref); +} diff --git a/src/platform_impl/macos/carbon_hotkey/carbon_hotkey_binding.h b/src/platform_impl/macos/carbon_hotkey/carbon_hotkey_binding.h new file mode 100644 index 000000000..4ea4e86c6 --- /dev/null +++ b/src/platform_impl/macos/carbon_hotkey/carbon_hotkey_binding.h @@ -0,0 +1,6 @@ +typedef void (*HotkeyCallback)(int, void *); + +void *install_event_handler(HotkeyCallback callback, void *data); +int uninstall_event_handler(void *event_handler_ref); +void *register_hotkey(int id, int modifier, int key); +int unregister_hotkey(void *hotkey_ref); diff --git a/src/platform_impl/macos/event.rs b/src/platform_impl/macos/event.rs index 557306132..7b57befbb 100644 --- a/src/platform_impl/macos/event.rs +++ b/src/platform_impl/macos/event.rs @@ -1,22 +1,39 @@ // Copyright 2019-2021 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 -use std::os::raw::c_ushort; +use std::{collections::HashSet, ffi::c_void, os::raw::c_ushort, sync::Mutex}; use cocoa::{ appkit::{NSEvent, NSEventModifierFlags}, base::id, }; +use core_foundation::{base::CFRelease, data::CFDataGetBytePtr}; + use crate::{ dpi::LogicalSize, - event::{ElementState, Event, KeyboardInput, ModifiersState, VirtualKeyCode, WindowEvent}, + event::{ElementState, Event, KeyEvent}, + keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NativeKeyCode}, platform_impl::platform::{ - util::{IdRef, Never}, - DEVICE_ID, + ffi, + util::{ns_string_to_rust, IdRef, Never}, }, }; +lazy_static! { + static ref KEY_STRINGS: Mutex> = Mutex::new(HashSet::new()); +} + +fn insert_or_get_key_str(string: String) -> &'static str { + let mut string_set = KEY_STRINGS.lock().unwrap(); + if let Some(contained) = string_set.get(string.as_str()) { + return contained; + } + let static_str = Box::leak(string.into_boxed_str()); + string_set.insert(static_str); + static_str +} + #[derive(Debug)] pub enum EventWrapper { StaticEvent(Event<'static, Never>), @@ -32,216 +49,259 @@ pub enum EventProxy { }, } -pub fn char_to_keycode(c: char) -> Option { - // We only translate keys that are affected by keyboard layout. - // - // Note that since keys are translated in a somewhat "dumb" way (reading character) - // there is a concern that some combination, i.e. Cmd+char, causes the wrong - // letter to be received, and so we receive the wrong key. - // - // Implementation reference: https://github.com/WebKit/webkit/blob/82bae82cf0f329dbe21059ef0986c4e92fea4ba6/Source/WebCore/platform/cocoa/KeyEventCocoa.mm#L626 - Some(match c { - 'a' | 'A' => VirtualKeyCode::A, - 'b' | 'B' => VirtualKeyCode::B, - 'c' | 'C' => VirtualKeyCode::C, - 'd' | 'D' => VirtualKeyCode::D, - 'e' | 'E' => VirtualKeyCode::E, - 'f' | 'F' => VirtualKeyCode::F, - 'g' | 'G' => VirtualKeyCode::G, - 'h' | 'H' => VirtualKeyCode::H, - 'i' | 'I' => VirtualKeyCode::I, - 'j' | 'J' => VirtualKeyCode::J, - 'k' | 'K' => VirtualKeyCode::K, - 'l' | 'L' => VirtualKeyCode::L, - 'm' | 'M' => VirtualKeyCode::M, - 'n' | 'N' => VirtualKeyCode::N, - 'o' | 'O' => VirtualKeyCode::O, - 'p' | 'P' => VirtualKeyCode::P, - 'q' | 'Q' => VirtualKeyCode::Q, - 'r' | 'R' => VirtualKeyCode::R, - 's' | 'S' => VirtualKeyCode::S, - 't' | 'T' => VirtualKeyCode::T, - 'u' | 'U' => VirtualKeyCode::U, - 'v' | 'V' => VirtualKeyCode::V, - 'w' | 'W' => VirtualKeyCode::W, - 'x' | 'X' => VirtualKeyCode::X, - 'y' | 'Y' => VirtualKeyCode::Y, - 'z' | 'Z' => VirtualKeyCode::Z, - '1' | '!' => VirtualKeyCode::Key1, - '2' | '@' => VirtualKeyCode::Key2, - '3' | '#' => VirtualKeyCode::Key3, - '4' | '$' => VirtualKeyCode::Key4, - '5' | '%' => VirtualKeyCode::Key5, - '6' | '^' => VirtualKeyCode::Key6, - '7' | '&' => VirtualKeyCode::Key7, - '8' | '*' => VirtualKeyCode::Key8, - '9' | '(' => VirtualKeyCode::Key9, - '0' | ')' => VirtualKeyCode::Key0, - '=' | '+' => VirtualKeyCode::Equals, - '-' | '_' => VirtualKeyCode::Minus, - ']' | '}' => VirtualKeyCode::RBracket, - '[' | '{' => VirtualKeyCode::LBracket, - '\'' | '"' => VirtualKeyCode::Apostrophe, - ';' | ':' => VirtualKeyCode::Semicolon, - '\\' | '|' => VirtualKeyCode::Backslash, - ',' | '<' => VirtualKeyCode::Comma, - '/' | '?' => VirtualKeyCode::Slash, - '.' | '>' => VirtualKeyCode::Period, - '`' | '~' => VirtualKeyCode::Grave, - _ => return None, - }) +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option<&'static str>, + pub key_without_modifiers: Key<'static>, } -pub fn scancode_to_keycode(scancode: c_ushort) -> Option { - Some(match scancode { - 0x00 => VirtualKeyCode::A, - 0x01 => VirtualKeyCode::S, - 0x02 => VirtualKeyCode::D, - 0x03 => VirtualKeyCode::F, - 0x04 => VirtualKeyCode::H, - 0x05 => VirtualKeyCode::G, - 0x06 => VirtualKeyCode::Z, - 0x07 => VirtualKeyCode::X, - 0x08 => VirtualKeyCode::C, - 0x09 => VirtualKeyCode::V, - //0x0a => World 1, - 0x0b => VirtualKeyCode::B, - 0x0c => VirtualKeyCode::Q, - 0x0d => VirtualKeyCode::W, - 0x0e => VirtualKeyCode::E, - 0x0f => VirtualKeyCode::R, - 0x10 => VirtualKeyCode::Y, - 0x11 => VirtualKeyCode::T, - 0x12 => VirtualKeyCode::Key1, - 0x13 => VirtualKeyCode::Key2, - 0x14 => VirtualKeyCode::Key3, - 0x15 => VirtualKeyCode::Key4, - 0x16 => VirtualKeyCode::Key6, - 0x17 => VirtualKeyCode::Key5, - 0x18 => VirtualKeyCode::Equals, - 0x19 => VirtualKeyCode::Key9, - 0x1a => VirtualKeyCode::Key7, - 0x1b => VirtualKeyCode::Minus, - 0x1c => VirtualKeyCode::Key8, - 0x1d => VirtualKeyCode::Key0, - 0x1e => VirtualKeyCode::RBracket, - 0x1f => VirtualKeyCode::O, - 0x20 => VirtualKeyCode::U, - 0x21 => VirtualKeyCode::LBracket, - 0x22 => VirtualKeyCode::I, - 0x23 => VirtualKeyCode::P, - 0x24 => VirtualKeyCode::Return, - 0x25 => VirtualKeyCode::L, - 0x26 => VirtualKeyCode::J, - 0x27 => VirtualKeyCode::Apostrophe, - 0x28 => VirtualKeyCode::K, - 0x29 => VirtualKeyCode::Semicolon, - 0x2a => VirtualKeyCode::Backslash, - 0x2b => VirtualKeyCode::Comma, - 0x2c => VirtualKeyCode::Slash, - 0x2d => VirtualKeyCode::N, - 0x2e => VirtualKeyCode::M, - 0x2f => VirtualKeyCode::Period, - 0x30 => VirtualKeyCode::Tab, - 0x31 => VirtualKeyCode::Space, - 0x32 => VirtualKeyCode::Grave, - 0x33 => VirtualKeyCode::Back, - //0x34 => unkown, - 0x35 => VirtualKeyCode::Escape, - 0x36 => VirtualKeyCode::RWin, - 0x37 => VirtualKeyCode::LWin, - 0x38 => VirtualKeyCode::LShift, - //0x39 => Caps lock, - 0x3a => VirtualKeyCode::LAlt, - 0x3b => VirtualKeyCode::LControl, - 0x3c => VirtualKeyCode::RShift, - 0x3d => VirtualKeyCode::RAlt, - 0x3e => VirtualKeyCode::RControl, - //0x3f => Fn key, - 0x40 => VirtualKeyCode::F17, - 0x41 => VirtualKeyCode::NumpadDecimal, - //0x42 -> unkown, - 0x43 => VirtualKeyCode::NumpadMultiply, - //0x44 => unkown, - 0x45 => VirtualKeyCode::NumpadAdd, - //0x46 => unkown, - 0x47 => VirtualKeyCode::Numlock, - //0x48 => KeypadClear, - 0x49 => VirtualKeyCode::VolumeUp, - 0x4a => VirtualKeyCode::VolumeDown, - 0x4b => VirtualKeyCode::NumpadDivide, - 0x4c => VirtualKeyCode::NumpadEnter, - //0x4d => unkown, - 0x4e => VirtualKeyCode::NumpadSubtract, - 0x4f => VirtualKeyCode::F18, - 0x50 => VirtualKeyCode::F19, - 0x51 => VirtualKeyCode::NumpadEquals, - 0x52 => VirtualKeyCode::Numpad0, - 0x53 => VirtualKeyCode::Numpad1, - 0x54 => VirtualKeyCode::Numpad2, - 0x55 => VirtualKeyCode::Numpad3, - 0x56 => VirtualKeyCode::Numpad4, - 0x57 => VirtualKeyCode::Numpad5, - 0x58 => VirtualKeyCode::Numpad6, - 0x59 => VirtualKeyCode::Numpad7, - 0x5a => VirtualKeyCode::F20, - 0x5b => VirtualKeyCode::Numpad8, - 0x5c => VirtualKeyCode::Numpad9, - 0x5d => VirtualKeyCode::Yen, - //0x5e => JIS Ro, - //0x5f => unkown, - 0x60 => VirtualKeyCode::F5, - 0x61 => VirtualKeyCode::F6, - 0x62 => VirtualKeyCode::F7, - 0x63 => VirtualKeyCode::F3, - 0x64 => VirtualKeyCode::F8, - 0x65 => VirtualKeyCode::F9, - //0x66 => JIS Eisuu (macOS), - 0x67 => VirtualKeyCode::F11, - //0x68 => JIS Kanna (macOS), - 0x69 => VirtualKeyCode::F13, - 0x6a => VirtualKeyCode::F16, - 0x6b => VirtualKeyCode::F14, - //0x6c => unkown, - 0x6d => VirtualKeyCode::F10, - //0x6e => unkown, - 0x6f => VirtualKeyCode::F12, - //0x70 => unkown, - 0x71 => VirtualKeyCode::F15, - 0x72 => VirtualKeyCode::Insert, - 0x73 => VirtualKeyCode::Home, - 0x74 => VirtualKeyCode::PageUp, - 0x75 => VirtualKeyCode::Delete, - 0x76 => VirtualKeyCode::F4, - 0x77 => VirtualKeyCode::End, - 0x78 => VirtualKeyCode::F2, - 0x79 => VirtualKeyCode::PageDown, - 0x7a => VirtualKeyCode::F1, - 0x7b => VirtualKeyCode::Left, - 0x7c => VirtualKeyCode::Right, - 0x7d => VirtualKeyCode::Down, - 0x7e => VirtualKeyCode::Up, - //0x7f => unkown, - 0xa => VirtualKeyCode::Caret, - _ => return None, - }) +pub fn get_modifierless_char(scancode: u16) -> Key<'static> { + let mut string = [0; 16]; + let input_source; + let layout; + unsafe { + input_source = ffi::TISCopyCurrentKeyboardLayoutInputSource(); + if input_source.is_null() { + log::error!("`TISCopyCurrentKeyboardLayoutInputSource` returned null ptr"); + return Key::Unidentified(NativeKeyCode::MacOS(scancode)); + } + let layout_data = + ffi::TISGetInputSourceProperty(input_source, ffi::kTISPropertyUnicodeKeyLayoutData); + if layout_data.is_null() { + CFRelease(input_source as *mut c_void); + log::error!("`TISGetInputSourceProperty` returned null ptr"); + return Key::Unidentified(NativeKeyCode::MacOS(scancode)); + } + layout = CFDataGetBytePtr(layout_data) as *const ffi::UCKeyboardLayout; + } + let keyboard_type = unsafe { ffi::LMGetKbdType() }; + + let mut result_len = 0; + let mut dead_keys = 0; + let modifiers = 0; + let translate_result = unsafe { + ffi::UCKeyTranslate( + layout, + scancode, + ffi::kUCKeyActionDisplay, + modifiers, + keyboard_type as u32, + ffi::kUCKeyTranslateNoDeadKeysMask, + &mut dead_keys, + string.len() as ffi::UniCharCount, + &mut result_len, + string.as_mut_ptr(), + ) + }; + unsafe { + CFRelease(input_source as *mut c_void); + } + if translate_result != 0 { + log::error!( + "`UCKeyTranslate` returned with the non-zero value: {}", + translate_result + ); + return Key::Unidentified(NativeKeyCode::MacOS(scancode)); + } + if result_len == 0 { + log::error!("`UCKeyTranslate` was succesful but gave a string of 0 length."); + return Key::Unidentified(NativeKeyCode::MacOS(scancode)); + } + let chars = String::from_utf16_lossy(&string[0..result_len as usize]); + Key::Character(insert_or_get_key_str(chars)) +} + +fn get_logical_key_char(ns_event: id, modifierless_chars: &str) -> Key<'static> { + let characters: id = unsafe { msg_send![ns_event, charactersIgnoringModifiers] }; + let string = unsafe { ns_string_to_rust(characters) }; + if string.is_empty() { + // Probably a dead key + let first_char = modifierless_chars.chars().next(); + return Key::Dead(first_char); + } + Key::Character(insert_or_get_key_str(string)) +} + +#[allow(clippy::unnecessary_unwrap)] +pub fn create_key_event( + ns_event: id, + is_press: bool, + is_repeat: bool, + in_ime: bool, + key_override: Option, +) -> KeyEvent { + use ElementState::{Pressed, Released}; + let state = if is_press { Pressed } else { Released }; + + let scancode = get_scancode(ns_event); + let mut physical_key = key_override.unwrap_or_else(|| KeyCode::from_scancode(scancode as u32)); + + let text_with_all_modifiers: Option<&'static str> = { + if key_override.is_some() { + None + } else { + let characters: id = unsafe { msg_send![ns_event, characters] }; + let characters = unsafe { ns_string_to_rust(characters) }; + if characters.is_empty() { + None + } else { + if matches!(physical_key, KeyCode::Unidentified(_)) { + // The key may be one of the funky function keys + physical_key = extra_function_key_to_code(scancode, &characters); + } + Some(insert_or_get_key_str(characters)) + } + } + }; + let key_from_code = code_to_key(physical_key, scancode); + let logical_key; + let key_without_modifiers; + if !matches!(key_from_code, Key::Unidentified(_)) { + logical_key = key_from_code.clone(); + key_without_modifiers = key_from_code; + } else { + //println!("Couldn't get key from code: {:?}", physical_key); + key_without_modifiers = get_modifierless_char(scancode); + + let modifiers = unsafe { NSEvent::modifierFlags(ns_event) }; + let has_alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask); + let has_ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); + if has_alt || has_ctrl || text_with_all_modifiers.is_none() || !is_press { + let modifierless_chars = match key_without_modifiers.clone() { + Key::Character(ch) => ch, + _ => "", + }; + logical_key = get_logical_key_char(ns_event, modifierless_chars); + } else { + logical_key = Key::Character(text_with_all_modifiers.unwrap()); + } + } + let text = if in_ime || !is_press { + None + } else { + logical_key.to_text() + }; + KeyEvent { + location: code_to_location(physical_key), + logical_key, + physical_key, + repeat: is_repeat, + state, + text, + platform_specific: KeyEventExtra { + text_with_all_modifiers, + key_without_modifiers, + }, + } +} + +pub fn code_to_key(code: KeyCode, scancode: u16) -> Key<'static> { + match code { + KeyCode::Enter => Key::Enter, + KeyCode::Tab => Key::Tab, + KeyCode::Space => Key::Space, + KeyCode::Backspace => Key::Backspace, + KeyCode::Escape => Key::Escape, + KeyCode::SuperRight => Key::Super, + KeyCode::SuperLeft => Key::Super, + KeyCode::ShiftLeft => Key::Shift, + KeyCode::AltLeft => Key::Alt, + KeyCode::ControlLeft => Key::Control, + KeyCode::ShiftRight => Key::Shift, + KeyCode::AltRight => Key::Alt, + KeyCode::ControlRight => Key::Control, + + KeyCode::NumLock => Key::NumLock, + KeyCode::AudioVolumeUp => Key::AudioVolumeUp, + KeyCode::AudioVolumeDown => Key::AudioVolumeDown, + + // Other numpad keys all generate text on macOS (if I understand correctly) + KeyCode::NumpadEnter => Key::Enter, + + KeyCode::F1 => Key::F1, + KeyCode::F2 => Key::F2, + KeyCode::F3 => Key::F3, + KeyCode::F4 => Key::F4, + KeyCode::F5 => Key::F5, + KeyCode::F6 => Key::F6, + KeyCode::F7 => Key::F7, + KeyCode::F8 => Key::F8, + KeyCode::F9 => Key::F9, + KeyCode::F10 => Key::F10, + KeyCode::F11 => Key::F11, + KeyCode::F12 => Key::F12, + KeyCode::F13 => Key::F13, + KeyCode::F14 => Key::F14, + KeyCode::F15 => Key::F15, + KeyCode::F16 => Key::F16, + KeyCode::F17 => Key::F17, + KeyCode::F18 => Key::F18, + KeyCode::F19 => Key::F19, + KeyCode::F20 => Key::F20, + + KeyCode::Insert => Key::Insert, + KeyCode::Home => Key::Home, + KeyCode::PageUp => Key::PageUp, + KeyCode::Delete => Key::Delete, + KeyCode::End => Key::End, + KeyCode::PageDown => Key::PageDown, + KeyCode::ArrowLeft => Key::ArrowLeft, + KeyCode::ArrowRight => Key::ArrowRight, + KeyCode::ArrowDown => Key::ArrowDown, + KeyCode::ArrowUp => Key::ArrowUp, + _ => Key::Unidentified(NativeKeyCode::MacOS(scancode)), + } +} + +fn code_to_location(code: KeyCode) -> KeyLocation { + match code { + KeyCode::SuperRight => KeyLocation::Right, + KeyCode::SuperLeft => KeyLocation::Left, + KeyCode::ShiftLeft => KeyLocation::Left, + KeyCode::AltLeft => KeyLocation::Left, + KeyCode::ControlLeft => KeyLocation::Left, + KeyCode::ShiftRight => KeyLocation::Right, + KeyCode::AltRight => KeyLocation::Right, + KeyCode::ControlRight => KeyLocation::Right, + + KeyCode::NumLock => KeyLocation::Numpad, + KeyCode::NumpadDecimal => KeyLocation::Numpad, + KeyCode::NumpadMultiply => KeyLocation::Numpad, + KeyCode::NumpadAdd => KeyLocation::Numpad, + KeyCode::NumpadDivide => KeyLocation::Numpad, + KeyCode::NumpadEnter => KeyLocation::Numpad, + KeyCode::NumpadSubtract => KeyLocation::Numpad, + KeyCode::NumpadEqual => KeyLocation::Numpad, + KeyCode::Numpad0 => KeyLocation::Numpad, + KeyCode::Numpad1 => KeyLocation::Numpad, + KeyCode::Numpad2 => KeyLocation::Numpad, + KeyCode::Numpad3 => KeyLocation::Numpad, + KeyCode::Numpad4 => KeyLocation::Numpad, + KeyCode::Numpad5 => KeyLocation::Numpad, + KeyCode::Numpad6 => KeyLocation::Numpad, + KeyCode::Numpad7 => KeyLocation::Numpad, + KeyCode::Numpad8 => KeyLocation::Numpad, + KeyCode::Numpad9 => KeyLocation::Numpad, + + _ => KeyLocation::Standard, + } } // While F1-F20 have scancodes we can match on, we have to check against UTF-16 // constants for the rest. // https://developer.apple.com/documentation/appkit/1535851-function-key_unicodes?preferredLanguage=occ -pub fn check_function_keys(string: &str) -> Option { +pub fn extra_function_key_to_code(scancode: u16, string: &str) -> KeyCode { if let Some(ch) = string.encode_utf16().next() { - return Some(match ch { - 0xf718 => VirtualKeyCode::F21, - 0xf719 => VirtualKeyCode::F22, - 0xf71a => VirtualKeyCode::F23, - 0xf71b => VirtualKeyCode::F24, - _ => return None, - }); + match ch { + 0xf718 => KeyCode::F21, + 0xf719 => KeyCode::F22, + 0xf71a => KeyCode::F23, + 0xf71b => KeyCode::F24, + _ => KeyCode::Unidentified(NativeKeyCode::MacOS(scancode)), + } + } else { + KeyCode::Unidentified(NativeKeyCode::MacOS(scancode)) } - - None } pub fn event_mods(event: id) -> ModifiersState { @@ -252,7 +312,7 @@ pub fn event_mods(event: id) -> ModifiersState { flags.contains(NSEventModifierFlags::NSShiftKeyMask), ); m.set( - ModifiersState::CTRL, + ModifiersState::CONTROL, flags.contains(NSEventModifierFlags::NSControlKeyMask), ); m.set( @@ -260,7 +320,7 @@ pub fn event_mods(event: id) -> ModifiersState { flags.contains(NSEventModifierFlags::NSAlternateKeyMask), ); m.set( - ModifiersState::LOGO, + ModifiersState::SUPER, flags.contains(NSEventModifierFlags::NSCommandKeyMask), ); m @@ -273,35 +333,3 @@ pub fn get_scancode(event: cocoa::base::id) -> c_ushort { // AppKit's terminology with ours. unsafe { msg_send![event, keyCode] } } - -pub unsafe fn modifier_event( - ns_event: id, - keymask: NSEventModifierFlags, - was_key_pressed: bool, -) -> Option> { - if !was_key_pressed && NSEvent::modifierFlags(ns_event).contains(keymask) - || was_key_pressed && !NSEvent::modifierFlags(ns_event).contains(keymask) - { - let state = if was_key_pressed { - ElementState::Released - } else { - ElementState::Pressed - }; - - let scancode = get_scancode(ns_event); - let virtual_keycode = scancode_to_keycode(scancode); - #[allow(deprecated)] - Some(WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state, - scancode: scancode as _, - virtual_keycode, - modifiers: event_mods(ns_event), - }, - is_synthetic: false, - }) - } else { - None - } -} diff --git a/src/platform_impl/macos/ffi.rs b/src/platform_impl/macos/ffi.rs index 929441253..ba195bfd2 100644 --- a/src/platform_impl/macos/ffi.rs +++ b/src/platform_impl/macos/ffi.rs @@ -15,13 +15,13 @@ use cocoa::{ foundation::{NSInteger, NSUInteger}, }; use core_foundation::{ - array::CFArrayRef, dictionary::CFDictionaryRef, string::CFStringRef, uuid::CFUUIDRef, + array::CFArrayRef, data::CFDataRef, dictionary::CFDictionaryRef, string::CFStringRef, + uuid::CFUUIDRef, }; use core_graphics::{ base::CGError, display::{CGDirectDisplayID, CGDisplayConfigRef}, }; - pub const NSNotFound: NSInteger = NSInteger::max_value(); #[repr(C)] @@ -225,3 +225,49 @@ extern "C" { pub fn CGDisplayModeRetain(mode: CGDisplayModeRef); pub fn CGDisplayModeRelease(mode: CGDisplayModeRef); } + +#[repr(transparent)] +pub struct TISInputSource(std::ffi::c_void); +pub type TISInputSourceRef = *mut TISInputSource; + +#[repr(transparent)] +pub struct UCKeyboardLayout(std::ffi::c_void); + +pub type OptionBits = u32; +pub type UniCharCount = std::os::raw::c_ulong; +pub type UniChar = u16; +pub type OSStatus = i32; + +#[allow(non_upper_case_globals)] +pub const kUCKeyActionDisplay: u16 = 3; +#[allow(non_upper_case_globals)] +pub const kUCKeyTranslateNoDeadKeysMask: OptionBits = 1; + +#[link(name = "Carbon", kind = "framework")] +extern "C" { + pub static kTISPropertyUnicodeKeyLayoutData: CFStringRef; + + #[allow(non_snake_case)] + pub fn TISGetInputSourceProperty( + inputSource: TISInputSourceRef, + propertyKey: CFStringRef, + ) -> CFDataRef; + + pub fn TISCopyCurrentKeyboardLayoutInputSource() -> TISInputSourceRef; + + pub fn LMGetKbdType() -> u8; + + #[allow(non_snake_case)] + pub fn UCKeyTranslate( + keyLayoutPtr: *const UCKeyboardLayout, + virtualKeyCode: u16, + keyAction: u16, + modifierKeyState: u32, + keyboardType: u32, + keyTranslateOptions: OptionBits, + deadKeyState: *mut u32, + maxStringLength: UniCharCount, + actualStringLength: *mut UniCharCount, + unicodeString: *mut UniChar, + ) -> OSStatus; +} diff --git a/src/platform_impl/macos/global_shortcut.rs b/src/platform_impl/macos/global_shortcut.rs new file mode 100644 index 000000000..08abfd818 --- /dev/null +++ b/src/platform_impl/macos/global_shortcut.rs @@ -0,0 +1,160 @@ +use std::os::raw::{c_int, c_void}; + +use crate::{ + accelerator::{Accelerator, AcceleratorId}, + event::Event, + event_loop::EventLoopWindowTarget, + platform::global_shortcut::{GlobalShortcut as RootGlobalShortcut, ShortcutManagerError}, +}; + +use super::{app_state::AppState, event::EventWrapper}; + +type KeyCallback = unsafe extern "C" fn(c_int, *mut c_void); +#[derive(Debug, Clone)] +pub struct ShortcutManager { + shortcuts: Vec, + event_handler: *mut c_void, +} + +impl ShortcutManager { + pub(crate) fn new(_window_target: &EventLoopWindowTarget) -> Self { + let saved_callback = Box::into_raw(Box::new(global_accelerator_handler)); + let event_handler = make_accelerator_callback(saved_callback); + ShortcutManager { + event_handler, + shortcuts: Vec::new(), + } + } + + pub(crate) fn unregister_all(&self) -> Result<(), ShortcutManagerError> { + for shortcut in &self.shortcuts { + shortcut.unregister(); + } + Ok(()) + } + + pub(crate) fn unregister( + &self, + shortcut: RootGlobalShortcut, + ) -> Result<(), ShortcutManagerError> { + shortcut.0.unregister(); + Ok(()) + } + + pub(crate) fn register( + &mut self, + accelerator: Accelerator, + ) -> Result { + unsafe { + let mut converted_modifiers: i32 = 0; + if accelerator.mods.shift_key() { + converted_modifiers |= 512; + } + if accelerator.mods.super_key() { + converted_modifiers |= 256; + } + if accelerator.mods.alt_key() { + converted_modifiers |= 2048; + } + if accelerator.mods.control_key() { + converted_modifiers |= 4096; + } + + // we get only 1 keycode as we don't generate it for the modifier + // it's safe to use first() + if let Some(scan_code) = accelerator.key.to_scancode() { + println!("register {:?}", accelerator); + // register hotkey + let handler_ref = register_hotkey( + accelerator.clone().id().0 as i32, + converted_modifiers as i32, + scan_code as i32, + ); + let shortcut = GlobalShortcut { + accelerator, + carbon_ref: CarbonRef::new(handler_ref), + }; + self.shortcuts.push(shortcut.clone()); + return Ok(RootGlobalShortcut(shortcut)); + } + } + + Err(ShortcutManagerError::InvalidAccelerator( + "Invalid accelerator".into(), + )) + } + + // connect_event_loop is not needed on macos +} + +unsafe extern "C" fn trampoline(result: c_int, user_data: *mut c_void) +where + F: FnMut(c_int) + 'static, +{ + let user_data = &mut *(user_data as *mut F); + user_data(result); +} + +fn get_trampoline() -> KeyCallback +where + F: FnMut(c_int) + 'static, +{ + trampoline:: +} + +#[link(name = "carbon_hotkey_binding.a", kind = "static")] +extern "C" { + fn install_event_handler(cb: KeyCallback, data: *mut c_void) -> *mut c_void; + fn uninstall_event_handler(handler_ref: *mut c_void) -> c_int; + fn register_hotkey(id: i32, modifier: i32, key: i32) -> *mut c_void; + fn unregister_hotkey(hotkey_ref: *mut c_void) -> c_int; +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct CarbonRef(pub(crate) *mut c_void); +impl CarbonRef { + pub fn new(start: *mut c_void) -> Self { + CarbonRef(start) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct GlobalShortcut { + pub(crate) carbon_ref: CarbonRef, + pub(crate) accelerator: Accelerator, +} + +impl GlobalShortcut { + pub fn id(&self) -> AcceleratorId { + self.accelerator.clone().id() + } +} + +impl GlobalShortcut { + pub(crate) fn unregister(&self) { + unsafe { unregister_hotkey(self.carbon_ref.0) }; + } +} + +fn make_accelerator_callback(handler: *mut F) -> *mut c_void +where + F: FnMut(i32) + 'static + Sync + Send, +{ + let cb = get_trampoline::(); + unsafe { install_event_handler(cb, handler as *mut c_void) } +} + +fn global_accelerator_handler(item_id: i32) { + AppState::queue_event(EventWrapper::StaticEvent(Event::GlobalShortcutEvent( + AcceleratorId(item_id as u16), + ))); +} + +impl Drop for ShortcutManager { + fn drop(&mut self) { + self.unregister_all().unwrap(); + unsafe { + uninstall_event_handler(self.event_handler); + } + } +} diff --git a/src/platform_impl/macos/keycode.rs b/src/platform_impl/macos/keycode.rs new file mode 100644 index 000000000..da00f0397 --- /dev/null +++ b/src/platform_impl/macos/keycode.rs @@ -0,0 +1,260 @@ +use crate::keyboard::{KeyCode, NativeKeyCode}; + +pub fn keycode_to_scancode(code: KeyCode) -> Option { + match code { + KeyCode::KeyA => Some(0x00), + KeyCode::KeyS => Some(0x01), + KeyCode::KeyD => Some(0x02), + KeyCode::KeyF => Some(0x03), + KeyCode::KeyH => Some(0x04), + KeyCode::KeyG => Some(0x05), + KeyCode::KeyZ => Some(0x06), + KeyCode::KeyX => Some(0x07), + KeyCode::KeyC => Some(0x08), + KeyCode::KeyV => Some(0x09), + KeyCode::KeyB => Some(0x0b), + KeyCode::KeyQ => Some(0x0c), + KeyCode::KeyW => Some(0x0d), + KeyCode::KeyE => Some(0x0e), + KeyCode::KeyR => Some(0x0f), + KeyCode::KeyY => Some(0x10), + KeyCode::KeyT => Some(0x11), + KeyCode::Digit1 => Some(0x12), + KeyCode::Digit2 => Some(0x13), + KeyCode::Digit3 => Some(0x14), + KeyCode::Digit4 => Some(0x15), + KeyCode::Digit6 => Some(0x16), + KeyCode::Digit5 => Some(0x17), + KeyCode::Equal => Some(0x18), + KeyCode::Digit9 => Some(0x19), + KeyCode::Digit7 => Some(0x1a), + KeyCode::Minus => Some(0x1b), + KeyCode::Digit8 => Some(0x1c), + KeyCode::Digit0 => Some(0x1d), + KeyCode::BracketRight => Some(0x1e), + KeyCode::KeyO => Some(0x1f), + KeyCode::KeyU => Some(0x20), + KeyCode::BracketLeft => Some(0x21), + KeyCode::KeyI => Some(0x22), + KeyCode::KeyP => Some(0x23), + KeyCode::Enter => Some(0x24), + KeyCode::KeyL => Some(0x25), + KeyCode::KeyJ => Some(0x26), + KeyCode::Quote => Some(0x27), + KeyCode::KeyK => Some(0x28), + KeyCode::Semicolon => Some(0x29), + KeyCode::Backslash => Some(0x2a), + KeyCode::Comma => Some(0x2b), + KeyCode::Slash => Some(0x2c), + KeyCode::KeyN => Some(0x2d), + KeyCode::KeyM => Some(0x2e), + KeyCode::Period => Some(0x2f), + KeyCode::Tab => Some(0x30), + KeyCode::Space => Some(0x31), + KeyCode::Backquote => Some(0x32), + KeyCode::Backspace => Some(0x33), + KeyCode::Escape => Some(0x35), + KeyCode::SuperRight => Some(0x36), + KeyCode::SuperLeft => Some(0x37), + KeyCode::ShiftLeft => Some(0x38), + KeyCode::AltLeft => Some(0x3a), + KeyCode::ControlLeft => Some(0x3b), + KeyCode::ShiftRight => Some(0x3c), + KeyCode::AltRight => Some(0x3d), + KeyCode::ControlRight => Some(0x3e), + KeyCode::F17 => Some(0x40), + KeyCode::NumpadDecimal => Some(0x41), + KeyCode::NumpadMultiply => Some(0x43), + KeyCode::NumpadAdd => Some(0x45), + KeyCode::NumLock => Some(0x47), + KeyCode::AudioVolumeUp => Some(0x49), + KeyCode::AudioVolumeDown => Some(0x4a), + KeyCode::NumpadDivide => Some(0x4b), + KeyCode::NumpadEnter => Some(0x4c), + KeyCode::NumpadSubtract => Some(0x4e), + KeyCode::F18 => Some(0x4f), + KeyCode::F19 => Some(0x50), + KeyCode::NumpadEqual => Some(0x51), + KeyCode::Numpad0 => Some(0x52), + KeyCode::Numpad1 => Some(0x53), + KeyCode::Numpad2 => Some(0x54), + KeyCode::Numpad3 => Some(0x55), + KeyCode::Numpad4 => Some(0x56), + KeyCode::Numpad5 => Some(0x57), + KeyCode::Numpad6 => Some(0x58), + KeyCode::Numpad7 => Some(0x59), + KeyCode::F20 => Some(0x5a), + KeyCode::Numpad8 => Some(0x5b), + KeyCode::Numpad9 => Some(0x5c), + KeyCode::IntlYen => Some(0x5d), + KeyCode::F5 => Some(0x60), + KeyCode::F6 => Some(0x61), + KeyCode::F7 => Some(0x62), + KeyCode::F3 => Some(0x63), + KeyCode::F8 => Some(0x64), + KeyCode::F9 => Some(0x65), + KeyCode::F11 => Some(0x67), + KeyCode::F13 => Some(0x69), + KeyCode::F16 => Some(0x6a), + KeyCode::F14 => Some(0x6b), + KeyCode::F10 => Some(0x6d), + KeyCode::F12 => Some(0x6f), + KeyCode::F15 => Some(0x71), + KeyCode::Insert => Some(0x72), + KeyCode::Home => Some(0x73), + KeyCode::PageUp => Some(0x74), + KeyCode::Delete => Some(0x75), + KeyCode::F4 => Some(0x76), + KeyCode::End => Some(0x77), + KeyCode::F2 => Some(0x78), + KeyCode::PageDown => Some(0x79), + KeyCode::F1 => Some(0x7a), + KeyCode::ArrowLeft => Some(0x7b), + KeyCode::ArrowRight => Some(0x7c), + KeyCode::ArrowDown => Some(0x7d), + KeyCode::ArrowUp => Some(0x7e), + _ => None, + } +} + +pub fn keycode_from_scancode(scancode: u32) -> KeyCode { + match scancode { + 0x00 => KeyCode::KeyA, + 0x01 => KeyCode::KeyS, + 0x02 => KeyCode::KeyD, + 0x03 => KeyCode::KeyF, + 0x04 => KeyCode::KeyH, + 0x05 => KeyCode::KeyG, + 0x06 => KeyCode::KeyZ, + 0x07 => KeyCode::KeyX, + 0x08 => KeyCode::KeyC, + 0x09 => KeyCode::KeyV, + //0x0a => World 1, + 0x0b => KeyCode::KeyB, + 0x0c => KeyCode::KeyQ, + 0x0d => KeyCode::KeyW, + 0x0e => KeyCode::KeyE, + 0x0f => KeyCode::KeyR, + 0x10 => KeyCode::KeyY, + 0x11 => KeyCode::KeyT, + 0x12 => KeyCode::Digit1, + 0x13 => KeyCode::Digit2, + 0x14 => KeyCode::Digit3, + 0x15 => KeyCode::Digit4, + 0x16 => KeyCode::Digit6, + 0x17 => KeyCode::Digit5, + 0x18 => KeyCode::Equal, + 0x19 => KeyCode::Digit9, + 0x1a => KeyCode::Digit7, + 0x1b => KeyCode::Minus, + 0x1c => KeyCode::Digit8, + 0x1d => KeyCode::Digit0, + 0x1e => KeyCode::BracketRight, + 0x1f => KeyCode::KeyO, + 0x20 => KeyCode::KeyU, + 0x21 => KeyCode::BracketLeft, + 0x22 => KeyCode::KeyI, + 0x23 => KeyCode::KeyP, + 0x24 => KeyCode::Enter, + 0x25 => KeyCode::KeyL, + 0x26 => KeyCode::KeyJ, + 0x27 => KeyCode::Quote, + 0x28 => KeyCode::KeyK, + 0x29 => KeyCode::Semicolon, + 0x2a => KeyCode::Backslash, + 0x2b => KeyCode::Comma, + 0x2c => KeyCode::Slash, + 0x2d => KeyCode::KeyN, + 0x2e => KeyCode::KeyM, + 0x2f => KeyCode::Period, + 0x30 => KeyCode::Tab, + 0x31 => KeyCode::Space, + 0x32 => KeyCode::Backquote, + 0x33 => KeyCode::Backspace, + //0x34 => unknown, + 0x35 => KeyCode::Escape, + 0x36 => KeyCode::SuperRight, + 0x37 => KeyCode::SuperLeft, + 0x38 => KeyCode::ShiftLeft, + 0x39 => KeyCode::CapsLock, + 0x3a => KeyCode::AltLeft, + 0x3b => KeyCode::ControlLeft, + 0x3c => KeyCode::ShiftRight, + 0x3d => KeyCode::AltRight, + 0x3e => KeyCode::ControlRight, + 0x3f => KeyCode::Fn, + 0x40 => KeyCode::F17, + 0x41 => KeyCode::NumpadDecimal, + //0x42 -> unknown, + 0x43 => KeyCode::NumpadMultiply, + //0x44 => unknown, + 0x45 => KeyCode::NumpadAdd, + //0x46 => unknown, + 0x47 => KeyCode::NumLock, + //0x48 => KeyCode::NumpadClear, + + // TODO: (Artur) for me, kVK_VolumeUp is 0x48 + // macOS 10.11 + // /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h + //0x49 => KeyCode::AudioVolumeUp, + 0x49 => KeyCode::AudioVolumeDown, + 0x4b => KeyCode::NumpadDivide, + 0x4c => KeyCode::NumpadEnter, + //0x4d => unknown, + 0x4e => KeyCode::NumpadSubtract, + 0x4f => KeyCode::F18, + 0x50 => KeyCode::F19, + 0x51 => KeyCode::NumpadEqual, + 0x52 => KeyCode::Numpad0, + 0x53 => KeyCode::Numpad1, + 0x54 => KeyCode::Numpad2, + 0x55 => KeyCode::Numpad3, + 0x56 => KeyCode::Numpad4, + 0x57 => KeyCode::Numpad5, + 0x58 => KeyCode::Numpad6, + 0x59 => KeyCode::Numpad7, + 0x5a => KeyCode::F20, + 0x5b => KeyCode::Numpad8, + 0x5c => KeyCode::Numpad9, + 0x5d => KeyCode::IntlYen, + //0x5e => JIS Ro, + //0x5f => unknown, + 0x60 => KeyCode::F5, + 0x61 => KeyCode::F6, + 0x62 => KeyCode::F7, + 0x63 => KeyCode::F3, + 0x64 => KeyCode::F8, + 0x65 => KeyCode::F9, + //0x66 => JIS Eisuu (macOS), + 0x67 => KeyCode::F11, + //0x68 => JIS Kanna (macOS), + 0x69 => KeyCode::F13, + 0x6a => KeyCode::F16, + 0x6b => KeyCode::F14, + //0x6c => unknown, + 0x6d => KeyCode::F10, + //0x6e => unknown, + 0x6f => KeyCode::F12, + //0x70 => unknown, + 0x71 => KeyCode::F15, + 0x72 => KeyCode::Insert, + 0x73 => KeyCode::Home, + 0x74 => KeyCode::PageUp, + 0x75 => KeyCode::Delete, + 0x76 => KeyCode::F4, + 0x77 => KeyCode::End, + 0x78 => KeyCode::F2, + 0x79 => KeyCode::PageDown, + 0x7a => KeyCode::F1, + 0x7b => KeyCode::ArrowLeft, + 0x7c => KeyCode::ArrowRight, + 0x7d => KeyCode::ArrowDown, + 0x7e => KeyCode::ArrowUp, + //0x7f => unknown, + + // 0xA is the caret (^) an macOS's German QERTZ layout. This key is at the same location as + // backquote (`) on Windows' US layout. + 0xa => KeyCode::Backquote, + _ => KeyCode::Unidentified(NativeKeyCode::MacOS(scancode as u16)), + } +} diff --git a/src/platform_impl/macos/menu.rs b/src/platform_impl/macos/menu.rs index bb2bd5c2e..c3209e40e 100644 --- a/src/platform_impl/macos/menu.rs +++ b/src/platform_impl/macos/menu.rs @@ -13,7 +13,9 @@ use objc::{ use std::sync::Once; use crate::{ + accelerator::{Accelerator, RawMods}, event::Event, + keyboard::{KeyCode, ModifiersState}, menu::{CustomMenuItem, MenuId, MenuItem, MenuType}, platform::macos::NativeImage, }; @@ -22,11 +24,6 @@ use super::{app_state::AppState, event::EventWrapper}; static BLOCK_PTR: &str = "taoMenuItemBlockPtr"; -pub(crate) struct KeyEquivalent<'a> { - pub(crate) key: &'a str, - pub(crate) masks: Option, -} - #[derive(Debug, Clone)] pub struct Menu { pub menu: id, @@ -108,43 +105,12 @@ impl Menu { &mut self, menu_id: MenuId, title: &str, - accelerators: Option<&str>, + accelerators: Option, enabled: bool, selected: bool, menu_type: MenuType, ) -> CustomMenuItem { - let mut key_equivalent = None; - let mut accelerator_string: String; - if let Some(accelerator) = accelerators { - accelerator_string = accelerator.to_string(); - let mut ns_modifier_flags: NSEventModifierFlags = NSEventModifierFlags::empty(); - if accelerator_string.contains("") { - accelerator_string = accelerator_string.replace("", ""); - ns_modifier_flags.insert(NSEventModifierFlags::NSCommandKeyMask); - } - - if accelerator_string.contains("") { - accelerator_string = accelerator_string.replace("", ""); - ns_modifier_flags.insert(NSEventModifierFlags::NSShiftKeyMask); - } - - if accelerator_string.contains("") { - accelerator_string = accelerator_string.replace("", ""); - ns_modifier_flags.insert(NSEventModifierFlags::NSControlKeyMask); - } - - let mut masks = None; - if !ns_modifier_flags.is_empty() { - masks = Some(ns_modifier_flags); - } - - key_equivalent = Some(KeyEquivalent { - key: accelerator_string.as_str(), - masks, - }); - } - - let menu_item = make_custom_menu_item(menu_id, title, None, key_equivalent, menu_type); + let menu_item = make_custom_menu_item(menu_id, title, None, accelerators, menu_type); unsafe { if selected { @@ -201,10 +167,7 @@ impl Menu { make_menu_item( "Close Window", Some(selector("performClose:")), - Some(KeyEquivalent { - key: "w", - masks: None, - }), + Some(Accelerator::new(RawMods::Meta, KeyCode::KeyX)), menu_type, ), )), @@ -213,10 +176,7 @@ impl Menu { make_menu_item( "Quit", Some(selector("terminate:")), - Some(KeyEquivalent { - key: "q", - masks: None, - }), + Some(Accelerator::new(RawMods::Meta, KeyCode::KeyQ)), menu_type, ), )), @@ -225,10 +185,7 @@ impl Menu { make_menu_item( "Hide", Some(selector("hide:")), - Some(KeyEquivalent { - key: "h", - masks: None, - }), + Some(Accelerator::new(RawMods::Meta, KeyCode::KeyH)), menu_type, ), )), @@ -237,12 +194,7 @@ impl Menu { make_menu_item( "Hide Others", Some(selector("hideOtherApplications:")), - Some(KeyEquivalent { - key: "h", - masks: Some( - NSEventModifierFlags::NSAlternateKeyMask | NSEventModifierFlags::NSCommandKeyMask, - ), - }), + Some(Accelerator::new(RawMods::AltMeta, KeyCode::KeyW)), menu_type, ), )), @@ -260,12 +212,7 @@ impl Menu { make_menu_item( "Enter Full Screen", Some(selector("toggleFullScreen:")), - Some(KeyEquivalent { - key: "f", - masks: Some( - NSEventModifierFlags::NSCommandKeyMask | NSEventModifierFlags::NSControlKeyMask, - ), - }), + Some(Accelerator::new(RawMods::CtrlMeta, KeyCode::KeyF)), menu_type, ), )), @@ -274,10 +221,7 @@ impl Menu { make_menu_item( "Minimize", Some(selector("performMiniaturize:")), - Some(KeyEquivalent { - key: "m", - masks: None, - }), + Some(Accelerator::new(RawMods::Meta, KeyCode::KeyM)), menu_type, ), )), @@ -290,10 +234,7 @@ impl Menu { make_menu_item( "Copy", Some(selector("copy:")), - Some(KeyEquivalent { - key: "c", - masks: None, - }), + Some(Accelerator::new(RawMods::Meta, KeyCode::KeyC)), menu_type, ), )), @@ -302,10 +243,7 @@ impl Menu { make_menu_item( "Cut", Some(selector("cut:")), - Some(KeyEquivalent { - key: "x", - masks: None, - }), + Some(Accelerator::new(RawMods::Meta, KeyCode::KeyX)), menu_type, ), )), @@ -314,10 +252,7 @@ impl Menu { make_menu_item( "Paste", Some(selector("paste:")), - Some(KeyEquivalent { - key: "v", - masks: None, - }), + Some(Accelerator::new(RawMods::Meta, KeyCode::KeyV)), menu_type, ), )), @@ -326,10 +261,7 @@ impl Menu { make_menu_item( "Undo", Some(selector("undo:")), - Some(KeyEquivalent { - key: "z", - masks: None, - }), + Some(Accelerator::new(RawMods::Meta, KeyCode::KeyZ)), menu_type, ), )), @@ -338,10 +270,7 @@ impl Menu { make_menu_item( "Redo", Some(selector("redo:")), - Some(KeyEquivalent { - key: "Z", - masks: None, - }), + Some(Accelerator::new(RawMods::Shift, KeyCode::KeyZ)), menu_type, ), )), @@ -350,10 +279,7 @@ impl Menu { make_menu_item( "Select All", Some(selector("selectAll:")), - Some(KeyEquivalent { - key: "a", - masks: None, - }), + Some(Accelerator::new(RawMods::Meta, KeyCode::KeyA)), menu_type, ), )), @@ -380,7 +306,7 @@ impl Menu { } #[derive(Debug)] -struct Action(Box); +struct Action(Box); pub fn initialize(menu_builder: Menu) { unsafe { @@ -397,7 +323,7 @@ pub(crate) fn make_custom_menu_item( id: MenuId, title: &str, selector: Option, - key_equivalent: Option>, + accelerators: Option, menu_type: MenuType, ) -> *mut Object { let alloc = make_menu_alloc(); @@ -408,20 +334,20 @@ pub(crate) fn make_custom_menu_item( (&mut *alloc).set_ivar(BLOCK_PTR, ptr as usize); let _: () = msg_send![&*alloc, setTarget:&*alloc]; let title = NSString::alloc(nil).init_str(title); - make_menu_item_from_alloc(alloc, title, selector, key_equivalent, menu_type) + make_menu_item_from_alloc(alloc, title, selector, accelerators, menu_type) } } pub(crate) fn make_menu_item( title: &str, selector: Option, - key_equivalent: Option>, + accelerator: Option, menu_type: MenuType, ) -> *mut Object { let alloc = make_menu_alloc(); unsafe { let title = NSString::alloc(nil).init_str(title); - make_menu_item_from_alloc(alloc, title, selector, key_equivalent, menu_type) + make_menu_item_from_alloc(alloc, title, selector, accelerator, menu_type) } } @@ -429,14 +355,16 @@ fn make_menu_item_from_alloc( alloc: *mut Object, title: *mut Object, selector: Option, - key_equivalent: Option>, + accelerator: Option, menu_type: MenuType, ) -> *mut Object { unsafe { - let (key, masks) = match key_equivalent { - Some(ke) => (NSString::alloc(nil).init_str(ke.key), ke.masks), - None => (NSString::alloc(nil).init_str(""), None), - }; + // build our Accelerator string + let key_equivalent = accelerator + .clone() + .map(Accelerator::key_equivalent) + .unwrap_or_else(|| "".into()); + let key_equivalent = NSString::alloc(nil).init_str(&key_equivalent); // if no selector defined, that mean it's a custom // menu so fire our handler let selector = match selector { @@ -446,10 +374,10 @@ fn make_menu_item_from_alloc( MenuType::ContextMenu => sel!(fireStatusbarAction:), }, }; - // allocate our item to our class - let item: id = msg_send![alloc, initWithTitle: title action: selector keyEquivalent: key]; - if let Some(masks) = masks { + let item: id = + msg_send![alloc, initWithTitle: title action: selector keyEquivalent: key_equivalent]; + if let Some(masks) = accelerator.map(Accelerator::key_modifier_mask) { item.setKeyEquivalentModifierMask_(masks) } @@ -518,3 +446,100 @@ extern "C" fn dealloc_custom_menuitem(this: &Object, _: Sel) { let _: () = msg_send![super(this, class!(NSMenuItem)), dealloc]; } } + +impl Accelerator { + /// Return the string value of this hotkey, for use with Cocoa `NSResponder` + /// objects. + /// + /// Returns the empty string if no key equivalent is known. + fn key_equivalent(self) -> String { + match self.key { + KeyCode::KeyA => "a".into(), + KeyCode::KeyB => "b".into(), + KeyCode::KeyC => "c".into(), + KeyCode::KeyD => "d".into(), + KeyCode::KeyE => "e".into(), + KeyCode::KeyF => "f".into(), + KeyCode::KeyG => "g".into(), + KeyCode::KeyH => "h".into(), + KeyCode::KeyI => "i".into(), + KeyCode::KeyJ => "j".into(), + KeyCode::KeyK => "k".into(), + KeyCode::KeyL => "l".into(), + KeyCode::KeyM => "m".into(), + KeyCode::KeyN => "n".into(), + KeyCode::KeyO => "o".into(), + KeyCode::KeyP => "p".into(), + KeyCode::KeyQ => "q".into(), + KeyCode::KeyR => "r".into(), + KeyCode::KeyS => "s".into(), + KeyCode::KeyT => "t".into(), + KeyCode::KeyU => "u".into(), + KeyCode::KeyV => "v".into(), + KeyCode::KeyW => "w".into(), + KeyCode::KeyX => "x".into(), + KeyCode::KeyY => "y".into(), + KeyCode::KeyZ => "z".into(), + KeyCode::Digit0 => "0".into(), + KeyCode::Digit1 => "1".into(), + KeyCode::Digit2 => "2".into(), + KeyCode::Digit3 => "3".into(), + KeyCode::Digit4 => "4".into(), + KeyCode::Digit5 => "5".into(), + KeyCode::Digit6 => "6".into(), + KeyCode::Digit7 => "7".into(), + KeyCode::Digit8 => "8".into(), + KeyCode::Digit9 => "9".into(), + // from NSText.h + KeyCode::Enter => "\u{0003}".into(), + KeyCode::Backspace => "\u{0008}".into(), + KeyCode::Delete => "\u{007f}".into(), + // from NSEvent.h + KeyCode::Insert => "\u{F727}".into(), + KeyCode::Home => "\u{F729}".into(), + KeyCode::End => "\u{F72B}".into(), + KeyCode::PageUp => "\u{F72C}".into(), + KeyCode::PageDown => "\u{F72D}".into(), + KeyCode::PrintScreen => "\u{F72E}".into(), + KeyCode::ScrollLock => "\u{F72F}".into(), + KeyCode::ArrowUp => "\u{F700}".into(), + KeyCode::ArrowDown => "\u{F701}".into(), + KeyCode::ArrowLeft => "\u{F702}".into(), + KeyCode::ArrowRight => "\u{F703}".into(), + KeyCode::F1 => "\u{F704}".into(), + KeyCode::F2 => "\u{F705}".into(), + KeyCode::F3 => "\u{F706}".into(), + KeyCode::F4 => "\u{F707}".into(), + KeyCode::F5 => "\u{F708}".into(), + KeyCode::F6 => "\u{F709}".into(), + KeyCode::F7 => "\u{F70A}".into(), + KeyCode::F8 => "\u{F70B}".into(), + KeyCode::F9 => "\u{F70C}".into(), + KeyCode::F10 => "\u{F70D}".into(), + KeyCode::F11 => "\u{F70E}".into(), + KeyCode::F12 => "\u{F70F}".into(), + _ => { + eprintln!("no key equivalent for {:?}", self); + "".into() + } + } + } + + fn key_modifier_mask(self) -> NSEventModifierFlags { + let mods: ModifiersState = self.mods; + let mut flags = NSEventModifierFlags::empty(); + if mods.shift_key() { + flags.insert(NSEventModifierFlags::NSShiftKeyMask); + } + if mods.super_key() { + flags.insert(NSEventModifierFlags::NSCommandKeyMask); + } + if mods.alt_key() { + flags.insert(NSEventModifierFlags::NSAlternateKeyMask); + } + if mods.control_key() { + flags.insert(NSEventModifierFlags::NSControlKeyMask); + } + flags + } +} diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 05e6a37ee..1b3c211e5 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -10,6 +10,8 @@ mod clipboard; mod event; mod event_loop; mod ffi; +mod global_shortcut; +mod keycode; mod menu; mod monitor; mod observer; @@ -28,7 +30,10 @@ pub use self::system_tray::{SystemTray, SystemTrayBuilder}; pub use self::{ app_delegate::{get_aux_state_mut, AuxDelegateState}, clipboard::Clipboard, + event::KeyEventExtra, event_loop::{EventLoop, EventLoopWindowTarget, Proxy as EventLoopProxy}, + global_shortcut::{GlobalShortcut, ShortcutManager}, + keycode::{keycode_from_scancode, keycode_to_scancode}, menu::{Menu, MenuItemAttributes}, monitor::{MonitorHandle, VideoMode}, window::{Id as WindowId, PlatformSpecificWindowBuilderAttributes, UnownedWindow}, diff --git a/src/platform_impl/macos/util/mod.rs b/src/platform_impl/macos/util/mod.rs index 36d9e982a..ee904f6e6 100644 --- a/src/platform_impl/macos/util/mod.rs +++ b/src/platform_impl/macos/util/mod.rs @@ -6,7 +6,10 @@ mod cursor; pub use self::{cursor::*, r#async::*}; -use std::ops::{BitAnd, Deref}; +use std::{ + ops::{BitAnd, Deref}, + slice, str, +}; use cocoa::{ appkit::{NSApp, NSWindowStyleMask}, @@ -123,6 +126,13 @@ pub unsafe fn ns_string_id_ref(s: &str) -> IdRef { IdRef::new(NSString::alloc(nil).init_str(s)) } +/// Copies the contents of the ns string into a `String` which gets returned. +pub unsafe fn ns_string_to_rust(ns_string: id) -> String { + let slice = slice::from_raw_parts(ns_string.UTF8String() as *mut u8, ns_string.len()); + let string = str::from_utf8_unchecked(slice); + string.to_owned() +} + #[allow(dead_code)] // In case we want to use this function in the future pub unsafe fn app_name() -> Option { let bundle: id = msg_send![class!(NSBundle), mainBundle]; diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index d21064bf4..8577a53b6 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -3,7 +3,7 @@ use std::{ boxed::Box, - collections::VecDeque, + collections::{HashSet, VecDeque}, os::raw::*, slice, str, sync::{Arc, Mutex, Weak}, @@ -22,15 +22,12 @@ use objc::{ use crate::{ dpi::LogicalPosition, event::{ - DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, - TouchPhase, VirtualKeyCode, WindowEvent, + DeviceEvent, ElementState, Event, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent, }, + keyboard::{KeyCode, ModifiersState}, platform_impl::platform::{ app_state::AppState, - event::{ - char_to_keycode, check_function_keys, event_mods, get_scancode, modifier_event, - scancode_to_keycode, EventWrapper, - }, + event::{code_to_key, create_key_event, event_mods, get_scancode, EventWrapper}, ffi::*, util::{self, IdRef}, window::get_window_id, @@ -57,9 +54,20 @@ pub(super) struct ViewState { ns_window: id, pub cursor_state: Arc>, ime_spot: Option<(f64, f64)>, - raw_characters: Option, + + /// This is true when we are currently modifying a marked text + /// using ime. When the text gets commited, this is set to false. + in_ime_preedit: bool, + + /// This is used to detect if a key-press causes an ime event. + /// If a key-press does not cause an ime event, that means + /// that the key-press cancelled the ime session. (Except arrow keys) + key_triggered_ime: bool, + // Not Needed Anymore + //raw_characters: Option, is_key_down: bool, pub(super) modifiers: ModifiersState, + phys_modifiers: HashSet, tracking_rect: Option, } @@ -76,9 +84,11 @@ pub fn new_view(ns_window: id) -> (IdRef, Weak>) { ns_window, cursor_state, ime_spot: None, - raw_characters: None, + in_ime_preedit: false, + key_triggered_ime: false, is_key_down: false, modifiers: Default::default(), + phys_modifiers: Default::default(), tracking_rect: None, }; unsafe { @@ -103,6 +113,23 @@ pub unsafe fn set_ime_position(ns_view: id, input_context: id, x: f64, y: f64) { let _: () = msg_send![input_context, invalidateCharacterCoordinates]; } +fn is_arrow_key(keycode: KeyCode) -> bool { + matches!( + keycode, + KeyCode::ArrowUp | KeyCode::ArrowDown | KeyCode::ArrowLeft | KeyCode::ArrowRight + ) +} + +/// `view` must be the reference to the `TaoView` class +/// +/// Returns the mutable reference to the `markedText` field. +unsafe fn clear_marked_text(view: &mut Object) -> &mut id { + let marked_text_ref: &mut id = view.get_mut_ivar("markedText"); + let () = msg_send![(*marked_text_ref), release]; + *marked_text_ref = NSMutableAttributedString::alloc(nil); + marked_text_ref +} + struct ViewClass(*const Class); unsafe impl Send for ViewClass {} unsafe impl Sync for ViewClass {} @@ -152,7 +179,10 @@ lazy_static! { sel!(setMarkedText:selectedRange:replacementRange:), set_marked_text as extern "C" fn(&mut Object, Sel, id, NSRange, NSRange), ); - decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel)); + decl.add_method( + sel!(unmarkText), + unmark_text as extern "C" fn(&mut Object, Sel), + ); decl.add_method( sel!(validAttributesForMarkedText), valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id, @@ -178,7 +208,10 @@ lazy_static! { sel!(doCommandBySelector:), do_command_by_selector as extern "C" fn(&Object, Sel, Sel), ); - decl.add_method(sel!(keyDown:), key_down as extern "C" fn(&Object, Sel, id)); + decl.add_method( + sel!(keyDown:), + key_down as extern "C" fn(&mut Object, Sel, id), + ); decl.add_method(sel!(keyUp:), key_up as extern "C" fn(&Object, Sel, id)); decl.add_method( sel!(flagsChanged:), @@ -416,6 +449,9 @@ extern "C" fn selected_range(_this: &Object, _sel: Sel) -> NSRange { util::EMPTY_RANGE } +/// An IME pre-edit operation happened, changing the text that's +/// currently being pre-edited. This more or less corresponds to +/// the `compositionupdate` event on the web. extern "C" fn set_marked_text( this: &mut Object, _sel: Sel, @@ -425,26 +461,26 @@ extern "C" fn set_marked_text( ) { trace!("Triggered `setMarkedText`"); unsafe { - let marked_text_ref: &mut id = this.get_mut_ivar("markedText"); - let _: () = msg_send![(*marked_text_ref), release]; - let marked_text = NSMutableAttributedString::alloc(nil); + let marked_text_ref = clear_marked_text(this); let has_attr = msg_send![string, isKindOfClass: class!(NSAttributedString)]; if has_attr { - marked_text.initWithAttributedString(string); + marked_text_ref.initWithAttributedString(string); } else { - marked_text.initWithString(string); + marked_text_ref.initWithString(string); }; - *marked_text_ref = marked_text; + + let state_ptr: *mut c_void = *this.get_ivar("taoState"); + let state = &mut *(state_ptr as *mut ViewState); + state.in_ime_preedit = true; + state.key_triggered_ime = true; } trace!("Completed `setMarkedText`"); } -extern "C" fn unmark_text(this: &Object, _sel: Sel) { +extern "C" fn unmark_text(this: &mut Object, _sel: Sel) { trace!("Triggered `unmarkText`"); unsafe { - let marked_text: id = *this.get_ivar("markedText"); - let mutable_string = marked_text.mutableString(); - let _: () = msg_send![mutable_string, setString:""]; + clear_marked_text(this); let input_context: id = msg_send![this, inputContext]; let _: () = msg_send![input_context, discardMarkedText]; } @@ -512,81 +548,89 @@ extern "C" fn insert_text(this: &Object, _sel: Sel, string: id, _replacement_ran }; let slice = slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len()); - let string = str::from_utf8_unchecked(slice); + let string: String = str::from_utf8_unchecked(slice) + .chars() + .filter(|c| !is_corporate_character(*c)) + .collect(); state.is_key_down = true; // We don't need this now, but it's here if that changes. //let event: id = msg_send![NSApp(), currentEvent]; - let mut events = VecDeque::with_capacity(characters.len()); - for character in string.chars().filter(|c| !is_corporate_character(*c)) { - events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { + // We only send the IME text input here. The text coming from the + // keyboard is handled by `key_down` + if state.in_ime_preedit { + AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::ReceivedCharacter(character), + event: WindowEvent::ReceivedImeText(string), })); + state.in_ime_preedit = false; + state.key_triggered_ime = true; } - - AppState::queue_events(events); } trace!("Completed `insertText`"); } -extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, command: Sel) { +extern "C" fn do_command_by_selector(_this: &Object, _sel: Sel, _command: Sel) { trace!("Triggered `doCommandBySelector`"); + // TODO: (Artur) all these inputs seem to trigger a key event with the correct text + // content so this is not needed anymore, it seems. + // Basically, we're sent this message whenever a keyboard event that doesn't generate a "human readable" character // happens, i.e. newlines, tabs, and Ctrl+C. - unsafe { - let state_ptr: *mut c_void = *this.get_ivar("taoState"); - let state = &mut *(state_ptr as *mut ViewState); - - let mut events = VecDeque::with_capacity(1); - if command == sel!(insertNewline:) { - // The `else` condition would emit the same character, but I'm keeping this here both... - // 1) as a reminder for how `doCommandBySelector` works - // 2) to make our use of carriage return explicit - events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::ReceivedCharacter('\r'), - })); - } else { - let raw_characters = state.raw_characters.take(); - if let Some(raw_characters) = raw_characters { - for character in raw_characters - .chars() - .filter(|c| !is_corporate_character(*c)) - { - events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { - window_id: WindowId(get_window_id(state.ns_window)), - event: WindowEvent::ReceivedCharacter(character), - })); - } - } - }; - AppState::queue_events(events); - } + // unsafe { + // let state_ptr: *mut c_void = *this.get_ivar("taoState"); + // let state = &mut *(state_ptr as *mut ViewState); + + // let mut events = VecDeque::with_capacity(1); + // if command == sel!(insertNewline:) { + // // The `else` condition would emit the same character, but I'm keeping this here both... + // // 1) as a reminder for how `doCommandBySelector` works + // // 2) to make our use of carriage return explicit + // events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { + // window_id: WindowId(get_window_id(state.ns_window)), + // event: WindowEvent::ReceivedCharacter('\r'), + // })); + // } else { + // let raw_characters = state.raw_characters.take(); + // if let Some(raw_characters) = raw_characters { + // for character in raw_characters + // .chars() + // .filter(|c| !is_corporate_character(*c)) + // { + // events.push_back(EventWrapper::StaticEvent(Event::WindowEvent { + // window_id: WindowId(get_window_id(state.ns_window)), + // event: WindowEvent::ReceivedCharacter(character), + // })); + // } + // } + // }; + // AppState::queue_events(events); + // } trace!("Completed `doCommandBySelector`"); } -fn get_characters(event: id, ignore_modifiers: bool) -> String { - unsafe { - let characters: id = if ignore_modifiers { - msg_send![event, charactersIgnoringModifiers] - } else { - msg_send![event, characters] - }; +// Get characters +// fn get_characters(event: id, ignore_modifiers: bool) -> String { +// unsafe { +// let characters: id = if ignore_modifiers { +// msg_send![event, charactersIgnoringModifiers] +// } else { +// msg_send![event, characters] +// }; - assert_ne!(characters, nil); - let slice = slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len()); +// assert_ne!(characters, nil); +// let slice = slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len()); - let string = str::from_utf8_unchecked(slice); - string.to_owned() - } -} +// let string = str::from_utf8_unchecked(slice); +// string.to_owned() +// } +// } // As defined in: https://www.unicode.org/Public/MAPPINGS/VENDORS/APPLE/CORPCHAR.TXT -#[allow(clippy::match_like_matches_macro)] fn is_corporate_character(c: char) -> bool { + #[allow(clippy::match_like_matches_macro)] match c { '\u{F700}'..='\u{F747}' | '\u{F802}'..='\u{F84F}' @@ -600,29 +644,29 @@ fn is_corporate_character(c: char) -> bool { } } -// Retrieves a layout-independent keycode given an event. -fn retrieve_keycode(event: id) -> Option { - #[inline] - fn get_code(ev: id, raw: bool) -> Option { - let characters = get_characters(ev, raw); - characters.chars().next().and_then(char_to_keycode) - } - - // Cmd switches Roman letters for Dvorak-QWERTY layout, so we try modified characters first. - // If we don't get a match, then we fall back to unmodified characters. - let code = get_code(event, false).or_else(|| get_code(event, true)); - - // We've checked all layout related keys, so fall through to scancode. - // Reaching this code means that the key is layout-independent (e.g. Backspace, Return). - // - // We're additionally checking here for F21-F24 keys, since their keycode - // can vary, but we know that they are encoded - // in characters property. - code.or_else(|| { - let scancode = get_scancode(event); - scancode_to_keycode(scancode).or_else(|| check_function_keys(&get_characters(event, true))) - }) -} +// // Retrieves a layout-independent keycode given an event. +// fn retrieve_keycode(event: id) -> Option { +// #[inline] +// fn get_code(ev: id, raw: bool) -> Option { +// let characters = get_characters(ev, raw); +// characters.chars().next().and_then(|c| char_to_keycode(c)) +// } + +// // Cmd switches Roman letters for Dvorak-QWERTY layout, so we try modified characters first. +// // If we don't get a match, then we fall back to unmodified characters. +// let code = get_code(event, false).or_else(|| get_code(event, true)); + +// // We've checked all layout related keys, so fall through to scancode. +// // Reaching this code means that the key is layout-independent (e.g. Backspace, Return). +// // +// // We're additionally checking here for F21-F24 keys, since their keycode +// // can vary, but we know that they are encoded +// // in characters property. +// code.or_else(|| { +// let scancode = get_scancode(event); +// scancode_to_keycode(scancode).or_else(|| check_function_keys(&get_characters(event, true))) +// }) +// } // Update `state.modifiers` if `event` has something different fn update_potentially_stale_modifiers(state: &mut ViewState, event: id) { @@ -637,61 +681,62 @@ fn update_potentially_stale_modifiers(state: &mut ViewState, event: id) { } } -extern "C" fn key_down(this: &Object, _sel: Sel, event: id) { +extern "C" fn key_down(this: &mut Object, _sel: Sel, event: id) { trace!("Triggered `keyDown`"); unsafe { let state_ptr: *mut c_void = *this.get_ivar("taoState"); let state = &mut *(state_ptr as *mut ViewState); let window_id = WindowId(get_window_id(state.ns_window)); - let characters = get_characters(event, false); - - state.raw_characters = Some(characters.clone()); - let scancode = get_scancode(event) as u32; - let virtual_keycode = retrieve_keycode(event); + // keyboard refactor: don't seems to be needed anymore + // let characters = get_characters(event, false); + //state.raw_characters = Some(characters); - let is_repeat = msg_send![event, isARepeat]; + let is_repeat: BOOL = msg_send![event, isARepeat]; + let is_repeat = is_repeat == YES; update_potentially_stale_modifiers(state, event); - #[allow(deprecated)] + let pass_along = !is_repeat || !state.is_key_down; + if pass_along { + // See below for why we do this. + clear_marked_text(this); + state.key_triggered_ime = false; + + // Some keys (and only *some*, with no known reason) don't trigger `insertText`, while others do... + // So, we don't give repeats the opportunity to trigger that, since otherwise our hack will cause some + // keys to generate twice as many characters. + let array: id = msg_send![class!(NSArray), arrayWithObject: event]; + let () = msg_send![this, interpretKeyEvents: array]; + } + // The `interpretKeyEvents` above, may invoke `set_marked_text` or `insert_text`, + // if the event corresponds to an IME event. + let in_ime = state.key_triggered_ime; + let key_event = create_key_event(event, true, is_repeat, in_ime, None); + let is_arrow_key = is_arrow_key(key_event.physical_key); + if pass_along { + // The `interpretKeyEvents` above, may invoke `set_marked_text` or `insert_text`, + // if the event corresponds to an IME event. + // If `set_marked_text` or `insert_text` were not invoked, then the IME was deactivated, + // and we should cancel the IME session. + // When using arrow keys in an IME window, the input context won't invoke the + // IME related methods, so in that case we shouldn't cancel the IME session. + let is_preediting: bool = state.in_ime_preedit; + if is_preediting && !state.key_triggered_ime && !is_arrow_key { + // In this case we should cancel the IME session. + let () = msg_send![this, unmarkText]; + state.in_ime_preedit = false; + } + } let window_event = Event::WindowEvent { window_id, event: WindowEvent::KeyboardInput { device_id: DEVICE_ID, - input: KeyboardInput { - state: ElementState::Pressed, - scancode, - virtual_keycode, - modifiers: event_mods(event), - }, + event: key_event, is_synthetic: false, }, }; - - let pass_along = { - AppState::queue_event(EventWrapper::StaticEvent(window_event)); - // Emit `ReceivedCharacter` for key repeats - if is_repeat && state.is_key_down { - for character in characters.chars().filter(|c| !is_corporate_character(*c)) { - AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent { - window_id, - event: WindowEvent::ReceivedCharacter(character), - })); - } - false - } else { - true - } - }; - - if pass_along { - // Some keys (and only *some*, with no known reason) don't trigger `insertText`, while others do... - // So, we don't give repeats the opportunity to trigger that, since otherwise our hack will cause some - // keys to generate twice as many characters. - let array: id = msg_send![class!(NSArray), arrayWithObject: event]; - let _: () = msg_send![this, interpretKeyEvents: array]; - } + AppState::queue_event(EventWrapper::StaticEvent(window_event)); } trace!("Completed `keyDown`"); } @@ -704,74 +749,129 @@ extern "C" fn key_up(this: &Object, _sel: Sel, event: id) { state.is_key_down = false; - let scancode = get_scancode(event) as u32; - let virtual_keycode = retrieve_keycode(event); - update_potentially_stale_modifiers(state, event); - #[allow(deprecated)] let window_event = Event::WindowEvent { window_id: WindowId(get_window_id(state.ns_window)), event: WindowEvent::KeyboardInput { device_id: DEVICE_ID, - input: KeyboardInput { - state: ElementState::Released, - scancode, - virtual_keycode, - modifiers: event_mods(event), - }, + event: create_key_event(event, false, false, false, None), is_synthetic: false, }, }; - AppState::queue_event(EventWrapper::StaticEvent(window_event)); } trace!("Completed `keyUp`"); } -extern "C" fn flags_changed(this: &Object, _sel: Sel, event: id) { +extern "C" fn flags_changed(this: &Object, _sel: Sel, ns_event: id) { + use KeyCode::{ + AltLeft, AltRight, ControlLeft, ControlRight, ShiftLeft, ShiftRight, SuperLeft, SuperRight, + }; + trace!("Triggered `flagsChanged`"); unsafe { let state_ptr: *mut c_void = *this.get_ivar("taoState"); let state = &mut *(state_ptr as *mut ViewState); + let scancode = get_scancode(ns_event); + + // We'll correct the `is_press` and the `key_override` below. + let event = create_key_event(ns_event, false, false, false, Some(KeyCode::SuperLeft)); let mut events = VecDeque::with_capacity(4); - if let Some(window_event) = modifier_event( - event, - NSEventModifierFlags::NSShiftKeyMask, - state.modifiers.shift(), - ) { - state.modifiers.toggle(ModifiersState::SHIFT); - events.push_back(window_event); + macro_rules! process_event { + ($tao_flag:expr, $ns_flag:expr, $target_key:expr) => { + let ns_event_contains_keymask = NSEvent::modifierFlags(ns_event).contains($ns_flag); + let mut actual_key = KeyCode::from_scancode(scancode as u32); + let was_pressed = state.phys_modifiers.contains(&actual_key); + let mut is_pressed = ns_event_contains_keymask; + let matching_keys; + if actual_key == KeyCode::KeyA { + // When switching keyboard layout using Ctrl+Space, the Ctrl release event + // has `KeyA` as its keycode which would produce an incorrect key event. + // To avoid this, we detect this scenario and override the key with one + // that should be reasonable + actual_key = $target_key; + matching_keys = true; + } else { + matching_keys = actual_key == $target_key; + if matching_keys && is_pressed && was_pressed { + // If we received a modifier flags changed event AND the key that triggered + // it was already pressed then this must mean that this event indicates the + // release of that key. + is_pressed = false; + } + } + if matching_keys { + if is_pressed != was_pressed { + let mut event = event.clone(); + event.state = if is_pressed { + ElementState::Pressed + } else { + ElementState::Released + }; + event.physical_key = actual_key; + event.logical_key = code_to_key(event.physical_key, scancode); + events.push_back(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + event, + is_synthetic: false, + }); + if is_pressed { + state.phys_modifiers.insert($target_key); + } else { + state.phys_modifiers.remove(&$target_key); + } + if ns_event_contains_keymask { + state.modifiers.insert($tao_flag); + } else { + state.modifiers.remove($tao_flag); + } + } + } + }; } - - if let Some(window_event) = modifier_event( - event, + process_event!( + ModifiersState::SHIFT, + NSEventModifierFlags::NSShiftKeyMask, + ShiftLeft + ); + process_event!( + ModifiersState::SHIFT, + NSEventModifierFlags::NSShiftKeyMask, + ShiftRight + ); + process_event!( + ModifiersState::CONTROL, NSEventModifierFlags::NSControlKeyMask, - state.modifiers.ctrl(), - ) { - state.modifiers.toggle(ModifiersState::CTRL); - events.push_back(window_event); - } - - if let Some(window_event) = modifier_event( - event, - NSEventModifierFlags::NSCommandKeyMask, - state.modifiers.logo(), - ) { - state.modifiers.toggle(ModifiersState::LOGO); - events.push_back(window_event); - } - - if let Some(window_event) = modifier_event( - event, + ControlLeft + ); + process_event!( + ModifiersState::CONTROL, + NSEventModifierFlags::NSControlKeyMask, + ControlRight + ); + process_event!( + ModifiersState::ALT, NSEventModifierFlags::NSAlternateKeyMask, - state.modifiers.alt(), - ) { - state.modifiers.toggle(ModifiersState::ALT); - events.push_back(window_event); - } + AltLeft + ); + process_event!( + ModifiersState::ALT, + NSEventModifierFlags::NSAlternateKeyMask, + AltRight + ); + process_event!( + ModifiersState::SUPER, + NSEventModifierFlags::NSCommandKeyMask, + SuperLeft + ); + process_event!( + ModifiersState::SUPER, + NSEventModifierFlags::NSCommandKeyMask, + SuperRight + ); let window_id = WindowId(get_window_id(state.ns_window)); @@ -820,29 +920,21 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { let state_ptr: *mut c_void = *this.get_ivar("taoState"); let state = &mut *(state_ptr as *mut ViewState); - let scancode = 0x2f; - let virtual_keycode = scancode_to_keycode(scancode); - debug_assert_eq!(virtual_keycode, Some(VirtualKeyCode::Period)); - let event: id = msg_send![NSApp(), currentEvent]; - update_potentially_stale_modifiers(state, event); - #[allow(deprecated)] + let scancode = 0x2f; + let key = KeyCode::from_scancode(scancode); + debug_assert_eq!(key, KeyCode::Period); + let window_event = Event::WindowEvent { window_id: WindowId(get_window_id(state.ns_window)), event: WindowEvent::KeyboardInput { device_id: DEVICE_ID, - input: KeyboardInput { - state: ElementState::Pressed, - scancode: scancode as _, - virtual_keycode, - modifiers: event_mods(event), - }, + event: create_key_event(event, true, false, false, Some(key)), is_synthetic: false, }, }; - AppState::queue_event(EventWrapper::StaticEvent(window_event)); } trace!("Completed `cancelOperation`"); diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs index 9c52beba1..87200edb4 100644 --- a/src/platform_impl/macos/window_delegate.rs +++ b/src/platform_impl/macos/window_delegate.rs @@ -19,7 +19,8 @@ use objc::{ use crate::{ dpi::{LogicalPosition, LogicalSize}, - event::{Event, ModifiersState, WindowEvent}, + event::{Event, WindowEvent}, + keyboard::ModifiersState, platform_impl::platform::{ app_state::{AppState, INTERRUPT_EVENT_LOOP_EXIT}, event::{EventProxy, EventWrapper}, diff --git a/src/platform_impl/windows/accelerator.rs b/src/platform_impl/windows/accelerator.rs new file mode 100644 index 000000000..704e22592 --- /dev/null +++ b/src/platform_impl/windows/accelerator.rs @@ -0,0 +1,67 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +use lazy_static::lazy_static; +use winapi::{ctypes::c_int, shared::windef::*, um::winuser::*}; + +// NOTE: +// https://docs.microsoft.com/en-us/windows/win32/wsw/thread-safety +// All handles you obtain from functions in Kernel32 are thread-safe, +// unless the MSDN Library article for the function explicitly mentions it is not. + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +struct WindowHandle(HWND); +unsafe impl Send for WindowHandle {} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +struct AccelHandle(HACCEL); +unsafe impl Send for AccelHandle {} +unsafe impl Sync for AccelHandle {} + +lazy_static! { + static ref ACCEL_TABLES: Mutex>> = + Mutex::new(HashMap::default()); +} + +/// A Accelerators Table for Windows +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub(crate) struct AccelTable { + accel: AccelHandle, +} + +impl AccelTable { + fn new(accel: &[ACCEL]) -> AccelTable { + let accel = + unsafe { CreateAcceleratorTableW(accel as *const _ as *mut _, accel.len() as c_int) }; + AccelTable { + accel: AccelHandle(accel), + } + } + + pub(crate) fn handle(&self) -> HACCEL { + self.accel.0 + } +} + +pub(crate) fn register_accel(hwnd: HWND, accel: &[ACCEL]) { + let mut table = ACCEL_TABLES.lock().unwrap(); + table.insert(WindowHandle(hwnd), Arc::new(AccelTable::new(accel))); +} + +impl Drop for AccelTable { + fn drop(&mut self) { + unsafe { + DestroyAcceleratorTable(self.accel.0); + } + } +} + +pub(crate) fn find_accels(hwnd: HWND) -> Option> { + let table = ACCEL_TABLES.lock().unwrap(); + table.get(&WindowHandle(hwnd)).cloned() +} diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index dfb1b976f..c95ef5354 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -22,6 +22,7 @@ use std::{ use winapi::shared::basetsd::{DWORD_PTR, UINT_PTR}; use winapi::{ + ctypes::c_int, shared::{ minwindef::{BOOL, DWORD, HIWORD, INT, LOWORD, LPARAM, LRESULT, UINT, WORD, WPARAM}, windef::{HWND, POINT, RECT}, @@ -30,20 +31,26 @@ use winapi::{ um::{ commctrl, libloaderapi, ole2, processthreadsapi, winbase, winnt::{HANDLE, LONG, LPCSTR, SHORT}, - winuser, + winuser::{self, RAWINPUT}, }, }; use crate::{ + accelerator::AcceleratorId, dpi::{PhysicalPosition, PhysicalSize}, - event::{DeviceEvent, Event, Force, KeyboardInput, Touch, TouchPhase, WindowEvent}, + event::{DeviceEvent, Event, Force, RawKeyEvent, Touch, TouchPhase, WindowEvent}, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, + keyboard::{KeyCode, ModifiersState}, + menu::{MenuId, MenuType}, monitor::MonitorHandle as RootMonitorHandle, platform_impl::platform::{ + accelerator, dark_mode::try_theme, dpi::{become_dpi_aware, dpi_to_scale_factor, enable_non_client_dpi_scaling}, drop_handler::FileDropHandler, - event::{self, handle_extended_keys, process_key_params, vkey_to_tao_vkey}, + keyboard::is_msg_keyboard_related, + keyboard_layout::LAYOUT_CACHE, + minimal_ime::is_msg_ime_related, monitor::{self, MonitorHandle}, raw_input, util, window_state::{CursorFlags, WindowFlags, WindowState}, @@ -111,6 +118,13 @@ impl ThreadMsgTargetSubclassInput { } } +/// The result of a subclass procedure (the message handling callback) +pub(crate) enum ProcResult { + DefSubclassProc, // <- this should be the default value + DefWindowProc, + Value(isize), +} + pub struct EventLoop { thread_msg_sender: Sender, window_target: RootELW, @@ -220,8 +234,23 @@ impl EventLoop { if 0 == winuser::GetMessageW(&mut msg, ptr::null_mut(), 0, 0) { break 'main; } - winuser::TranslateMessage(&mut msg); - winuser::DispatchMessageW(&mut msg); + + // global accelerator + if msg.message == winuser::WM_HOTKEY { + let event_loop_runner = self.window_target.p.runner_shared.clone(); + event_loop_runner + .send_event(Event::GlobalShortcutEvent(AcceleratorId(msg.wParam as u16))); + } + + // window accelerator + let accels = accelerator::find_accels(winuser::GetAncestor(msg.hwnd, winuser::GA_ROOT)); + let translated = accels.map_or(false, |it| { + winuser::TranslateAcceleratorW(msg.hwnd, it.handle(), &mut msg) != 0 + }); + if !translated { + winuser::TranslateMessage(&mut msg); + winuser::DispatchMessageW(&mut msg); + } if let Err(payload) = runner.take_panic_error() { runner.reset_runner(); @@ -294,7 +323,7 @@ fn get_wait_thread_id() -> DWORD { ); assert_eq!( msg.message, *SEND_WAIT_THREAD_ID_MSG_ID, - "this shouldn't be possible. please open an issue with Tao. error code: {}", + "this shouldn't be possible. please open an issue with Tauri. error code: {}", result ); msg.lParam as DWORD @@ -751,10 +780,15 @@ unsafe fn process_control_flow(runner: &EventLoopRunner) { } /// Emit a `ModifiersChanged` event whenever modifiers have changed. -fn update_modifiers(window: HWND, subclass_input: &SubclassInput) { +/// Returns the current modifier state +fn update_modifiers(window: HWND, subclass_input: &SubclassInput) -> ModifiersState { use crate::event::WindowEvent::ModifiersChanged; - let modifiers = event::get_key_mods(); + let modifiers = { + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + layouts.get_agnostic_mods() + }; + let mut window_state = subclass_input.window_state.lock(); if window_state.modifiers_state != modifiers { window_state.modifiers_state = modifiers; @@ -769,6 +803,7 @@ fn update_modifiers(window: HWND, subclass_input: &SubclassInput) { }); } } + modifiers } /// Any window whose callback is configured to this function will have its events propagated @@ -825,6 +860,75 @@ unsafe fn public_window_callback_inner( winuser::RDW_INTERNALPAINT, ); + let mut result = ProcResult::DefSubclassProc; + + // Send new modifiers before sending key events. + let mods_changed_callback = || match msg { + winuser::WM_KEYDOWN | winuser::WM_SYSKEYDOWN | winuser::WM_KEYUP | winuser::WM_SYSKEYUP => { + update_modifiers(window, subclass_input); + result = ProcResult::Value(0); + } + _ => (), + }; + subclass_input + .event_loop_runner + .catch_unwind(mods_changed_callback) + .unwrap_or_else(|| result = ProcResult::Value(-1)); + + let keyboard_callback = || { + use crate::event::WindowEvent::KeyboardInput; + let is_keyboard_related = is_msg_keyboard_related(msg); + if !is_keyboard_related { + // We return early to avoid a deadlock from locking the window state + // when not appropriate. + return; + } + let events = { + let mut window_state = subclass_input.window_state.lock(); + window_state + .key_event_builder + .process_message(window, msg, wparam, lparam, &mut result) + }; + for event in events { + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: KeyboardInput { + device_id: DEVICE_ID, + event: event.event, + is_synthetic: event.is_synthetic, + }, + }); + } + }; + subclass_input + .event_loop_runner + .catch_unwind(keyboard_callback) + .unwrap_or_else(|| result = ProcResult::Value(-1)); + + let ime_callback = || { + use crate::event::WindowEvent::ReceivedImeText; + let is_ime_related = is_msg_ime_related(msg); + if !is_ime_related { + return; + } + let text = { + let mut window_state = subclass_input.window_state.lock(); + window_state + .ime_handler + .process_message(window, msg, wparam, lparam, &mut result) + }; + if let Some(str) = text { + subclass_input.send_event(Event::WindowEvent { + window_id: RootWindowId(WindowId(window)), + event: ReceivedImeText(str), + }); + } + }; + subclass_input + .event_loop_runner + .catch_unwind(ime_callback) + .unwrap_or_else(|| result = ProcResult::Value(-1)); + // I decided to bind the closure to `callback` and pass it to catch_unwind rather than passing // the closure to catch_unwind directly so that the match body indendation wouldn't change and // the git blame and history would be preserved. @@ -834,7 +938,7 @@ unsafe fn public_window_callback_inner( .window_state .lock() .set_window_flags_in_place(|f| f.insert(WindowFlags::MARKER_IN_SIZE_MOVE)); - 0 + result = ProcResult::Value(0); } winuser::WM_EXITSIZEMOVE => { @@ -842,18 +946,16 @@ unsafe fn public_window_callback_inner( .window_state .lock() .set_window_flags_in_place(|f| f.remove(WindowFlags::MARKER_IN_SIZE_MOVE)); - 0 + result = ProcResult::Value(0); } winuser::WM_NCCREATE => { enable_non_client_dpi_scaling(window); - commctrl::DefSubclassProc(window, msg, wparam, lparam) } winuser::WM_NCLBUTTONDOWN => { if wparam == winuser::HTCAPTION as _ { winuser::PostMessageW(window, winuser::WM_MOUSEMOVE, 0, lparam); } - commctrl::DefSubclassProc(window, msg, wparam, lparam) } winuser::WM_CLOSE => { @@ -862,7 +964,7 @@ unsafe fn public_window_callback_inner( window_id: RootWindowId(WindowId(window)), event: CloseRequested, }); - 0 + result = ProcResult::Value(0); } winuser::WM_DESTROY => { @@ -873,13 +975,13 @@ unsafe fn public_window_callback_inner( event: Destroyed, }); subclass_input.event_loop_runner.remove_window(window); - 0 + result = ProcResult::Value(0); } winuser::WM_NCDESTROY => { remove_window_subclass::(window); subclass_input.subclass_removed.set(true); - 0 + result = ProcResult::Value(0); } winuser::WM_PAINT => { @@ -900,8 +1002,6 @@ unsafe fn public_window_callback_inner( process_control_flow(&subclass_input.event_loop_runner); } } - - commctrl::DefSubclassProc(window, msg, wparam, lparam) } winuser::WM_WINDOWPOSCHANGING => { @@ -948,7 +1048,7 @@ unsafe fn public_window_callback_inner( } } - 0 + result = ProcResult::Value(0); } // WM_MOVE supplies client area positions, so we send Moved here instead. @@ -965,7 +1065,7 @@ unsafe fn public_window_callback_inner( } // This is necessary for us to still get sent WM_SIZE. - commctrl::DefSubclassProc(window, msg, wparam, lparam) + result = ProcResult::DefSubclassProc; } winuser::WM_SIZE => { @@ -992,40 +1092,17 @@ unsafe fn public_window_callback_inner( } subclass_input.send_event(event); - 0 + result = ProcResult::Value(0); } - winuser::WM_CHAR | winuser::WM_SYSCHAR => { - use crate::event::WindowEvent::ReceivedCharacter; - use std::char; - let is_high_surrogate = 0xD800 <= wparam && wparam <= 0xDBFF; - let is_low_surrogate = 0xDC00 <= wparam && wparam <= 0xDFFF; - - if is_high_surrogate { - subclass_input.window_state.lock().high_surrogate = Some(wparam as u16); - } else if is_low_surrogate { - let high_surrogate = subclass_input.window_state.lock().high_surrogate.take(); - - if let Some(high_surrogate) = high_surrogate { - let pair = [high_surrogate, wparam as u16]; - if let Some(Ok(chr)) = char::decode_utf16(pair.iter().copied()).next() { - subclass_input.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: ReceivedCharacter(chr), - }); - } - } - } else { - subclass_input.window_state.lock().high_surrogate = None; - - if let Some(chr) = char::from_u32(wparam as u32) { - subclass_input.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: ReceivedCharacter(chr), - }); - } - } - 0 + // this is catched by the accelerator translator + winuser::WM_COMMAND => { + let menu_id = LOWORD(wparam as u32) as u16; + subclass_input.send_event(Event::MenuEvent { + menu_id: MenuId(menu_id as u16), + // todo fix menutype + origin: MenuType::MenuBar, + }); } // this is necessary for us to maintain minimize/restore state @@ -1043,11 +1120,12 @@ unsafe fn public_window_callback_inner( if wparam == winuser::SC_SCREENSAVE { let window_state = subclass_input.window_state.lock(); if window_state.fullscreen.is_some() { - return 0; + result = ProcResult::Value(0); + return; } } - winuser::DefWindowProcW(window, msg, wparam, lparam) + result = ProcResult::DefWindowProc; } winuser::WM_MOUSEMOVE => { @@ -1092,19 +1170,18 @@ unsafe fn public_window_callback_inner( w.mouse.last_position = Some(position); } if cursor_moved { - update_modifiers(window, subclass_input); - + let modifiers = update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: CursorMoved { device_id: DEVICE_ID, position, - modifiers: event::get_key_mods(), + modifiers, }, }); } - 0 + result = ProcResult::Value(0); } winuser::WM_MOUSELEAVE => { @@ -1123,7 +1200,7 @@ unsafe fn public_window_callback_inner( }, }); - 0 + result = ProcResult::Value(0); } winuser::WM_MOUSEWHEEL => { @@ -1133,7 +1210,7 @@ unsafe fn public_window_callback_inner( let value = value as i32; let value = value as f32 / winuser::WHEEL_DELTA as f32; - update_modifiers(window, subclass_input); + let modifiers = update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), @@ -1141,11 +1218,11 @@ unsafe fn public_window_callback_inner( device_id: DEVICE_ID, delta: LineDelta(0.0, value), phase: TouchPhase::Moved, - modifiers: event::get_key_mods(), + modifiers, }, }); - 0 + result = ProcResult::Value(0); } winuser::WM_MOUSEHWHEEL => { @@ -1155,7 +1232,7 @@ unsafe fn public_window_callback_inner( let value = value as i32; let value = value as f32 / winuser::WHEEL_DELTA as f32; - update_modifiers(window, subclass_input); + let modifiers = update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), @@ -1163,77 +1240,25 @@ unsafe fn public_window_callback_inner( device_id: DEVICE_ID, delta: LineDelta(value, 0.0), phase: TouchPhase::Moved, - modifiers: event::get_key_mods(), + modifiers, }, }); - 0 + result = ProcResult::Value(0); } winuser::WM_KEYDOWN | winuser::WM_SYSKEYDOWN => { - use crate::event::{ElementState::Pressed, VirtualKeyCode}; if msg == winuser::WM_SYSKEYDOWN && wparam as i32 == winuser::VK_F4 { - commctrl::DefSubclassProc(window, msg, wparam, lparam) - } else { - if let Some((scancode, vkey)) = process_key_params(wparam, lparam) { - update_modifiers(window, subclass_input); - - #[allow(deprecated)] - subclass_input.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: Pressed, - scancode, - virtual_keycode: vkey, - modifiers: event::get_key_mods(), - }, - is_synthetic: false, - }, - }); - // Windows doesn't emit a delete character by default, but in order to make it - // consistent with the other platforms we'll emit a delete character here. - if vkey == Some(VirtualKeyCode::Delete) { - subclass_input.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: WindowEvent::ReceivedCharacter('\u{7F}'), - }); - } - } - 0 + result = ProcResult::DefSubclassProc; } } - winuser::WM_KEYUP | winuser::WM_SYSKEYUP => { - use crate::event::ElementState::Released; - if let Some((scancode, vkey)) = process_key_params(wparam, lparam) { - update_modifiers(window, subclass_input); - - #[allow(deprecated)] - subclass_input.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: Released, - scancode, - virtual_keycode: vkey, - modifiers: event::get_key_mods(), - }, - is_synthetic: false, - }, - }); - } - 0 - } - winuser::WM_LBUTTONDOWN => { use crate::event::{ElementState::Pressed, MouseButton::Left, WindowEvent::MouseInput}; capture_mouse(window, &mut *subclass_input.window_state.lock()); - update_modifiers(window, subclass_input); + let modifiers = update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), @@ -1241,10 +1266,10 @@ unsafe fn public_window_callback_inner( device_id: DEVICE_ID, state: Pressed, button: Left, - modifiers: event::get_key_mods(), + modifiers, }, }); - 0 + result = ProcResult::Value(0); } winuser::WM_LBUTTONUP => { @@ -1252,7 +1277,7 @@ unsafe fn public_window_callback_inner( release_mouse(subclass_input.window_state.lock()); - update_modifiers(window, subclass_input); + let modifiers = update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), @@ -1260,10 +1285,10 @@ unsafe fn public_window_callback_inner( device_id: DEVICE_ID, state: Released, button: Left, - modifiers: event::get_key_mods(), + modifiers, }, }); - 0 + result = ProcResult::Value(0); } winuser::WM_RBUTTONDOWN => { @@ -1271,7 +1296,7 @@ unsafe fn public_window_callback_inner( capture_mouse(window, &mut *subclass_input.window_state.lock()); - update_modifiers(window, subclass_input); + let modifiers = update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), @@ -1279,10 +1304,10 @@ unsafe fn public_window_callback_inner( device_id: DEVICE_ID, state: Pressed, button: Right, - modifiers: event::get_key_mods(), + modifiers, }, }); - 0 + result = ProcResult::Value(0); } winuser::WM_RBUTTONUP => { @@ -1290,7 +1315,7 @@ unsafe fn public_window_callback_inner( release_mouse(subclass_input.window_state.lock()); - update_modifiers(window, subclass_input); + let modifiers = update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), @@ -1298,10 +1323,10 @@ unsafe fn public_window_callback_inner( device_id: DEVICE_ID, state: Released, button: Right, - modifiers: event::get_key_mods(), + modifiers, }, }); - 0 + result = ProcResult::Value(0); } winuser::WM_MBUTTONDOWN => { @@ -1309,7 +1334,7 @@ unsafe fn public_window_callback_inner( capture_mouse(window, &mut *subclass_input.window_state.lock()); - update_modifiers(window, subclass_input); + let modifiers = update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), @@ -1317,10 +1342,10 @@ unsafe fn public_window_callback_inner( device_id: DEVICE_ID, state: Pressed, button: Middle, - modifiers: event::get_key_mods(), + modifiers, }, }); - 0 + result = ProcResult::Value(0); } winuser::WM_MBUTTONUP => { @@ -1328,7 +1353,7 @@ unsafe fn public_window_callback_inner( release_mouse(subclass_input.window_state.lock()); - update_modifiers(window, subclass_input); + let modifiers = update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), @@ -1336,10 +1361,10 @@ unsafe fn public_window_callback_inner( device_id: DEVICE_ID, state: Released, button: Middle, - modifiers: event::get_key_mods(), + modifiers, }, }); - 0 + result = ProcResult::Value(0); } winuser::WM_XBUTTONDOWN => { @@ -1348,7 +1373,7 @@ unsafe fn public_window_callback_inner( capture_mouse(window, &mut *subclass_input.window_state.lock()); - update_modifiers(window, subclass_input); + let modifiers = update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), @@ -1356,10 +1381,10 @@ unsafe fn public_window_callback_inner( device_id: DEVICE_ID, state: Pressed, button: Other(xbutton), - modifiers: event::get_key_mods(), + modifiers, }, }); - 0 + result = ProcResult::Value(0); } winuser::WM_XBUTTONUP => { @@ -1368,7 +1393,7 @@ unsafe fn public_window_callback_inner( release_mouse(subclass_input.window_state.lock()); - update_modifiers(window, subclass_input); + let modifiers = update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), @@ -1376,10 +1401,10 @@ unsafe fn public_window_callback_inner( device_id: DEVICE_ID, state: Released, button: Other(xbutton), - modifiers: event::get_key_mods(), + modifiers, }, }); - 0 + result = ProcResult::Value(0); } winuser::WM_CAPTURECHANGED => { @@ -1390,7 +1415,7 @@ unsafe fn public_window_callback_inner( if lparam != window as isize { subclass_input.window_state.lock().mouse.capture_count = 0; } - 0 + result = ProcResult::Value(0); } winuser::WM_TOUCH => { @@ -1439,7 +1464,7 @@ unsafe fn public_window_callback_inner( } } winuser::CloseTouchInputHandle(htouch); - 0 + result = ProcResult::Value(0); } winuser::WM_POINTERDOWN | winuser::WM_POINTERUPDATE | winuser::WM_POINTERUP => { @@ -1462,7 +1487,8 @@ unsafe fn public_window_callback_inner( std::ptr::null_mut(), ) == 0 { - return 0; + result = ProcResult::Value(0); + return; } let pointer_info_count = (entries_count * pointers_count) as usize; @@ -1475,7 +1501,8 @@ unsafe fn public_window_callback_inner( pointer_infos.as_mut_ptr(), ) == 0 { - return 0; + result = ProcResult::Value(0); + return; } // https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getpointerframeinfohistory @@ -1568,66 +1595,23 @@ unsafe fn public_window_callback_inner( SkipPointerFrameMessages(pointer_id); } - 0 + result = ProcResult::Value(0); } winuser::WM_SETFOCUS => { - use crate::event::{ElementState::Released, WindowEvent::Focused}; - for windows_keycode in event::get_pressed_keys() { - let scancode = winuser::MapVirtualKeyA(windows_keycode as _, winuser::MAPVK_VK_TO_VSC); - let virtual_keycode = event::vkey_to_tao_vkey(windows_keycode); - - update_modifiers(window, subclass_input); - - #[allow(deprecated)] - subclass_input.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - scancode, - virtual_keycode, - state: Released, - modifiers: event::get_key_mods(), - }, - is_synthetic: true, - }, - }) - } + use crate::event::WindowEvent::Focused; + update_modifiers(window, subclass_input); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: Focused(true), }); - 0 + result = ProcResult::Value(0); } winuser::WM_KILLFOCUS => { - use crate::event::{ - ElementState::Released, - ModifiersState, - WindowEvent::{Focused, ModifiersChanged}, - }; - for windows_keycode in event::get_pressed_keys() { - let scancode = winuser::MapVirtualKeyA(windows_keycode as _, winuser::MAPVK_VK_TO_VSC); - let virtual_keycode = event::vkey_to_tao_vkey(windows_keycode); - - #[allow(deprecated)] - subclass_input.send_event(Event::WindowEvent { - window_id: RootWindowId(WindowId(window)), - event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - scancode, - virtual_keycode, - state: Released, - modifiers: event::get_key_mods(), - }, - is_synthetic: true, - }, - }) - } + use crate::event::WindowEvent::{Focused, ModifiersChanged}; subclass_input.window_state.lock().modifiers_state = ModifiersState::empty(); subclass_input.send_event(Event::WindowEvent { @@ -1639,7 +1623,7 @@ unsafe fn public_window_callback_inner( window_id: RootWindowId(WindowId(window)), event: Focused(false), }); - 0 + result = ProcResult::Value(0); } winuser::WM_SETCURSOR => { @@ -1660,17 +1644,12 @@ unsafe fn public_window_callback_inner( Some(cursor) => { let cursor = winuser::LoadCursorW(ptr::null_mut(), cursor.to_windows_cursor()); winuser::SetCursor(cursor); - 0 + result = ProcResult::Value(0); } - None => winuser::DefWindowProcW(window, msg, wparam, lparam), + None => result = ProcResult::DefWindowProc, } } - winuser::WM_DROPFILES => { - // See `FileDropHandler` for implementation. - 0 - } - winuser::WM_GETMINMAXINFO => { let mmi = lparam as *mut winuser::MINMAXINFO; @@ -1695,7 +1674,7 @@ unsafe fn public_window_callback_inner( } } - 0 + result = ProcResult::Value(0); } // Only sent on Windows 8.1 or newer. On Windows 7 and older user has to log out to change @@ -1717,7 +1696,8 @@ unsafe fn public_window_callback_inner( window_state.scale_factor = new_scale_factor; if new_scale_factor == old_scale_factor { - return 0; + result = ProcResult::Value(0); + return; } window_state.fullscreen.is_none() @@ -1907,7 +1887,7 @@ unsafe fn public_window_callback_inner( winuser::SWP_NOZORDER | winuser::SWP_NOACTIVATE, ); - 0 + result = ProcResult::Value(0); } winuser::WM_SETTINGCHANGE => { @@ -1928,22 +1908,18 @@ unsafe fn public_window_callback_inner( }); } } - - commctrl::DefSubclassProc(window, msg, wparam, lparam) } _ => { if msg == *DESTROY_MSG_ID { winuser::DestroyWindow(window); - 0 + result = ProcResult::Value(0); } else if msg == *SET_RETAIN_STATE_ON_SIZE_MSG_ID { let mut window_state = subclass_input.window_state.lock(); window_state.set_window_flags_in_place(|f| { f.set(WindowFlags::MARKER_RETAIN_STATE_ON_SIZE, wparam != 0) }); - 0 - } else { - commctrl::DefSubclassProc(window, msg, wparam, lparam) + result = ProcResult::Value(0); } } }; @@ -1951,7 +1927,13 @@ unsafe fn public_window_callback_inner( subclass_input .event_loop_runner .catch_unwind(callback) - .unwrap_or(-1) + .unwrap_or_else(|| result = ProcResult::Value(-1)); + + match result { + ProcResult::DefSubclassProc => commctrl::DefSubclassProc(window, msg, wparam, lparam), + ProcResult::DefWindowProc => winuser::DefWindowProcW(window, msg, wparam, lparam), + ProcResult::Value(val) => val, + } } unsafe extern "system" fn thread_event_target_callback( @@ -2033,101 +2015,8 @@ unsafe extern "system" fn thread_event_target_callback( } winuser::WM_INPUT => { - use crate::event::{ - DeviceEvent::{Button, Key, Motion, MouseMotion, MouseWheel}, - ElementState::{Pressed, Released}, - MouseScrollDelta::LineDelta, - }; - if let Some(data) = raw_input::get_raw_input_data(lparam as _) { - let device_id = wrap_device_id(data.header.hDevice as _); - - if data.header.dwType == winuser::RIM_TYPEMOUSE { - let mouse = data.data.mouse(); - - if util::has_flag(mouse.usFlags, winuser::MOUSE_MOVE_RELATIVE) { - let x = mouse.lLastX as f64; - let y = mouse.lLastY as f64; - - if x != 0.0 { - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: Motion { axis: 0, value: x }, - }); - } - - if y != 0.0 { - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: Motion { axis: 1, value: y }, - }); - } - - if x != 0.0 || y != 0.0 { - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: MouseMotion { delta: (x, y) }, - }); - } - } - - if util::has_flag(mouse.usButtonFlags, winuser::RI_MOUSE_WHEEL) { - let delta = mouse.usButtonData as SHORT as f32 / winuser::WHEEL_DELTA as f32; - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: MouseWheel { - delta: LineDelta(0.0, delta), - }, - }); - } - - let button_state = raw_input::get_raw_mouse_button_state(mouse.usButtonFlags); - // Left, middle, and right, respectively. - for (index, state) in button_state.iter().enumerate() { - if let Some(state) = *state { - // This gives us consistency with Linux, since there doesn't - // seem to be anything else reasonable to do for a mouse - // button ID. - let button = (index + 1) as _; - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: Button { button, state }, - }); - } - } - } else if data.header.dwType == winuser::RIM_TYPEKEYBOARD { - let keyboard = data.data.keyboard(); - - let pressed = - keyboard.Message == winuser::WM_KEYDOWN || keyboard.Message == winuser::WM_SYSKEYDOWN; - let released = - keyboard.Message == winuser::WM_KEYUP || keyboard.Message == winuser::WM_SYSKEYUP; - - if pressed || released { - let state = if pressed { Pressed } else { Released }; - - let scancode = keyboard.MakeCode as _; - let extended = util::has_flag(keyboard.Flags, winuser::RI_KEY_E0 as _) - | util::has_flag(keyboard.Flags, winuser::RI_KEY_E1 as _); - - if let Some((vkey, scancode)) = - handle_extended_keys(keyboard.VKey as _, scancode, extended) - { - let virtual_keycode = vkey_to_tao_vkey(vkey); - - #[allow(deprecated)] - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: Key(KeyboardInput { - scancode, - state, - virtual_keycode, - modifiers: event::get_key_mods(), - }), - }); - } - } - } + handle_raw_input(&subclass_input, data); } commctrl::DefSubclassProc(window, msg, wparam, lparam) @@ -2197,3 +2086,183 @@ unsafe extern "system" fn thread_event_target_callback( } result } + +unsafe fn handle_raw_input( + subclass_input: &Box>, + data: RAWINPUT, +) { + use crate::event::{ + DeviceEvent::{Button, Key, Motion, MouseMotion, MouseWheel}, + ElementState::{Pressed, Released}, + MouseScrollDelta::LineDelta, + }; + + let device_id = wrap_device_id(data.header.hDevice as _); + + if data.header.dwType == winuser::RIM_TYPEMOUSE { + let mouse = data.data.mouse(); + + if util::has_flag(mouse.usFlags, winuser::MOUSE_MOVE_RELATIVE) { + let x = mouse.lLastX as f64; + let y = mouse.lLastY as f64; + + if x != 0.0 { + subclass_input.send_event(Event::DeviceEvent { + device_id, + event: Motion { axis: 0, value: x }, + }); + } + + if y != 0.0 { + subclass_input.send_event(Event::DeviceEvent { + device_id, + event: Motion { axis: 1, value: y }, + }); + } + + if x != 0.0 || y != 0.0 { + subclass_input.send_event(Event::DeviceEvent { + device_id, + event: MouseMotion { delta: (x, y) }, + }); + } + } + + if util::has_flag(mouse.usButtonFlags, winuser::RI_MOUSE_WHEEL) { + // We must cast to SHORT first, becaues `usButtonData` must be interpreted as signed. + let delta = mouse.usButtonData as SHORT as f32 / winuser::WHEEL_DELTA as f32; + subclass_input.send_event(Event::DeviceEvent { + device_id, + event: MouseWheel { + delta: LineDelta(0.0, delta), + }, + }); + } + + let button_state = raw_input::get_raw_mouse_button_state(mouse.usButtonFlags); + // Left, middle, and right, respectively. + for (index, state) in button_state.iter().enumerate() { + if let Some(state) = *state { + // This gives us consistency with X11, since there doesn't + // seem to be anything else reasonable to do for a mouse + // button ID. + let button = (index + 1) as _; + subclass_input.send_event(Event::DeviceEvent { + device_id, + event: Button { button, state }, + }); + } + } + } else if data.header.dwType == winuser::RIM_TYPEKEYBOARD { + let keyboard = data.data.keyboard(); + + let pressed = + keyboard.Message == winuser::WM_KEYDOWN || keyboard.Message == winuser::WM_SYSKEYDOWN; + let released = + keyboard.Message == winuser::WM_KEYUP || keyboard.Message == winuser::WM_SYSKEYUP; + + if !pressed && !released { + return; + } + + let state = if pressed { Pressed } else { Released }; + let extension = { + if util::has_flag(keyboard.Flags, winuser::RI_KEY_E0 as _) { + 0xE000 + } else if util::has_flag(keyboard.Flags, winuser::RI_KEY_E1 as _) { + 0xE100 + } else { + 0x0000 + } + }; + let scancode; + if keyboard.MakeCode == 0 { + // In some cases (often with media keys) the device reports a scancode of 0 but a + // valid virtual key. In these cases we obtain the scancode from the virtual key. + scancode = winuser::MapVirtualKeyW(keyboard.VKey as u32, winuser::MAPVK_VK_TO_VSC_EX) as u16; + } else { + scancode = keyboard.MakeCode | extension; + } + if scancode == 0xE11D || scancode == 0xE02A { + // At the hardware (or driver?) level, pressing the Pause key is equivalent to pressing + // Ctrl+NumLock. + // This equvalence means that if the user presses Pause, the keyboard will emit two + // subsequent keypresses: + // 1, 0xE11D - Which is a left Ctrl (0x1D) with an extension flag (0xE100) + // 2, 0x0045 - Which on its own can be interpreted as Pause + // + // There's another combination which isn't quite an equivalence: + // PrtSc used to be Shift+Asterisk. This means that on some keyboards, presssing + // PrtSc (print screen) produces the following sequence: + // 1, 0xE02A - Which is a left shift (0x2A) with an extension flag (0xE000) + // 2, 0xE037 - Which is a numpad multiply (0x37) with an exteion flag (0xE000). This on + // its own it can be interpreted as PrtSc + // + // For this reason, if we encounter the first keypress, we simply ignore it, trusting + // that there's going to be another event coming, from which we can extract the + // appropriate key. + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + return; + } + let code; + if keyboard.VKey as c_int == winuser::VK_NUMLOCK { + // Historically, the NumLock and the Pause key were one and the same physical key. + // The user could trigger Pause by pressing Ctrl+NumLock. + // Now these are often physically separate and the two keys can be differentiated by + // checking the extension flag of the scancode. NumLock is 0xE045, Pause is 0x0045. + // + // However in this event, both keys are reported as 0x0045 even on modern hardware. + // Therefore we use the virtual key instead to determine whether it's a NumLock and + // set the KeyCode accordingly. + // + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + code = KeyCode::NumLock; + } else { + code = KeyCode::from_scancode(scancode as u32); + } + if keyboard.VKey as c_int == winuser::VK_SHIFT { + match code { + KeyCode::NumpadDecimal + | KeyCode::Numpad0 + | KeyCode::Numpad1 + | KeyCode::Numpad2 + | KeyCode::Numpad3 + | KeyCode::Numpad4 + | KeyCode::Numpad5 + | KeyCode::Numpad6 + | KeyCode::Numpad7 + | KeyCode::Numpad8 + | KeyCode::Numpad9 => { + // On Windows, holding the Shift key makes numpad keys behave as if NumLock + // wasn't active. The way this is exposed to applications by the system is that + // the application receives a fake key release event for the shift key at the + // moment when the numpad key is pressed, just before receiving the numpad key + // as well. + // + // The issue is that in the raw device event (here), the fake shift release + // event reports the numpad key as the scancode. Unfortunately, the event doesn't + // have any information to tell whether it's the left shift or the right shift + // that needs to get the fake release (or press) event so we don't forward this + // event to the application at all. + // + // For more on this, read the article by Raymond Chen, titled: + // "The shift key overrides NumLock" + // https://devblogs.microsoft.com/oldnewthing/20040906-00/?p=37953 + return; + } + _ => (), + } + } + subclass_input.send_event(Event::DeviceEvent { + device_id, + event: Key(RawKeyEvent { + physical_key: code, + state, + }), + }); + } +} diff --git a/src/platform_impl/windows/global_shortcut.rs b/src/platform_impl/windows/global_shortcut.rs new file mode 100644 index 000000000..4a34da18c --- /dev/null +++ b/src/platform_impl/windows/global_shortcut.rs @@ -0,0 +1,100 @@ +use super::keyboard::key_to_vk; +use crate::{ + accelerator::{Accelerator, AcceleratorId}, + event_loop::EventLoopWindowTarget, + keyboard::ModifiersState, + platform::global_shortcut::{GlobalShortcut as RootGlobalShortcut, ShortcutManagerError}, +}; +use std::ptr; +use winapi::{shared::windef::HWND, um::winuser}; + +#[derive(Debug, Clone)] +pub struct ShortcutManager { + shortcuts: Vec, +} + +impl ShortcutManager { + pub(crate) fn new(_window_target: &EventLoopWindowTarget) -> Self { + ShortcutManager { + shortcuts: Vec::new(), + } + } + + pub(crate) fn register( + &mut self, + accelerator: Accelerator, + ) -> Result { + unsafe { + let mut converted_modifiers: u32 = 0; + let modifiers: ModifiersState = accelerator.mods.into(); + if modifiers.shift_key() { + converted_modifiers |= winuser::MOD_SHIFT as u32; + } + if modifiers.super_key() { + converted_modifiers |= winuser::MOD_WIN as u32; + } + if modifiers.alt_key() { + converted_modifiers |= winuser::MOD_ALT as u32; + } + if modifiers.control_key() { + converted_modifiers |= winuser::MOD_CONTROL as u32; + } + + // get key scan code + match key_to_vk(&accelerator.key) { + Some(vk_code) => { + let result = winuser::RegisterHotKey( + ptr::null_mut(), + accelerator.clone().id().0 as i32, + converted_modifiers, + vk_code as u32, + ); + if result == 0 { + return Err(ShortcutManagerError::InvalidAccelerator( + "Unable to register accelerator with `winuser::RegisterHotKey`.".into(), + )); + } + let shortcut = GlobalShortcut { accelerator }; + self.shortcuts.push(shortcut.clone()); + return Ok(RootGlobalShortcut(shortcut)); + } + _ => { + return Err(ShortcutManagerError::InvalidAccelerator( + "Unable to register accelerator (unknown VKCode for this char).".into(), + )); + } + } + } + } + + pub(crate) fn unregister_all(&self) -> Result<(), ShortcutManagerError> { + for shortcut in &self.shortcuts { + shortcut.unregister(); + } + Ok(()) + } + + pub(crate) fn unregister( + &self, + shortcut: RootGlobalShortcut, + ) -> Result<(), ShortcutManagerError> { + shortcut.0.unregister(); + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct GlobalShortcut { + pub(crate) accelerator: Accelerator, +} + +impl GlobalShortcut { + pub fn id(&self) -> AcceleratorId { + self.accelerator.clone().id() + } + pub(crate) fn unregister(&self) { + unsafe { + winuser::UnregisterHotKey(0 as HWND, self.accelerator.clone().id().0 as i32); + } + } +} diff --git a/src/platform_impl/windows/keyboard.rs b/src/platform_impl/windows/keyboard.rs new file mode 100644 index 000000000..7b508afa1 --- /dev/null +++ b/src/platform_impl/windows/keyboard.rs @@ -0,0 +1,890 @@ +use std::{ + char, + collections::HashSet, + ffi::OsString, + mem::MaybeUninit, + os::{raw::c_int, windows::ffi::OsStringExt}, + sync::MutexGuard, +}; + +use winapi::{ + shared::{ + minwindef::{HKL, LPARAM, UINT, WPARAM}, + windef::HWND, + }, + um::winuser, +}; + +use unicode_segmentation::UnicodeSegmentation; + +use crate::{ + event::{ElementState, KeyEvent}, + keyboard::{Key, KeyCode, KeyLocation, NativeKeyCode}, + platform_impl::platform::{ + event_loop::ProcResult, + keyboard_layout::{get_or_insert_str, Layout, LayoutCache, WindowsModifiers, LAYOUT_CACHE}, + KeyEventExtra, + }, +}; + +pub fn is_msg_keyboard_related(msg: u32) -> bool { + use winuser::{WM_KEYFIRST, WM_KEYLAST, WM_KILLFOCUS, WM_SETFOCUS}; + let is_keyboard_msg = WM_KEYFIRST <= msg && msg <= WM_KEYLAST; + + is_keyboard_msg || msg == WM_SETFOCUS || msg == WM_KILLFOCUS +} + +pub type ExScancode = u16; + +pub struct MessageAsKeyEvent { + pub event: KeyEvent, + pub is_synthetic: bool, +} + +/// Stores information required to make `KeyEvent`s. +/// +/// A single Tao `KeyEvent` contains information which the Windows API passes to the application +/// in multiple window messages. In other words: a Tao `KeyEvent` cannot be built from a single +/// window message. Therefore, this type keeps track of certain information from previous events so +/// that a `KeyEvent` can be constructed when the last event related to a keypress is received. +/// +/// `PeekMessage` is sometimes used to determine whether the next window message still belongs to the +/// current keypress. If it doesn't and the current state represents a key event waiting to be +/// dispatched, then said event is considered complete and is dispatched. +/// +/// The sequence of window messages for a key press event is the following: +/// - Exactly one WM_KEYDOWN / WM_SYSKEYDOWN +/// - Zero or one WM_DEADCHAR / WM_SYSDEADCHAR +/// - Zero or more WM_CHAR / WM_SYSCHAR. These messages each come with a UTF-16 code unit which when +/// put together in the sequence they arrived in, forms the text which is the result of pressing the +/// key. +/// +/// Key release messages are a bit different due to the fact that they don't contribute to +/// text input. The "sequence" only consists of one WM_KEYUP / WM_SYSKEYUP event. +pub struct KeyEventBuilder { + event_info: Option, +} +impl Default for KeyEventBuilder { + fn default() -> Self { + KeyEventBuilder { event_info: None } + } +} +impl KeyEventBuilder { + /// Call this function for every window message. + /// Returns Some() if this window message completes a KeyEvent. + /// Returns None otherwise. + pub(crate) fn process_message( + &mut self, + hwnd: HWND, + msg_kind: u32, + wparam: WPARAM, + lparam: LPARAM, + result: &mut ProcResult, + ) -> Vec { + match msg_kind { + winuser::WM_SETFOCUS => { + // synthesize keydown events + let kbd_state = get_async_kbd_state(); + let key_events = self.synthesize_kbd_state(ElementState::Pressed, &kbd_state); + if !key_events.is_empty() { + return key_events; + } + } + winuser::WM_KILLFOCUS => { + // sythesize keyup events + let kbd_state = get_kbd_state(); + let key_events = self.synthesize_kbd_state(ElementState::Released, &kbd_state); + if !key_events.is_empty() { + return key_events; + } + } + winuser::WM_KEYDOWN | winuser::WM_SYSKEYDOWN => { + if msg_kind == winuser::WM_SYSKEYDOWN && wparam as i32 == winuser::VK_F4 { + // Don't dispatch Alt+F4 to the application. + // This is handled in `event_loop.rs` + return vec![]; + } + *result = ProcResult::Value(0); + + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let event_info = + PartialKeyEventInfo::from_message(wparam, lparam, ElementState::Pressed, &mut layouts); + + let mut next_msg = MaybeUninit::uninit(); + let peek_retval = unsafe { + winuser::PeekMessageW( + next_msg.as_mut_ptr(), + hwnd, + winuser::WM_KEYFIRST, + winuser::WM_KEYLAST, + winuser::PM_NOREMOVE, + ) + }; + let has_next_key_message = peek_retval != 0; + self.event_info = None; + let mut finished_event_info = Some(event_info); + if has_next_key_message { + let next_msg = unsafe { next_msg.assume_init() }; + let next_msg_kind = next_msg.message; + let next_belongs_to_this = !matches!( + next_msg_kind, + winuser::WM_KEYDOWN | winuser::WM_SYSKEYDOWN | winuser::WM_KEYUP | winuser::WM_SYSKEYUP + ); + if next_belongs_to_this { + self.event_info = finished_event_info.take(); + } else { + let (_, layout) = layouts.get_current_layout(); + let is_fake = { + let curr_event = finished_event_info.as_ref().unwrap(); + is_current_fake(curr_event, next_msg, layout) + }; + if is_fake { + finished_event_info = None; + } + } + } + if let Some(event_info) = finished_event_info { + let ev = event_info.finalize(&mut layouts.strings); + return vec![MessageAsKeyEvent { + event: ev, + is_synthetic: false, + }]; + } + } + winuser::WM_DEADCHAR | winuser::WM_SYSDEADCHAR => { + *result = ProcResult::Value(0); + // At this point, we know that there isn't going to be any more events related to + // this key press + let event_info = self.event_info.take().unwrap(); + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let ev = event_info.finalize(&mut layouts.strings); + return vec![MessageAsKeyEvent { + event: ev, + is_synthetic: false, + }]; + } + winuser::WM_CHAR | winuser::WM_SYSCHAR => { + if self.event_info.is_none() { + trace!("Received a CHAR message but no `event_info` was available. The message is probably IME, returning."); + return vec![]; + } + *result = ProcResult::Value(0); + let is_high_surrogate = 0xD800 <= wparam && wparam <= 0xDBFF; + let is_low_surrogate = 0xDC00 <= wparam && wparam <= 0xDFFF; + + let is_utf16 = is_high_surrogate || is_low_surrogate; + + let more_char_coming; + unsafe { + let mut next_msg = MaybeUninit::uninit(); + let has_message = winuser::PeekMessageW( + next_msg.as_mut_ptr(), + hwnd, + winuser::WM_KEYFIRST, + winuser::WM_KEYLAST, + winuser::PM_NOREMOVE, + ); + let has_message = has_message != 0; + if !has_message { + more_char_coming = false; + } else { + let next_msg = next_msg.assume_init().message; + if next_msg == winuser::WM_CHAR || next_msg == winuser::WM_SYSCHAR { + more_char_coming = true; + } else { + more_char_coming = false; + } + } + } + + if is_utf16 { + if let Some(ev_info) = self.event_info.as_mut() { + ev_info.utf16parts.push(wparam as u16); + } + } else { + // In this case, wparam holds a UTF-32 character. + // Let's encode it as UTF-16 and append it to the end of `utf16parts` + let utf16parts = match self.event_info.as_mut() { + Some(ev_info) => &mut ev_info.utf16parts, + None => { + warn!("The event_info was None when it was expected to be some"); + return vec![]; + } + }; + let start_offset = utf16parts.len(); + let new_size = utf16parts.len() + 2; + utf16parts.resize(new_size, 0); + if let Some(ch) = char::from_u32(wparam as u32) { + let encode_len = ch.encode_utf16(&mut utf16parts[start_offset..]).len(); + let new_size = start_offset + encode_len; + utf16parts.resize(new_size, 0); + } + } + if !more_char_coming { + let mut event_info = match self.event_info.take() { + Some(ev_info) => ev_info, + None => { + warn!("The event_info was None when it was expected to be some"); + return vec![]; + } + }; + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + // It's okay to call `ToUnicode` here, because at this point the dead key + // is already consumed by the character. + let kbd_state = get_kbd_state(); + let mod_state = WindowsModifiers::active_modifiers(&kbd_state); + + let (_, layout) = layouts.get_current_layout(); + let ctrl_on; + if layout.has_alt_graph { + let alt_on = mod_state.contains(WindowsModifiers::ALT); + ctrl_on = !alt_on && mod_state.contains(WindowsModifiers::CONTROL) + } else { + ctrl_on = mod_state.contains(WindowsModifiers::CONTROL) + } + + // If Ctrl is not pressed, just use the text with all + // modifiers because that already consumed the dead key. Otherwise, + // we would interpret the character incorrectly, missing the dead key. + if !ctrl_on { + event_info.text = PartialText::System(event_info.utf16parts.clone()); + } else { + let mod_no_ctrl = mod_state.remove_only_ctrl(); + let num_lock_on = kbd_state[winuser::VK_NUMLOCK as usize] & 1 != 0; + let vkey = event_info.vkey; + let scancode = event_info.scancode; + let keycode = event_info.code; + let key = layout.get_key(mod_no_ctrl, num_lock_on, vkey, scancode, keycode); + event_info.text = PartialText::Text(key.to_text()); + } + let ev = event_info.finalize(&mut layouts.strings); + return vec![MessageAsKeyEvent { + event: ev, + is_synthetic: false, + }]; + } + } + winuser::WM_KEYUP | winuser::WM_SYSKEYUP => { + *result = ProcResult::Value(0); + + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let event_info = + PartialKeyEventInfo::from_message(wparam, lparam, ElementState::Released, &mut layouts); + let mut next_msg = MaybeUninit::uninit(); + let peek_retval = unsafe { + winuser::PeekMessageW( + next_msg.as_mut_ptr(), + hwnd, + winuser::WM_KEYFIRST, + winuser::WM_KEYLAST, + winuser::PM_NOREMOVE, + ) + }; + let has_next_key_message = peek_retval != 0; + let mut valid_event_info = Some(event_info); + if has_next_key_message { + let next_msg = unsafe { next_msg.assume_init() }; + let (_, layout) = layouts.get_current_layout(); + let is_fake = { + let event_info = valid_event_info.as_ref().unwrap(); + is_current_fake(&event_info, next_msg, layout) + }; + if is_fake { + valid_event_info = None; + } + } + if let Some(event_info) = valid_event_info { + let event = event_info.finalize(&mut layouts.strings); + return vec![MessageAsKeyEvent { + event, + is_synthetic: false, + }]; + } + } + _ => (), + } + + Vec::new() + } + + fn synthesize_kbd_state( + &mut self, + key_state: ElementState, + kbd_state: &[u8; 256], + ) -> Vec { + let mut key_events = Vec::new(); + + let mut layouts = LAYOUT_CACHE.lock().unwrap(); + let (locale_id, _) = layouts.get_current_layout(); + + macro_rules! is_key_pressed { + ($vk:expr) => { + kbd_state[$vk as usize] & 0x80 != 0 + }; + } + + // Is caps-lock active? Note that this is different from caps-lock + // being held down. + let caps_lock_on = kbd_state[winuser::VK_CAPITAL as usize] & 1 != 0; + let num_lock_on = kbd_state[winuser::VK_NUMLOCK as usize] & 1 != 0; + + // We are synthesizing the press event for caps-lock first for the following reasons: + // 1. If caps-lock is *not* held down but *is* active, then we have to + // synthesize all printable keys, respecting the caps-lock state. + // 2. If caps-lock is held down, we could choose to sythesize its + // keypress after every other key, in which case all other keys *must* + // be sythesized as if the caps-lock state was be the opposite + // of what it currently is. + // -- + // For the sake of simplicity we are choosing to always sythesize + // caps-lock first, and always use the current caps-lock state + // to determine the produced text + if is_key_pressed!(winuser::VK_CAPITAL) { + let event = self.create_synthetic( + winuser::VK_CAPITAL, + key_state, + caps_lock_on, + num_lock_on, + locale_id as HKL, + &mut layouts, + ); + if let Some(event) = event { + key_events.push(event); + } + } + let do_non_modifier = |key_events: &mut Vec<_>, layouts: &mut _| { + for vk in 0..256 { + match vk { + winuser::VK_CONTROL + | winuser::VK_LCONTROL + | winuser::VK_RCONTROL + | winuser::VK_SHIFT + | winuser::VK_LSHIFT + | winuser::VK_RSHIFT + | winuser::VK_MENU + | winuser::VK_LMENU + | winuser::VK_RMENU + | winuser::VK_CAPITAL => continue, + _ => (), + } + if !is_key_pressed!(vk) { + continue; + } + let event = self.create_synthetic( + vk, + key_state, + caps_lock_on, + num_lock_on, + locale_id as HKL, + layouts, + ); + if let Some(event) = event { + key_events.push(event); + } + } + }; + let do_modifier = |key_events: &mut Vec<_>, layouts: &mut _| { + const CLEAR_MODIFIER_VKS: [i32; 6] = [ + winuser::VK_LCONTROL, + winuser::VK_LSHIFT, + winuser::VK_LMENU, + winuser::VK_RCONTROL, + winuser::VK_RSHIFT, + winuser::VK_RMENU, + ]; + for vk in CLEAR_MODIFIER_VKS.iter() { + if is_key_pressed!(*vk) { + let event = self.create_synthetic( + *vk, + key_state, + caps_lock_on, + num_lock_on, + locale_id as HKL, + layouts, + ); + if let Some(event) = event { + key_events.push(event); + } + } + } + }; + + // Be cheeky and sequence modifier and non-modifier + // key events such that non-modifier keys are not affected + // by modifiers (except for caps-lock) + match key_state { + ElementState::Pressed => { + do_non_modifier(&mut key_events, &mut layouts); + do_modifier(&mut key_events, &mut layouts); + } + ElementState::Released => { + do_modifier(&mut key_events, &mut layouts); + do_non_modifier(&mut key_events, &mut layouts); + } + } + + key_events + } + + fn create_synthetic( + &self, + vk: i32, + key_state: ElementState, + caps_lock_on: bool, + num_lock_on: bool, + locale_id: HKL, + layouts: &mut MutexGuard<'_, LayoutCache>, + ) -> Option { + let scancode = + unsafe { winuser::MapVirtualKeyExW(vk as UINT, winuser::MAPVK_VK_TO_VSC_EX, locale_id) }; + if scancode == 0 { + return None; + } + let scancode = scancode as ExScancode; + let code = KeyCode::from_scancode(scancode as u32); + let mods = if caps_lock_on { + WindowsModifiers::CAPS_LOCK + } else { + WindowsModifiers::empty() + }; + let layout = layouts.layouts.get(&(locale_id as u64)).unwrap(); + let logical_key = layout.get_key(mods, num_lock_on, vk, scancode, code); + let key_without_modifiers = + layout.get_key(WindowsModifiers::empty(), false, vk, scancode, code); + let text; + if key_state == ElementState::Pressed { + text = logical_key.to_text(); + } else { + text = None; + } + let event_info = PartialKeyEventInfo { + vkey: vk, + logical_key: PartialLogicalKey::This(logical_key.clone()), + key_without_modifiers, + key_state, + scancode, + is_repeat: false, + code, + location: get_location(scancode, locale_id), + utf16parts: Vec::with_capacity(8), + text: PartialText::Text(text.clone()), + }; + + let mut event = event_info.finalize(&mut layouts.strings); + event.logical_key = logical_key; + event.platform_specific.text_with_all_modifiers = text; + Some(MessageAsKeyEvent { + event, + is_synthetic: true, + }) + } +} + +enum PartialText { + // Unicode + System(Vec), + Text(Option<&'static str>), +} + +enum PartialLogicalKey { + /// Use the text provided by the WM_CHAR messages and report that as a `Character` variant. If + /// the text consists of multiple grapheme clusters (user-precieved characters) that means that + /// dead key could not be combined with the second input, and in that case we should fall back + /// to using what would have without a dead-key input. + TextOr(Key<'static>), + + /// Use the value directly provided by this variant + This(Key<'static>), +} + +struct PartialKeyEventInfo { + vkey: c_int, + scancode: ExScancode, + key_state: ElementState, + is_repeat: bool, + code: KeyCode, + location: KeyLocation, + logical_key: PartialLogicalKey, + + key_without_modifiers: Key<'static>, + + /// 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, + + text: PartialText, +} + +impl PartialKeyEventInfo { + fn from_message( + wparam: WPARAM, + lparam: LPARAM, + state: ElementState, + layouts: &mut MutexGuard<'_, LayoutCache>, + ) -> Self { + const NO_MODS: WindowsModifiers = WindowsModifiers::empty(); + + let (_, layout) = layouts.get_current_layout(); + let lparam_struct = destructure_key_lparam(lparam); + let scancode; + let vkey = wparam as c_int; + if lparam_struct.scancode == 0 { + // In some cases (often with media keys) the device reports a scancode of 0 but a + // valid virtual key. In these cases we obtain the scancode from the virtual key. + scancode = unsafe { + winuser::MapVirtualKeyExW(vkey as u32, winuser::MAPVK_VK_TO_VSC_EX, layout.hkl as HKL) + as u16 + }; + } else { + scancode = new_ex_scancode(lparam_struct.scancode, lparam_struct.extended); + } + let code = KeyCode::from_scancode(scancode as u32); + let location = get_location(scancode, layout.hkl as HKL); + + let kbd_state = get_kbd_state(); + let mods = WindowsModifiers::active_modifiers(&kbd_state); + let mods_without_ctrl = mods.remove_only_ctrl(); + let num_lock_on = kbd_state[winuser::VK_NUMLOCK as usize] & 1 != 0; + + // On Windows Ctrl+NumLock = Pause (and apparently Ctrl+Pause -> NumLock). In these cases + // the KeyCode still stores the real key, so in the name of consistency across platforms, we + // circumvent this mapping and force the key values to match the keycode. + // For more on this, read the article by Raymond Chen, titled: + // "Why does Ctrl+ScrollLock cancel dialogs?" + // https://devblogs.microsoft.com/oldnewthing/20080211-00/?p=23503 + let code_as_key = if mods.contains(WindowsModifiers::CONTROL) { + match code { + KeyCode::NumLock => Some(Key::NumLock), + KeyCode::Pause => Some(Key::Pause), + _ => None, + } + } else { + None + }; + + let preliminary_logical_key = + layout.get_key(mods_without_ctrl, num_lock_on, vkey, scancode, code); + let key_is_char = matches!(preliminary_logical_key, Key::Character(_)); + let is_pressed = state == ElementState::Pressed; + + let logical_key = if let Some(key) = code_as_key.clone() { + PartialLogicalKey::This(key) + } else if is_pressed && key_is_char && !mods.contains(WindowsModifiers::CONTROL) { + // In some cases we want to use the UNICHAR text for logical_key in order to allow + // dead keys to have an effect on the character reported by `logical_key`. + PartialLogicalKey::TextOr(preliminary_logical_key) + } else { + PartialLogicalKey::This(preliminary_logical_key) + }; + let key_without_modifiers = if let Some(key) = code_as_key { + key + } else { + match layout.get_key(NO_MODS, false, vkey, scancode, code) { + // We convert dead keys into their character. + // The reason for this is that `key_without_modifiers` is designed for key-bindings, + // but the US International layout treats `'` (apostrophe) as a dead key and the + // reguar US layout treats it a character. In order for a single binding + // configuration to work with both layouts, we forward each dead key as a character. + Key::Dead(k) => { + if let Some(ch) = k { + // I'm avoiding the heap allocation. I don't want to talk about it :( + let mut utf8 = [0; 4]; + let s = ch.encode_utf8(&mut utf8); + let static_str = get_or_insert_str(&mut layouts.strings, s); + Key::Character(static_str) + } else { + Key::Unidentified(NativeKeyCode::Unidentified) + } + } + key => key, + } + }; + + PartialKeyEventInfo { + vkey, + scancode, + key_state: state, + logical_key, + key_without_modifiers, + is_repeat: lparam_struct.is_repeat, + code, + location, + utf16parts: Vec::with_capacity(8), + text: PartialText::System(Vec::new()), + } + } + + fn finalize(self, strings: &mut HashSet<&'static str>) -> KeyEvent { + let mut char_with_all_modifiers = None; + if !self.utf16parts.is_empty() { + let os_string = OsString::from_wide(&self.utf16parts); + if let Ok(string) = os_string.into_string() { + let static_str = get_or_insert_str(strings, string); + char_with_all_modifiers = Some(static_str); + } + } + + // The text without Ctrl + let mut text = None; + match self.text { + PartialText::System(wide) => { + if !wide.is_empty() { + let os_string = OsString::from_wide(&wide); + if let Ok(string) = os_string.into_string() { + let static_str = get_or_insert_str(strings, string); + text = Some(static_str); + } + } + } + PartialText::Text(s) => { + text = s; + } + } + + let logical_key = match self.logical_key { + PartialLogicalKey::TextOr(fallback) => match text.clone() { + Some(s) => { + if s.grapheme_indices(true).count() > 1 { + fallback + } else { + Key::Character(s) + } + } + None => Key::Unidentified(NativeKeyCode::Windows(self.scancode)), + }, + PartialLogicalKey::This(v) => v, + }; + + KeyEvent { + physical_key: self.code, + logical_key, + text, + location: self.location, + state: self.key_state, + repeat: self.is_repeat, + platform_specific: KeyEventExtra { + text_with_all_modifiers: char_with_all_modifiers, + key_without_modifiers: self.key_without_modifiers, + }, + } + } +} + +#[derive(Debug, Copy, Clone)] +struct KeyLParam { + pub scancode: u8, + pub extended: bool, + + /// This is `previous_state XOR transition_state`. See the lParam for WM_KEYDOWN and WM_KEYUP for further details. + pub is_repeat: bool, +} + +fn destructure_key_lparam(lparam: LPARAM) -> KeyLParam { + let previous_state = (lparam >> 30) & 0x01; + let transition_state = (lparam >> 31) & 0x01; + KeyLParam { + scancode: ((lparam >> 16) & 0xFF) as u8, + extended: ((lparam >> 24) & 0x01) != 0, + is_repeat: (previous_state ^ transition_state) != 0, + } +} + +#[inline] +fn new_ex_scancode(scancode: u8, extended: bool) -> ExScancode { + (scancode as u16) | (if extended { 0xE000 } else { 0 }) +} + +#[inline] +fn ex_scancode_from_lparam(lparam: LPARAM) -> ExScancode { + let lparam = destructure_key_lparam(lparam); + new_ex_scancode(lparam.scancode, lparam.extended) +} + +/// Gets the keyboard state as reported by messages that have been removed from the event queue. +/// See also: get_async_kbd_state +fn get_kbd_state() -> [u8; 256] { + unsafe { + let mut kbd_state: MaybeUninit<[u8; 256]> = MaybeUninit::uninit(); + winuser::GetKeyboardState(kbd_state.as_mut_ptr() as *mut u8); + kbd_state.assume_init() + } +} + +/// Gets the current keyboard state regardless of whether the corresponding keyboard events have +/// been removed from the event queue. See also: get_kbd_state +fn get_async_kbd_state() -> [u8; 256] { + unsafe { + let mut kbd_state: [u8; 256] = MaybeUninit::uninit().assume_init(); + for (vk, state) in kbd_state.iter_mut().enumerate() { + let vk = vk as c_int; + let async_state = winuser::GetAsyncKeyState(vk as c_int); + let is_down = (async_state & (1 << 15)) != 0; + *state = if is_down { 0x80 } else { 0 }; + + if matches!( + vk, + winuser::VK_CAPITAL | winuser::VK_NUMLOCK | winuser::VK_SCROLL + ) { + // Toggle states aren't reported by `GetAsyncKeyState` + let toggle_state = winuser::GetKeyState(vk); + let is_active = (toggle_state & 1) != 0; + *state |= if is_active { 1 } else { 0 }; + } + } + kbd_state + } +} + +/// On windows, AltGr == Ctrl + Alt +/// +/// Due to this equivalence, the system generates a fake Ctrl key-press (and key-release) preceeding +/// every AltGr key-press (and key-release). We check if the current event is a Ctrl event and if +/// the next event is a right Alt (AltGr) event. If this is the case, the current event must be the +/// fake Ctrl event. +fn is_current_fake( + curr_info: &PartialKeyEventInfo, + next_msg: winuser::MSG, + layout: &Layout, +) -> bool { + let curr_is_ctrl = matches!(curr_info.logical_key, PartialLogicalKey::This(Key::Control)); + if layout.has_alt_graph { + let next_code = ex_scancode_from_lparam(next_msg.lParam); + let next_is_altgr = next_code == 0xE038; // 0xE038 is right alt + if curr_is_ctrl && next_is_altgr { + return true; + } + } + false +} + +fn get_location(scancode: ExScancode, hkl: HKL) -> KeyLocation { + use winuser::*; + const VK_ABNT_C2: c_int = 0xc2; + + let extension = 0xE000; + let extended = (scancode & extension) == extension; + let vkey = + unsafe { winuser::MapVirtualKeyExW(scancode as u32, winuser::MAPVK_VSC_TO_VK_EX, hkl) as i32 }; + + // Use the native VKEY and the extended flag to cover most cases + // This is taken from the `druid` GUI library, specifically + // druid-shell/src/platform/windows/keyboard.rs + match vkey { + VK_LSHIFT | VK_LCONTROL | VK_LMENU | VK_LWIN => KeyLocation::Left, + VK_RSHIFT | VK_RCONTROL | VK_RMENU | VK_RWIN => KeyLocation::Right, + VK_RETURN if extended => KeyLocation::Numpad, + VK_INSERT | VK_DELETE | VK_END | VK_DOWN | VK_NEXT | VK_LEFT | VK_CLEAR | VK_RIGHT + | VK_HOME | VK_UP | VK_PRIOR => { + if extended { + KeyLocation::Standard + } else { + KeyLocation::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 => KeyLocation::Numpad, + _ => KeyLocation::Standard, + } +} + +// used to build accelerators table from Key +pub(crate) fn key_to_vk(key: &KeyCode) -> Option { + Some(match key { + KeyCode::KeyA => unsafe { winuser::VkKeyScanW('a' as u16) as i32 }, + KeyCode::KeyB => unsafe { winuser::VkKeyScanW('b' as u16) as i32 }, + KeyCode::KeyC => unsafe { winuser::VkKeyScanW('c' as u16) as i32 }, + KeyCode::KeyD => unsafe { winuser::VkKeyScanW('d' as u16) as i32 }, + KeyCode::KeyE => unsafe { winuser::VkKeyScanW('e' as u16) as i32 }, + KeyCode::KeyF => unsafe { winuser::VkKeyScanW('f' as u16) as i32 }, + KeyCode::KeyG => unsafe { winuser::VkKeyScanW('g' as u16) as i32 }, + KeyCode::KeyH => unsafe { winuser::VkKeyScanW('h' as u16) as i32 }, + KeyCode::KeyI => unsafe { winuser::VkKeyScanW('i' as u16) as i32 }, + KeyCode::KeyJ => unsafe { winuser::VkKeyScanW('j' as u16) as i32 }, + KeyCode::KeyK => unsafe { winuser::VkKeyScanW('k' as u16) as i32 }, + KeyCode::KeyL => unsafe { winuser::VkKeyScanW('l' as u16) as i32 }, + KeyCode::KeyM => unsafe { winuser::VkKeyScanW('m' as u16) as i32 }, + KeyCode::KeyN => unsafe { winuser::VkKeyScanW('n' as u16) as i32 }, + KeyCode::KeyO => unsafe { winuser::VkKeyScanW('o' as u16) as i32 }, + KeyCode::KeyP => unsafe { winuser::VkKeyScanW('p' as u16) as i32 }, + KeyCode::KeyQ => unsafe { winuser::VkKeyScanW('q' as u16) as i32 }, + KeyCode::KeyR => unsafe { winuser::VkKeyScanW('r' as u16) as i32 }, + KeyCode::KeyS => unsafe { winuser::VkKeyScanW('s' as u16) as i32 }, + KeyCode::KeyT => unsafe { winuser::VkKeyScanW('t' as u16) as i32 }, + KeyCode::KeyU => unsafe { winuser::VkKeyScanW('u' as u16) as i32 }, + KeyCode::KeyV => unsafe { winuser::VkKeyScanW('v' as u16) as i32 }, + KeyCode::KeyW => unsafe { winuser::VkKeyScanW('w' as u16) as i32 }, + KeyCode::KeyX => unsafe { winuser::VkKeyScanW('x' as u16) as i32 }, + KeyCode::KeyY => unsafe { winuser::VkKeyScanW('y' as u16) as i32 }, + KeyCode::KeyZ => unsafe { winuser::VkKeyScanW('z' as u16) as i32 }, + KeyCode::Digit0 => unsafe { winuser::VkKeyScanW('0' as u16) as i32 }, + KeyCode::Digit1 => unsafe { winuser::VkKeyScanW('1' as u16) as i32 }, + KeyCode::Digit2 => unsafe { winuser::VkKeyScanW('2' as u16) as i32 }, + KeyCode::Digit3 => unsafe { winuser::VkKeyScanW('3' as u16) as i32 }, + KeyCode::Digit4 => unsafe { winuser::VkKeyScanW('4' as u16) as i32 }, + KeyCode::Digit5 => unsafe { winuser::VkKeyScanW('5' as u16) as i32 }, + KeyCode::Digit6 => unsafe { winuser::VkKeyScanW('6' as u16) as i32 }, + KeyCode::Digit7 => unsafe { winuser::VkKeyScanW('7' as u16) as i32 }, + KeyCode::Digit8 => unsafe { winuser::VkKeyScanW('8' as u16) as i32 }, + KeyCode::Digit9 => unsafe { winuser::VkKeyScanW('9' as u16) as i32 }, + KeyCode::Backspace => winuser::VK_BACK, + KeyCode::Tab => winuser::VK_TAB, + KeyCode::Enter => winuser::VK_RETURN, + KeyCode::Pause => winuser::VK_PAUSE, + KeyCode::CapsLock => winuser::VK_CAPITAL, + KeyCode::KanaMode => winuser::VK_KANA, + KeyCode::Escape => winuser::VK_ESCAPE, + KeyCode::NonConvert => winuser::VK_NONCONVERT, + KeyCode::PageUp => winuser::VK_PRIOR, + KeyCode::PageDown => winuser::VK_NEXT, + KeyCode::End => winuser::VK_END, + KeyCode::Home => winuser::VK_HOME, + KeyCode::ArrowLeft => winuser::VK_LEFT, + KeyCode::ArrowUp => winuser::VK_UP, + KeyCode::ArrowRight => winuser::VK_RIGHT, + KeyCode::ArrowDown => winuser::VK_DOWN, + KeyCode::PrintScreen => winuser::VK_SNAPSHOT, + KeyCode::Insert => winuser::VK_INSERT, + KeyCode::Delete => winuser::VK_DELETE, + KeyCode::Help => winuser::VK_HELP, + KeyCode::ContextMenu => winuser::VK_APPS, + KeyCode::F1 => winuser::VK_F1, + KeyCode::F2 => winuser::VK_F2, + KeyCode::F3 => winuser::VK_F3, + KeyCode::F4 => winuser::VK_F4, + KeyCode::F5 => winuser::VK_F5, + KeyCode::F6 => winuser::VK_F6, + KeyCode::F7 => winuser::VK_F7, + KeyCode::F8 => winuser::VK_F8, + KeyCode::F9 => winuser::VK_F9, + KeyCode::F10 => winuser::VK_F10, + KeyCode::F11 => winuser::VK_F11, + KeyCode::F12 => winuser::VK_F12, + KeyCode::F13 => winuser::VK_F13, + KeyCode::F14 => winuser::VK_F14, + KeyCode::F15 => winuser::VK_F15, + KeyCode::F16 => winuser::VK_F16, + KeyCode::F17 => winuser::VK_F17, + KeyCode::F18 => winuser::VK_F18, + KeyCode::F19 => winuser::VK_F19, + KeyCode::NumLock => winuser::VK_NUMLOCK, + KeyCode::ScrollLock => winuser::VK_SCROLL, + KeyCode::BrowserBack => winuser::VK_BROWSER_BACK, + KeyCode::BrowserForward => winuser::VK_BROWSER_FORWARD, + KeyCode::BrowserRefresh => winuser::VK_BROWSER_REFRESH, + KeyCode::BrowserStop => winuser::VK_BROWSER_STOP, + KeyCode::BrowserSearch => winuser::VK_BROWSER_SEARCH, + KeyCode::BrowserFavorites => winuser::VK_BROWSER_FAVORITES, + KeyCode::BrowserHome => winuser::VK_BROWSER_HOME, + KeyCode::AudioVolumeMute => winuser::VK_VOLUME_MUTE, + KeyCode::AudioVolumeDown => winuser::VK_VOLUME_DOWN, + KeyCode::AudioVolumeUp => winuser::VK_VOLUME_UP, + KeyCode::MediaTrackNext => winuser::VK_MEDIA_NEXT_TRACK, + KeyCode::MediaTrackPrevious => winuser::VK_MEDIA_PREV_TRACK, + KeyCode::MediaStop => winuser::VK_MEDIA_STOP, + KeyCode::MediaPlayPause => winuser::VK_MEDIA_PLAY_PAUSE, + KeyCode::LaunchMail => winuser::VK_LAUNCH_MAIL, + KeyCode::Convert => winuser::VK_CONVERT, + _ => return None, + }) +} diff --git a/src/platform_impl/windows/keyboard_layout.rs b/src/platform_impl/windows/keyboard_layout.rs new file mode 100644 index 000000000..b4f15cfb0 --- /dev/null +++ b/src/platform_impl/windows/keyboard_layout.rs @@ -0,0 +1,985 @@ +use std::{ + collections::{hash_map::Entry, HashMap, HashSet}, + ffi::OsString, + os::windows::ffi::OsStringExt, + sync::Mutex, +}; + +use lazy_static::lazy_static; + +use winapi::{ + ctypes::c_int, + shared::minwindef::{HKL, LOWORD}, + um::{ + winnt::{LANG_JAPANESE, LANG_KOREAN, PRIMARYLANGID}, + winuser, + }, +}; + +use super::keyboard::ExScancode; +use crate::keyboard::{Key, KeyCode, ModifiersState, NativeKeyCode}; + +lazy_static! { + pub(crate) static ref LAYOUT_CACHE: Mutex = Mutex::new(LayoutCache::default()); +} + +fn key_pressed(vkey: c_int) -> bool { + unsafe { (winuser::GetKeyState(vkey) & (1 << 15)) == (1 << 15) } +} + +const NUMPAD_VKEYS: [c_int; 16] = [ + winuser::VK_NUMPAD0, + winuser::VK_NUMPAD1, + winuser::VK_NUMPAD2, + winuser::VK_NUMPAD3, + winuser::VK_NUMPAD4, + winuser::VK_NUMPAD5, + winuser::VK_NUMPAD6, + winuser::VK_NUMPAD7, + winuser::VK_NUMPAD8, + winuser::VK_NUMPAD9, + winuser::VK_MULTIPLY, + winuser::VK_ADD, + winuser::VK_SEPARATOR, + winuser::VK_SUBTRACT, + winuser::VK_DECIMAL, + winuser::VK_DIVIDE, +]; + +lazy_static! { + static ref NUMPAD_KEYCODES: HashSet = { + let mut keycodes = HashSet::new(); + keycodes.insert(KeyCode::Numpad0); + keycodes.insert(KeyCode::Numpad1); + keycodes.insert(KeyCode::Numpad2); + keycodes.insert(KeyCode::Numpad3); + keycodes.insert(KeyCode::Numpad4); + keycodes.insert(KeyCode::Numpad5); + keycodes.insert(KeyCode::Numpad6); + keycodes.insert(KeyCode::Numpad7); + keycodes.insert(KeyCode::Numpad8); + keycodes.insert(KeyCode::Numpad9); + keycodes.insert(KeyCode::NumpadMultiply); + keycodes.insert(KeyCode::NumpadAdd); + keycodes.insert(KeyCode::NumpadComma); + keycodes.insert(KeyCode::NumpadSubtract); + keycodes.insert(KeyCode::NumpadDecimal); + keycodes.insert(KeyCode::NumpadDivide); + keycodes + }; +} + +bitflags! { + pub struct WindowsModifiers : u8 { + const SHIFT = 1 << 0; + const CONTROL = 1 << 1; + const ALT = 1 << 2; + const CAPS_LOCK = 1 << 3; + const FLAGS_END = 1 << 4; + } +} + +impl WindowsModifiers { + pub fn active_modifiers(key_state: &[u8; 256]) -> WindowsModifiers { + let shift = key_state[winuser::VK_SHIFT as usize] & 0x80 != 0; + let lshift = key_state[winuser::VK_LSHIFT as usize] & 0x80 != 0; + let rshift = key_state[winuser::VK_RSHIFT as usize] & 0x80 != 0; + + let control = key_state[winuser::VK_CONTROL as usize] & 0x80 != 0; + let lcontrol = key_state[winuser::VK_LCONTROL as usize] & 0x80 != 0; + let rcontrol = key_state[winuser::VK_RCONTROL as usize] & 0x80 != 0; + + let alt = key_state[winuser::VK_MENU as usize] & 0x80 != 0; + let lalt = key_state[winuser::VK_LMENU as usize] & 0x80 != 0; + let ralt = key_state[winuser::VK_RMENU as usize] & 0x80 != 0; + + let caps = key_state[winuser::VK_CAPITAL as usize] & 0x01 != 0; + + let mut result = WindowsModifiers::empty(); + if shift || lshift || rshift { + result.insert(WindowsModifiers::SHIFT); + } + if control || lcontrol || rcontrol { + result.insert(WindowsModifiers::CONTROL); + } + if alt || lalt || ralt { + result.insert(WindowsModifiers::ALT); + } + if caps { + result.insert(WindowsModifiers::CAPS_LOCK); + } + + result + } + + pub fn apply_to_kbd_state(self, key_state: &mut [u8; 256]) { + if self.intersects(Self::SHIFT) { + key_state[winuser::VK_SHIFT as usize] |= 0x80; + } else { + key_state[winuser::VK_SHIFT as usize] &= !0x80; + key_state[winuser::VK_LSHIFT as usize] &= !0x80; + key_state[winuser::VK_RSHIFT as usize] &= !0x80; + } + if self.intersects(Self::CONTROL) { + key_state[winuser::VK_CONTROL as usize] |= 0x80; + } else { + key_state[winuser::VK_CONTROL as usize] &= !0x80; + key_state[winuser::VK_LCONTROL as usize] &= !0x80; + key_state[winuser::VK_RCONTROL as usize] &= !0x80; + } + if self.intersects(Self::ALT) { + key_state[winuser::VK_MENU as usize] |= 0x80; + } else { + key_state[winuser::VK_MENU as usize] &= !0x80; + key_state[winuser::VK_LMENU as usize] &= !0x80; + key_state[winuser::VK_RMENU as usize] &= !0x80; + } + if self.intersects(Self::CAPS_LOCK) { + key_state[winuser::VK_CAPITAL as usize] |= 0x01; + } else { + key_state[winuser::VK_CAPITAL as usize] &= !0x01; + } + } + + /// Removes the control modifier if the alt modifier is not present. + /// This is useful because on Windows: (Control + Alt) == AltGr + /// but we don't want to interfere with the AltGr state. + pub fn remove_only_ctrl(mut self) -> WindowsModifiers { + if !self.contains(WindowsModifiers::ALT) { + self.remove(WindowsModifiers::CONTROL); + } + self + } +} + +pub(crate) struct Layout { + pub hkl: u64, + + /// Maps numpad keys from Windows virtual key to a `Key`. + /// + /// This is useful because some numpad keys generate different charcaters based on the locale. + /// For example `VK_DECIMAL` is sometimes "." and sometimes ",". Note: numpad-specific virtual + /// keys are only produced by Windows when the NumLock is active. + /// + /// Making this field separate from the `keys` field saves having to add NumLock as a modifier + /// to `WindowsModifiers`, which would double the number of items in keys. + pub numlock_on_keys: HashMap>, + /// Like `numlock_on_keys` but this will map to the key that would be produced if numlock was + /// off. The keys of this map are identical to the keys of `numlock_on_keys`. + pub numlock_off_keys: HashMap>, + + /// Maps a modifier state to group of key strings + /// We're not using `ModifiersState` here because that object cannot express caps lock, + /// but we need to handle caps lock too. + /// + /// This map shouldn't need to exist. + /// However currently this seems to be the only good way + /// of getting the label for the pressed key. Note that calling `ToUnicode` + /// just when the key is pressed/released would be enough if `ToUnicode` wouldn't + /// change the keyboard state (it clears the dead key). There is a flag to prevent + /// changing the state, but that flag requires Windows 10, version 1607 or newer) + pub keys: HashMap>>, + pub has_alt_graph: bool, +} + +impl Layout { + pub fn get_key( + &self, + mods: WindowsModifiers, + num_lock_on: bool, + vkey: c_int, + scancode: ExScancode, + keycode: KeyCode, + ) -> Key<'static> { + let native_code = NativeKeyCode::Windows(scancode); + + let unknown_alt = vkey == winuser::VK_MENU; + if !unknown_alt { + // Here we try using the virtual key directly but if the virtual key doesn't distinguish + // between left and right alt, we can't report AltGr. Therefore, we only do this if the + // key is not the "unknown alt" key. + // + // The reason for using the virtual key directly is that `MapVirtualKeyExW` (used when + // building the keys map) sometimes maps virtual keys to odd scancodes that don't match + // the scancode coming from the KEYDOWN message for the same key. For example: `VK_LEFT` + // is mapped to `0x004B`, but the scancode for the left arrow is `0xE04B`. + let key_from_vkey = vkey_to_non_char_key(vkey, native_code, self.hkl, self.has_alt_graph); + + if !matches!(key_from_vkey, Key::Unidentified(_)) { + return key_from_vkey; + } + } + if num_lock_on { + if let Some(key) = self.numlock_on_keys.get(&vkey) { + return key.clone(); + } + } else { + if let Some(key) = self.numlock_off_keys.get(&vkey) { + return key.clone(); + } + } + if let Some(keys) = self.keys.get(&mods) { + if let Some(key) = keys.get(&keycode) { + return key.clone(); + } + } + Key::Unidentified(native_code) + } +} + +#[derive(Default)] +pub(crate) struct LayoutCache { + /// Maps locale identifiers (HKL) to layouts + pub layouts: HashMap, + pub strings: HashSet<&'static str>, +} + +impl LayoutCache { + /// Checks whether the current layout is already known and + /// prepares the layout if it isn't known. + /// The current layout is then returned. + pub fn get_current_layout<'a>(&'a mut self) -> (u64, &'a Layout) { + let locale_id = unsafe { winuser::GetKeyboardLayout(0) } as u64; + match self.layouts.entry(locale_id) { + Entry::Occupied(entry) => (locale_id, entry.into_mut()), + Entry::Vacant(entry) => { + let layout = Self::prepare_layout(&mut self.strings, locale_id); + (locale_id, entry.insert(layout)) + } + } + } + + pub fn get_agnostic_mods(&mut self) -> ModifiersState { + let (_, layout) = self.get_current_layout(); + let filter_out_altgr = layout.has_alt_graph && key_pressed(winuser::VK_RMENU); + let mut mods = ModifiersState::empty(); + mods.set(ModifiersState::SHIFT, key_pressed(winuser::VK_SHIFT)); + mods.set( + ModifiersState::CONTROL, + key_pressed(winuser::VK_CONTROL) && !filter_out_altgr, + ); + mods.set( + ModifiersState::ALT, + key_pressed(winuser::VK_MENU) && !filter_out_altgr, + ); + mods.set( + ModifiersState::SUPER, + key_pressed(winuser::VK_LWIN) || key_pressed(winuser::VK_RWIN), + ); + mods + } + + fn prepare_layout(strings: &mut HashSet<&'static str>, locale_id: u64) -> Layout { + let mut layout = Layout { + hkl: locale_id, + numlock_on_keys: Default::default(), + numlock_off_keys: Default::default(), + keys: Default::default(), + has_alt_graph: false, + }; + + // We initialize the keyboard state with all zeros to + // simulate a scenario when no modifier is active. + let mut key_state = [0u8; 256]; + + // `MapVirtualKeyExW` maps (non-numpad-specific) virtual keys to scancodes as if numlock + // was off. We rely on this behavior to find all virtual keys which are not numpad-specific + // but map to the numpad. + // + // src_vkey: VK ==> scancode: u16 (on the numpad) + // + // Then we convert the source virtual key into a `Key` and the scancode into a virtual key + // to get the reverse mapping. + // + // src_vkey: VK ==> scancode: u16 (on the numpad) + // || || + // \/ \/ + // map_value: Key <- map_vkey: VK + layout.numlock_off_keys.reserve(NUMPAD_KEYCODES.len()); + for vk in 0..256 { + let scancode = + unsafe { winuser::MapVirtualKeyExW(vk, winuser::MAPVK_VK_TO_VSC_EX, locale_id as HKL) }; + if scancode == 0 { + continue; + } + let keycode = KeyCode::from_scancode(scancode); + if !is_numpad_specific(vk as i32) && NUMPAD_KEYCODES.contains(&keycode) { + let native_code = NativeKeyCode::Windows(scancode as u16); + let map_vkey = keycode_to_vkey(keycode, locale_id); + if map_vkey == 0 { + continue; + } + let map_value = vkey_to_non_char_key(vk as i32, native_code, locale_id, false); + if matches!(map_value, Key::Unidentified(_)) { + continue; + } + layout.numlock_off_keys.insert(map_vkey, map_value); + } + } + + layout.numlock_on_keys.reserve(NUMPAD_VKEYS.len()); + for vk in NUMPAD_VKEYS.iter() { + let vk = (*vk) as u32; + let scancode = + unsafe { winuser::MapVirtualKeyExW(vk, winuser::MAPVK_VK_TO_VSC_EX, locale_id as HKL) }; + let unicode = Self::to_unicode_string(&key_state, vk, scancode, locale_id); + if let ToUnicodeResult::Str(s) = unicode { + let static_str = get_or_insert_str(strings, s); + layout + .numlock_on_keys + .insert(vk as i32, Key::Character(static_str)); + } + } + + // Iterate through every combination of modifiers + let mods_end = WindowsModifiers::FLAGS_END.bits; + for mod_state in 0..mods_end { + let mut keys_for_this_mod = HashMap::with_capacity(256); + + let mod_state = unsafe { WindowsModifiers::from_bits_unchecked(mod_state) }; + mod_state.apply_to_kbd_state(&mut key_state); + + // Virtual key values are in the domain [0, 255]. + // This is reinforced by the fact that the keyboard state array has 256 + // elements. This array is allowed to be indexed by virtual key values + // giving the key state for the virtual key used for indexing. + for vk in 0..256 { + let scancode = + unsafe { winuser::MapVirtualKeyExW(vk, winuser::MAPVK_VK_TO_VSC_EX, locale_id as HKL) }; + if scancode == 0 { + continue; + } + + let native_code = NativeKeyCode::Windows(scancode as ExScancode); + let key_code = KeyCode::from_scancode(scancode); + // Let's try to get the key from just the scancode and vk + // We don't necessarily know yet if AltGraph is present on this layout so we'll + // assume it isn't. Then we'll do a second pass where we set the "AltRight" keys to + // "AltGr" in case we find out that there's an AltGraph. + let preliminary_key = vkey_to_non_char_key(vk as i32, native_code, locale_id, false); + match preliminary_key { + Key::Unidentified(_) => (), + _ => { + keys_for_this_mod.insert(key_code, preliminary_key); + continue; + } + } + + let unicode = Self::to_unicode_string(&key_state, vk, scancode, locale_id); + let key = match unicode { + ToUnicodeResult::Str(str) => { + let static_str = get_or_insert_str(strings, str); + Key::Character(static_str) + } + ToUnicodeResult::Dead(dead_char) => { + //println!("{:?} - {:?} produced dead {:?}", key_code, mod_state, dead_char); + Key::Dead(dead_char) + } + ToUnicodeResult::None => { + let has_alt = mod_state.contains(WindowsModifiers::ALT); + let has_ctrl = mod_state.contains(WindowsModifiers::CONTROL); + // HACK: `ToUnicodeEx` seems to fail getting the string for the numpad + // divide key, so we handle that explicitly here + if !has_alt && !has_ctrl && key_code == KeyCode::NumpadDivide { + Key::Character("/") + } else { + // Just use the unidentified key, we got earlier + preliminary_key + } + } + }; + + // Check for alt graph. + // The logic is that if a key pressed with no modifier produces + // a different `Character` from when it's pressed with CTRL+ALT then the layout + // has AltGr. + let ctrl_alt: WindowsModifiers = WindowsModifiers::CONTROL | WindowsModifiers::ALT; + let is_in_ctrl_alt = mod_state == ctrl_alt; + if !layout.has_alt_graph && is_in_ctrl_alt { + // Unwrapping here because if we are in the ctrl+alt modifier state + // then the alt modifier state must have come before. + let simple_keys = layout.keys.get(&WindowsModifiers::empty()).unwrap(); + if let Some(Key::Character(key_no_altgr)) = simple_keys.get(&key_code) { + if let Key::Character(key) = key.clone() { + layout.has_alt_graph = key != *key_no_altgr; + } + } + } + + keys_for_this_mod.insert(key_code, key); + } + layout.keys.insert(mod_state, keys_for_this_mod); + } + + // Second pass: replace right alt keys with AltGr if the layout has alt graph + if layout.has_alt_graph { + for mod_state in 0..mods_end { + let mod_state = unsafe { WindowsModifiers::from_bits_unchecked(mod_state) }; + if let Some(keys) = layout.keys.get_mut(&mod_state) { + if let Some(key) = keys.get_mut(&KeyCode::AltRight) { + *key = Key::AltGraph; + } + } + } + } + + layout + } + + fn to_unicode_string( + key_state: &[u8; 256], + vkey: u32, + scancode: u32, + locale_id: u64, + ) -> ToUnicodeResult { + unsafe { + let mut label_wide = [0u16; 8]; + let mut wide_len = winuser::ToUnicodeEx( + vkey, + scancode, + (&key_state[0]) as *const _, + (&mut label_wide[0]) as *mut _, + label_wide.len() as i32, + 0, + locale_id as HKL, + ); + if wide_len < 0 { + // If it's dead, we run `ToUnicode` again to consume the dead-key + wide_len = winuser::ToUnicodeEx( + vkey, + scancode, + (&key_state[0]) as *const _, + (&mut label_wide[0]) as *mut _, + label_wide.len() as i32, + 0, + locale_id as HKL, + ); + if wide_len > 0 { + let os_string = OsString::from_wide(&label_wide[0..wide_len as usize]); + if let Ok(label_str) = os_string.into_string() { + if let Some(ch) = label_str.chars().next() { + return ToUnicodeResult::Dead(Some(ch)); + } + } + } + return ToUnicodeResult::Dead(None); + } + if wide_len > 0 { + let os_string = OsString::from_wide(&label_wide[0..wide_len as usize]); + if let Ok(label_str) = os_string.into_string() { + return ToUnicodeResult::Str(label_str); + } + } + } + ToUnicodeResult::None + } +} + +pub fn get_or_insert_str(strings: &mut HashSet<&'static str>, string: T) -> &'static str +where + T: AsRef, + String: From, +{ + { + let str_ref = string.as_ref(); + if let Some(&existing) = strings.get(str_ref) { + return existing; + } + } + let leaked = Box::leak(Box::from(String::from(string))); + strings.insert(leaked); + leaked +} + +#[derive(Debug, Clone, Eq, PartialEq)] +enum ToUnicodeResult { + Str(String), + Dead(Option), + None, +} + +fn is_numpad_specific(vk: i32) -> bool { + match vk { + winuser::VK_NUMPAD0 => true, + winuser::VK_NUMPAD1 => true, + winuser::VK_NUMPAD2 => true, + winuser::VK_NUMPAD3 => true, + winuser::VK_NUMPAD4 => true, + winuser::VK_NUMPAD5 => true, + winuser::VK_NUMPAD6 => true, + winuser::VK_NUMPAD7 => true, + winuser::VK_NUMPAD8 => true, + winuser::VK_NUMPAD9 => true, + winuser::VK_ADD => true, + winuser::VK_SUBTRACT => true, + winuser::VK_DIVIDE => true, + winuser::VK_DECIMAL => true, + winuser::VK_SEPARATOR => true, + _ => false, + } +} + +fn keycode_to_vkey(keycode: KeyCode, hkl: u64) -> i32 { + let primary_lang_id = PRIMARYLANGID(LOWORD(hkl as u32)); + let is_korean = primary_lang_id == LANG_KOREAN; + let is_japanese = primary_lang_id == LANG_JAPANESE; + + match keycode { + KeyCode::Backquote => 0, + KeyCode::Backslash => 0, + KeyCode::BracketLeft => 0, + KeyCode::BracketRight => 0, + KeyCode::Comma => 0, + KeyCode::Digit0 => 0, + KeyCode::Digit1 => 0, + KeyCode::Digit2 => 0, + KeyCode::Digit3 => 0, + KeyCode::Digit4 => 0, + KeyCode::Digit5 => 0, + KeyCode::Digit6 => 0, + KeyCode::Digit7 => 0, + KeyCode::Digit8 => 0, + KeyCode::Digit9 => 0, + KeyCode::Equal => 0, + KeyCode::IntlBackslash => 0, + KeyCode::IntlRo => 0, + KeyCode::IntlYen => 0, + KeyCode::KeyA => 0, + KeyCode::KeyB => 0, + KeyCode::KeyC => 0, + KeyCode::KeyD => 0, + KeyCode::KeyE => 0, + KeyCode::KeyF => 0, + KeyCode::KeyG => 0, + KeyCode::KeyH => 0, + KeyCode::KeyI => 0, + KeyCode::KeyJ => 0, + KeyCode::KeyK => 0, + KeyCode::KeyL => 0, + KeyCode::KeyM => 0, + KeyCode::KeyN => 0, + KeyCode::KeyO => 0, + KeyCode::KeyP => 0, + KeyCode::KeyQ => 0, + KeyCode::KeyR => 0, + KeyCode::KeyS => 0, + KeyCode::KeyT => 0, + KeyCode::KeyU => 0, + KeyCode::KeyV => 0, + KeyCode::KeyW => 0, + KeyCode::KeyX => 0, + KeyCode::KeyY => 0, + KeyCode::KeyZ => 0, + KeyCode::Minus => 0, + KeyCode::Period => 0, + KeyCode::Quote => 0, + KeyCode::Semicolon => 0, + KeyCode::Slash => 0, + KeyCode::AltLeft => winuser::VK_LMENU, + KeyCode::AltRight => winuser::VK_RMENU, + KeyCode::Backspace => winuser::VK_BACK, + KeyCode::CapsLock => winuser::VK_CAPITAL, + KeyCode::ContextMenu => winuser::VK_APPS, + KeyCode::ControlLeft => winuser::VK_LCONTROL, + KeyCode::ControlRight => winuser::VK_RCONTROL, + KeyCode::Enter => winuser::VK_RETURN, + KeyCode::SuperLeft => winuser::VK_LWIN, + KeyCode::SuperRight => winuser::VK_RWIN, + KeyCode::ShiftLeft => winuser::VK_RSHIFT, + KeyCode::ShiftRight => winuser::VK_LSHIFT, + KeyCode::Space => winuser::VK_SPACE, + KeyCode::Tab => winuser::VK_TAB, + KeyCode::Convert => winuser::VK_CONVERT, + KeyCode::KanaMode => winuser::VK_KANA, + KeyCode::Lang1 if is_korean => winuser::VK_HANGUL, + KeyCode::Lang1 if is_japanese => winuser::VK_KANA, + KeyCode::Lang2 if is_korean => winuser::VK_HANJA, + KeyCode::Lang2 if is_japanese => 0, + KeyCode::Lang3 if is_japanese => winuser::VK_OEM_FINISH, + KeyCode::Lang4 if is_japanese => 0, + KeyCode::Lang5 if is_japanese => 0, + KeyCode::NonConvert => winuser::VK_NONCONVERT, + KeyCode::Delete => winuser::VK_DELETE, + KeyCode::End => winuser::VK_END, + KeyCode::Help => winuser::VK_HELP, + KeyCode::Home => winuser::VK_HOME, + KeyCode::Insert => winuser::VK_INSERT, + KeyCode::PageDown => winuser::VK_NEXT, + KeyCode::PageUp => winuser::VK_PRIOR, + KeyCode::ArrowDown => winuser::VK_DOWN, + KeyCode::ArrowLeft => winuser::VK_LEFT, + KeyCode::ArrowRight => winuser::VK_RIGHT, + KeyCode::ArrowUp => winuser::VK_UP, + KeyCode::NumLock => winuser::VK_NUMLOCK, + KeyCode::Numpad0 => winuser::VK_NUMPAD0, + KeyCode::Numpad1 => winuser::VK_NUMPAD1, + KeyCode::Numpad2 => winuser::VK_NUMPAD2, + KeyCode::Numpad3 => winuser::VK_NUMPAD3, + KeyCode::Numpad4 => winuser::VK_NUMPAD4, + KeyCode::Numpad5 => winuser::VK_NUMPAD5, + KeyCode::Numpad6 => winuser::VK_NUMPAD6, + KeyCode::Numpad7 => winuser::VK_NUMPAD7, + KeyCode::Numpad8 => winuser::VK_NUMPAD8, + KeyCode::Numpad9 => winuser::VK_NUMPAD9, + KeyCode::NumpadAdd => winuser::VK_ADD, + KeyCode::NumpadBackspace => winuser::VK_BACK, + KeyCode::NumpadClear => winuser::VK_CLEAR, + KeyCode::NumpadClearEntry => 0, + KeyCode::NumpadComma => winuser::VK_SEPARATOR, + KeyCode::NumpadDecimal => winuser::VK_DECIMAL, + KeyCode::NumpadDivide => winuser::VK_DIVIDE, + KeyCode::NumpadEnter => winuser::VK_RETURN, + KeyCode::NumpadEqual => 0, + KeyCode::NumpadHash => 0, + KeyCode::NumpadMemoryAdd => 0, + KeyCode::NumpadMemoryClear => 0, + KeyCode::NumpadMemoryRecall => 0, + KeyCode::NumpadMemoryStore => 0, + KeyCode::NumpadMemorySubtract => 0, + KeyCode::NumpadMultiply => winuser::VK_MULTIPLY, + KeyCode::NumpadParenLeft => 0, + KeyCode::NumpadParenRight => 0, + KeyCode::NumpadStar => 0, + KeyCode::NumpadSubtract => winuser::VK_SUBTRACT, + KeyCode::Escape => winuser::VK_ESCAPE, + KeyCode::Fn => 0, + KeyCode::FnLock => 0, + KeyCode::PrintScreen => winuser::VK_SNAPSHOT, + KeyCode::ScrollLock => winuser::VK_SCROLL, + KeyCode::Pause => winuser::VK_PAUSE, + KeyCode::BrowserBack => winuser::VK_BROWSER_BACK, + KeyCode::BrowserFavorites => winuser::VK_BROWSER_FAVORITES, + KeyCode::BrowserForward => winuser::VK_BROWSER_FORWARD, + KeyCode::BrowserHome => winuser::VK_BROWSER_HOME, + KeyCode::BrowserRefresh => winuser::VK_BROWSER_REFRESH, + KeyCode::BrowserSearch => winuser::VK_BROWSER_SEARCH, + KeyCode::BrowserStop => winuser::VK_BROWSER_STOP, + KeyCode::Eject => 0, + KeyCode::LaunchApp1 => winuser::VK_LAUNCH_APP1, + KeyCode::LaunchApp2 => winuser::VK_LAUNCH_APP2, + KeyCode::LaunchMail => winuser::VK_LAUNCH_MAIL, + KeyCode::MediaPlayPause => winuser::VK_MEDIA_PLAY_PAUSE, + KeyCode::MediaSelect => winuser::VK_LAUNCH_MEDIA_SELECT, + KeyCode::MediaStop => winuser::VK_MEDIA_STOP, + KeyCode::MediaTrackNext => winuser::VK_MEDIA_NEXT_TRACK, + KeyCode::MediaTrackPrevious => winuser::VK_MEDIA_PREV_TRACK, + KeyCode::Power => 0, + KeyCode::Sleep => 0, + KeyCode::AudioVolumeDown => winuser::VK_VOLUME_DOWN, + KeyCode::AudioVolumeMute => winuser::VK_VOLUME_MUTE, + KeyCode::AudioVolumeUp => winuser::VK_VOLUME_UP, + KeyCode::WakeUp => 0, + KeyCode::Hyper => 0, + KeyCode::Turbo => 0, + KeyCode::Abort => 0, + KeyCode::Resume => 0, + KeyCode::Suspend => 0, + KeyCode::Again => 0, + KeyCode::Copy => 0, + KeyCode::Cut => 0, + KeyCode::Find => 0, + KeyCode::Open => 0, + KeyCode::Paste => 0, + KeyCode::Props => 0, + KeyCode::Select => winuser::VK_SELECT, + KeyCode::Undo => 0, + KeyCode::Hiragana => 0, + KeyCode::Katakana => 0, + KeyCode::F1 => winuser::VK_F1, + KeyCode::F2 => winuser::VK_F2, + KeyCode::F3 => winuser::VK_F3, + KeyCode::F4 => winuser::VK_F4, + KeyCode::F5 => winuser::VK_F5, + KeyCode::F6 => winuser::VK_F6, + KeyCode::F7 => winuser::VK_F7, + KeyCode::F8 => winuser::VK_F8, + KeyCode::F9 => winuser::VK_F9, + KeyCode::F10 => winuser::VK_F10, + KeyCode::F11 => winuser::VK_F11, + KeyCode::F12 => winuser::VK_F12, + KeyCode::F13 => winuser::VK_F13, + KeyCode::F14 => winuser::VK_F14, + KeyCode::F15 => winuser::VK_F15, + KeyCode::F16 => winuser::VK_F16, + KeyCode::F17 => winuser::VK_F17, + KeyCode::F18 => winuser::VK_F18, + KeyCode::F19 => winuser::VK_F19, + KeyCode::F20 => winuser::VK_F20, + KeyCode::F21 => winuser::VK_F21, + KeyCode::F22 => winuser::VK_F22, + KeyCode::F23 => winuser::VK_F23, + KeyCode::F24 => winuser::VK_F24, + KeyCode::F25 => 0, + KeyCode::F26 => 0, + KeyCode::F27 => 0, + KeyCode::F28 => 0, + KeyCode::F29 => 0, + KeyCode::F30 => 0, + KeyCode::F31 => 0, + KeyCode::F32 => 0, + KeyCode::F33 => 0, + KeyCode::F34 => 0, + KeyCode::F35 => 0, + KeyCode::Unidentified(_) => 0, + _ => 0, + } +} + +/// This converts virtual keys to `Key`s. Only virtual keys which can be unambiguously converted to +/// a `Key`, with only the information passed in as arguments, are converted. +/// +/// In other words: this function does not need to "prepare" the current layout in order to do +/// the conversion, but as such it cannot convert certain keys, like language-specific character keys. +/// +/// The result includes all non-character keys defined within `Key` plus characters from numpad keys. +/// For example, backspace and tab are included. +fn vkey_to_non_char_key( + vkey: i32, + native_code: NativeKeyCode, + hkl: u64, + has_alt_graph: bool, +) -> Key<'static> { + // List of the Web key names and their corresponding platform-native key names: + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + + let primary_lang_id = PRIMARYLANGID(LOWORD(hkl as u32)); + let is_korean = primary_lang_id == LANG_KOREAN; + let is_japanese = primary_lang_id == LANG_JAPANESE; + + match vkey { + winuser::VK_LBUTTON => Key::Unidentified(NativeKeyCode::Unidentified), // Mouse + winuser::VK_RBUTTON => Key::Unidentified(NativeKeyCode::Unidentified), // Mouse + + // I don't think this can be represented with a Key + winuser::VK_CANCEL => Key::Unidentified(native_code), + + winuser::VK_MBUTTON => Key::Unidentified(NativeKeyCode::Unidentified), // Mouse + winuser::VK_XBUTTON1 => Key::Unidentified(NativeKeyCode::Unidentified), // Mouse + winuser::VK_XBUTTON2 => Key::Unidentified(NativeKeyCode::Unidentified), // Mouse + winuser::VK_BACK => Key::Backspace, + winuser::VK_TAB => Key::Tab, + winuser::VK_CLEAR => Key::Clear, + winuser::VK_RETURN => Key::Enter, + winuser::VK_SHIFT => Key::Shift, + winuser::VK_CONTROL => Key::Control, + winuser::VK_MENU => Key::Alt, + winuser::VK_PAUSE => Key::Pause, + winuser::VK_CAPITAL => Key::CapsLock, + + //winuser::VK_HANGEUL => Key::HangulMode, // Deprecated in favour of VK_HANGUL + + // VK_HANGUL and VK_KANA are defined as the same constant, therefore + // we use appropriate conditions to differentate between them + winuser::VK_HANGUL if is_korean => Key::HangulMode, + winuser::VK_KANA if is_japanese => Key::KanaMode, + + winuser::VK_JUNJA => Key::JunjaMode, + winuser::VK_FINAL => Key::FinalMode, + + // VK_HANJA and VK_KANJI are defined as the same constant, therefore + // we use appropriate conditions to differentate between them + winuser::VK_HANJA if is_korean => Key::HanjaMode, + winuser::VK_KANJI if is_japanese => Key::KanjiMode, + + winuser::VK_ESCAPE => Key::Escape, + winuser::VK_CONVERT => Key::Convert, + winuser::VK_NONCONVERT => Key::NonConvert, + winuser::VK_ACCEPT => Key::Accept, + winuser::VK_MODECHANGE => Key::ModeChange, + winuser::VK_SPACE => Key::Space, + winuser::VK_PRIOR => Key::PageUp, + winuser::VK_NEXT => Key::PageDown, + winuser::VK_END => Key::End, + winuser::VK_HOME => Key::Home, + winuser::VK_LEFT => Key::ArrowLeft, + winuser::VK_UP => Key::ArrowUp, + winuser::VK_RIGHT => Key::ArrowRight, + winuser::VK_DOWN => Key::ArrowDown, + winuser::VK_SELECT => Key::Select, + winuser::VK_PRINT => Key::Print, + winuser::VK_EXECUTE => Key::Execute, + winuser::VK_SNAPSHOT => Key::PrintScreen, + winuser::VK_INSERT => Key::Insert, + winuser::VK_DELETE => Key::Delete, + winuser::VK_HELP => Key::Help, + winuser::VK_LWIN => Key::Super, + winuser::VK_RWIN => Key::Super, + winuser::VK_APPS => Key::ContextMenu, + winuser::VK_SLEEP => Key::Standby, + + // Numpad keys produce characters + winuser::VK_NUMPAD0 => Key::Unidentified(native_code), + winuser::VK_NUMPAD1 => Key::Unidentified(native_code), + winuser::VK_NUMPAD2 => Key::Unidentified(native_code), + winuser::VK_NUMPAD3 => Key::Unidentified(native_code), + winuser::VK_NUMPAD4 => Key::Unidentified(native_code), + winuser::VK_NUMPAD5 => Key::Unidentified(native_code), + winuser::VK_NUMPAD6 => Key::Unidentified(native_code), + winuser::VK_NUMPAD7 => Key::Unidentified(native_code), + winuser::VK_NUMPAD8 => Key::Unidentified(native_code), + winuser::VK_NUMPAD9 => Key::Unidentified(native_code), + winuser::VK_MULTIPLY => Key::Unidentified(native_code), + winuser::VK_ADD => Key::Unidentified(native_code), + winuser::VK_SEPARATOR => Key::Unidentified(native_code), + winuser::VK_SUBTRACT => Key::Unidentified(native_code), + winuser::VK_DECIMAL => Key::Unidentified(native_code), + winuser::VK_DIVIDE => Key::Unidentified(native_code), + + winuser::VK_F1 => Key::F1, + winuser::VK_F2 => Key::F2, + winuser::VK_F3 => Key::F3, + winuser::VK_F4 => Key::F4, + winuser::VK_F5 => Key::F5, + winuser::VK_F6 => Key::F6, + winuser::VK_F7 => Key::F7, + winuser::VK_F8 => Key::F8, + winuser::VK_F9 => Key::F9, + winuser::VK_F10 => Key::F10, + winuser::VK_F11 => Key::F11, + winuser::VK_F12 => Key::F12, + winuser::VK_F13 => Key::F13, + winuser::VK_F14 => Key::F14, + winuser::VK_F15 => Key::F15, + winuser::VK_F16 => Key::F16, + winuser::VK_F17 => Key::F17, + winuser::VK_F18 => Key::F18, + winuser::VK_F19 => Key::F19, + winuser::VK_F20 => Key::F20, + winuser::VK_F21 => Key::F21, + winuser::VK_F22 => Key::F22, + winuser::VK_F23 => Key::F23, + winuser::VK_F24 => Key::F24, + winuser::VK_NAVIGATION_VIEW => Key::Unidentified(native_code), + winuser::VK_NAVIGATION_MENU => Key::Unidentified(native_code), + winuser::VK_NAVIGATION_UP => Key::Unidentified(native_code), + winuser::VK_NAVIGATION_DOWN => Key::Unidentified(native_code), + winuser::VK_NAVIGATION_LEFT => Key::Unidentified(native_code), + winuser::VK_NAVIGATION_RIGHT => Key::Unidentified(native_code), + winuser::VK_NAVIGATION_ACCEPT => Key::Unidentified(native_code), + winuser::VK_NAVIGATION_CANCEL => Key::Unidentified(native_code), + winuser::VK_NUMLOCK => Key::NumLock, + winuser::VK_SCROLL => Key::ScrollLock, + winuser::VK_OEM_NEC_EQUAL => Key::Unidentified(native_code), + //winuser::VK_OEM_FJ_JISHO => Key::Unidentified(native_code), // Conflicts with `VK_OEM_NEC_EQUAL` + winuser::VK_OEM_FJ_MASSHOU => Key::Unidentified(native_code), + winuser::VK_OEM_FJ_TOUROKU => Key::Unidentified(native_code), + winuser::VK_OEM_FJ_LOYA => Key::Unidentified(native_code), + winuser::VK_OEM_FJ_ROYA => Key::Unidentified(native_code), + winuser::VK_LSHIFT => Key::Shift, + winuser::VK_RSHIFT => Key::Shift, + winuser::VK_LCONTROL => Key::Control, + winuser::VK_RCONTROL => Key::Control, + winuser::VK_LMENU => Key::Alt, + winuser::VK_RMENU => { + if has_alt_graph { + Key::AltGraph + } else { + Key::Alt + } + } + winuser::VK_BROWSER_BACK => Key::BrowserBack, + winuser::VK_BROWSER_FORWARD => Key::BrowserForward, + winuser::VK_BROWSER_REFRESH => Key::BrowserRefresh, + winuser::VK_BROWSER_STOP => Key::BrowserStop, + winuser::VK_BROWSER_SEARCH => Key::BrowserSearch, + winuser::VK_BROWSER_FAVORITES => Key::BrowserFavorites, + winuser::VK_BROWSER_HOME => Key::BrowserHome, + winuser::VK_VOLUME_MUTE => Key::AudioVolumeMute, + winuser::VK_VOLUME_DOWN => Key::AudioVolumeDown, + winuser::VK_VOLUME_UP => Key::AudioVolumeUp, + winuser::VK_MEDIA_NEXT_TRACK => Key::MediaTrackNext, + winuser::VK_MEDIA_PREV_TRACK => Key::MediaTrackPrevious, + winuser::VK_MEDIA_STOP => Key::MediaStop, + winuser::VK_MEDIA_PLAY_PAUSE => Key::MediaPlayPause, + winuser::VK_LAUNCH_MAIL => Key::LaunchMail, + winuser::VK_LAUNCH_MEDIA_SELECT => Key::LaunchMediaPlayer, + winuser::VK_LAUNCH_APP1 => Key::LaunchApplication1, + winuser::VK_LAUNCH_APP2 => Key::LaunchApplication2, + + // This function only converts "non-printable" + winuser::VK_OEM_1 => Key::Unidentified(native_code), + winuser::VK_OEM_PLUS => Key::Unidentified(native_code), + winuser::VK_OEM_COMMA => Key::Unidentified(native_code), + winuser::VK_OEM_MINUS => Key::Unidentified(native_code), + winuser::VK_OEM_PERIOD => Key::Unidentified(native_code), + winuser::VK_OEM_2 => Key::Unidentified(native_code), + winuser::VK_OEM_3 => Key::Unidentified(native_code), + + winuser::VK_GAMEPAD_A => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_B => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_X => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_Y => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_RIGHT_SHOULDER => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_LEFT_SHOULDER => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_LEFT_TRIGGER => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_RIGHT_TRIGGER => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_DPAD_UP => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_DPAD_DOWN => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_DPAD_LEFT => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_DPAD_RIGHT => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_MENU => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_VIEW => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_LEFT_THUMBSTICK_BUTTON => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_RIGHT_THUMBSTICK_BUTTON => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_LEFT_THUMBSTICK_UP => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_LEFT_THUMBSTICK_DOWN => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_LEFT_THUMBSTICK_RIGHT => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_LEFT_THUMBSTICK_LEFT => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_RIGHT_THUMBSTICK_UP => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_RIGHT_THUMBSTICK_DOWN => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_RIGHT_THUMBSTICK_RIGHT => Key::Unidentified(native_code), + winuser::VK_GAMEPAD_RIGHT_THUMBSTICK_LEFT => Key::Unidentified(native_code), + + // This function only converts "non-printable" + winuser::VK_OEM_4 => Key::Unidentified(native_code), + winuser::VK_OEM_5 => Key::Unidentified(native_code), + winuser::VK_OEM_6 => Key::Unidentified(native_code), + winuser::VK_OEM_7 => Key::Unidentified(native_code), + winuser::VK_OEM_8 => Key::Unidentified(native_code), + winuser::VK_OEM_AX => Key::Unidentified(native_code), + winuser::VK_OEM_102 => Key::Unidentified(native_code), + + winuser::VK_ICO_HELP => Key::Unidentified(native_code), + winuser::VK_ICO_00 => Key::Unidentified(native_code), + + winuser::VK_PROCESSKEY => Key::Process, + + winuser::VK_ICO_CLEAR => Key::Unidentified(native_code), + winuser::VK_PACKET => Key::Unidentified(native_code), + winuser::VK_OEM_RESET => Key::Unidentified(native_code), + winuser::VK_OEM_JUMP => Key::Unidentified(native_code), + winuser::VK_OEM_PA1 => Key::Unidentified(native_code), + winuser::VK_OEM_PA2 => Key::Unidentified(native_code), + winuser::VK_OEM_PA3 => Key::Unidentified(native_code), + winuser::VK_OEM_WSCTRL => Key::Unidentified(native_code), + winuser::VK_OEM_CUSEL => Key::Unidentified(native_code), + + winuser::VK_OEM_ATTN => Key::Attn, + winuser::VK_OEM_FINISH => { + if is_japanese { + Key::Katakana + } else { + // This matches IE and Firefox behaviour according to + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + // At the time of writing, there is no `Key::Finish` variant as + // Finish is not mentionned at https://w3c.github.io/uievents-key/ + // Also see: https://github.com/pyfisch/keyboard-types/issues/9 + Key::Unidentified(native_code) + } + } + winuser::VK_OEM_COPY => Key::Copy, + winuser::VK_OEM_AUTO => Key::Hankaku, + winuser::VK_OEM_ENLW => Key::Zenkaku, + winuser::VK_OEM_BACKTAB => Key::Romaji, + winuser::VK_ATTN => Key::KanaMode, + winuser::VK_CRSEL => Key::CrSel, + winuser::VK_EXSEL => Key::ExSel, + winuser::VK_EREOF => Key::EraseEof, + winuser::VK_PLAY => Key::Play, + winuser::VK_ZOOM => Key::ZoomToggle, + winuser::VK_NONAME => Key::Unidentified(native_code), + winuser::VK_PA1 => Key::Unidentified(native_code), + winuser::VK_OEM_CLEAR => Key::Clear, + _ => Key::Unidentified(native_code), + } +} diff --git a/src/platform_impl/windows/keycode.rs b/src/platform_impl/windows/keycode.rs new file mode 100644 index 000000000..495b394b2 --- /dev/null +++ b/src/platform_impl/windows/keycode.rs @@ -0,0 +1,339 @@ +use crate::keyboard::{KeyCode, NativeKeyCode}; +use winapi::{ + shared::minwindef::LOWORD, + um::{ + winnt::{LANG_KOREAN, PRIMARYLANGID}, + winuser::GetKeyboardLayout, + }, +}; + +pub fn keycode_to_scancode(code: KeyCode) -> Option { + // See `from_scancode` for more info + let hkl = unsafe { GetKeyboardLayout(0) }; + + let primary_lang_id = PRIMARYLANGID(LOWORD(hkl as u32)); + let is_korean = primary_lang_id == LANG_KOREAN; + + match code { + KeyCode::Backquote => Some(0x0029), + KeyCode::Backslash => Some(0x002B), + KeyCode::Backspace => Some(0x000E), + KeyCode::BracketLeft => Some(0x001A), + KeyCode::BracketRight => Some(0x001B), + KeyCode::Comma => Some(0x0033), + KeyCode::Digit0 => Some(0x000B), + KeyCode::Digit1 => Some(0x0002), + KeyCode::Digit2 => Some(0x0003), + KeyCode::Digit3 => Some(0x0004), + KeyCode::Digit4 => Some(0x0005), + KeyCode::Digit5 => Some(0x0006), + KeyCode::Digit6 => Some(0x0007), + KeyCode::Digit7 => Some(0x0008), + KeyCode::Digit8 => Some(0x0009), + KeyCode::Digit9 => Some(0x000A), + KeyCode::Equal => Some(0x000D), + KeyCode::IntlBackslash => Some(0x0056), + KeyCode::IntlRo => Some(0x0073), + KeyCode::IntlYen => Some(0x007D), + KeyCode::KeyA => Some(0x001E), + KeyCode::KeyB => Some(0x0030), + KeyCode::KeyC => Some(0x002E), + KeyCode::KeyD => Some(0x0020), + KeyCode::KeyE => Some(0x0012), + KeyCode::KeyF => Some(0x0021), + KeyCode::KeyG => Some(0x0022), + KeyCode::KeyH => Some(0x0023), + KeyCode::KeyI => Some(0x0017), + KeyCode::KeyJ => Some(0x0024), + KeyCode::KeyK => Some(0x0025), + KeyCode::KeyL => Some(0x0026), + KeyCode::KeyM => Some(0x0032), + KeyCode::KeyN => Some(0x0031), + KeyCode::KeyO => Some(0x0018), + KeyCode::KeyP => Some(0x0019), + KeyCode::KeyQ => Some(0x0010), + KeyCode::KeyR => Some(0x0013), + KeyCode::KeyS => Some(0x001F), + KeyCode::KeyT => Some(0x0014), + KeyCode::KeyU => Some(0x0016), + KeyCode::KeyV => Some(0x002F), + KeyCode::KeyW => Some(0x0011), + KeyCode::KeyX => Some(0x002D), + KeyCode::KeyY => Some(0x0015), + KeyCode::KeyZ => Some(0x002C), + KeyCode::Minus => Some(0x000C), + KeyCode::Period => Some(0x0034), + KeyCode::Quote => Some(0x0028), + KeyCode::Semicolon => Some(0x0027), + KeyCode::Slash => Some(0x0035), + KeyCode::AltLeft => Some(0x0038), + KeyCode::AltRight => Some(0xE038), + KeyCode::CapsLock => Some(0x003A), + KeyCode::ContextMenu => Some(0xE05D), + KeyCode::ControlLeft => Some(0x001D), + KeyCode::ControlRight => Some(0xE01D), + KeyCode::Enter => Some(0x001C), + KeyCode::SuperLeft => Some(0xE05B), + KeyCode::SuperRight => Some(0xE05C), + KeyCode::ShiftLeft => Some(0x002A), + KeyCode::ShiftRight => Some(0x0036), + KeyCode::Space => Some(0x0039), + KeyCode::Tab => Some(0x000F), + KeyCode::Convert => Some(0x0079), + KeyCode::Lang1 => { + if is_korean { + Some(0xE0F2) + } else { + Some(0x0072) + } + } + KeyCode::Lang2 => { + if is_korean { + Some(0xE0F1) + } else { + Some(0x0071) + } + } + KeyCode::KanaMode => Some(0x0070), + KeyCode::NonConvert => Some(0x007B), + KeyCode::Delete => Some(0xE053), + KeyCode::End => Some(0xE04F), + KeyCode::Home => Some(0xE047), + KeyCode::Insert => Some(0xE052), + KeyCode::PageDown => Some(0xE051), + KeyCode::PageUp => Some(0xE049), + KeyCode::ArrowDown => Some(0xE050), + KeyCode::ArrowLeft => Some(0xE04B), + KeyCode::ArrowRight => Some(0xE04D), + KeyCode::ArrowUp => Some(0xE048), + KeyCode::NumLock => Some(0xE045), + KeyCode::Numpad0 => Some(0x0052), + KeyCode::Numpad1 => Some(0x004F), + KeyCode::Numpad2 => Some(0x0050), + KeyCode::Numpad3 => Some(0x0051), + KeyCode::Numpad4 => Some(0x004B), + KeyCode::Numpad5 => Some(0x004C), + KeyCode::Numpad6 => Some(0x004D), + KeyCode::Numpad7 => Some(0x0047), + KeyCode::Numpad8 => Some(0x0048), + KeyCode::Numpad9 => Some(0x0049), + KeyCode::NumpadAdd => Some(0x004E), + KeyCode::NumpadComma => Some(0x007E), + KeyCode::NumpadDecimal => Some(0x0053), + KeyCode::NumpadDivide => Some(0xE035), + KeyCode::NumpadEnter => Some(0xE01C), + KeyCode::NumpadEqual => Some(0x0059), + KeyCode::NumpadMultiply => Some(0x0037), + KeyCode::NumpadSubtract => Some(0x004A), + KeyCode::Escape => Some(0x0001), + KeyCode::F1 => Some(0x003B), + KeyCode::F2 => Some(0x003C), + KeyCode::F3 => Some(0x003D), + KeyCode::F4 => Some(0x003E), + KeyCode::F5 => Some(0x003F), + KeyCode::F6 => Some(0x0040), + KeyCode::F7 => Some(0x0041), + KeyCode::F8 => Some(0x0042), + KeyCode::F9 => Some(0x0043), + KeyCode::F10 => Some(0x0044), + KeyCode::F11 => Some(0x0057), + KeyCode::F12 => Some(0x0058), + KeyCode::F13 => Some(0x0064), + KeyCode::F14 => Some(0x0065), + KeyCode::F15 => Some(0x0066), + KeyCode::F16 => Some(0x0067), + KeyCode::F17 => Some(0x0068), + KeyCode::F18 => Some(0x0069), + KeyCode::F19 => Some(0x006A), + KeyCode::F20 => Some(0x006B), + KeyCode::F21 => Some(0x006C), + KeyCode::F22 => Some(0x006D), + KeyCode::F23 => Some(0x006E), + KeyCode::F24 => Some(0x0076), + KeyCode::PrintScreen => Some(0xE037), + //KeyCode::PrintScreen => Some(0x0054), // Alt + PrintScreen + KeyCode::ScrollLock => Some(0x0046), + KeyCode::Pause => Some(0x0045), + //KeyCode::Pause => Some(0xE046), // Ctrl + Pause + KeyCode::BrowserBack => Some(0xE06A), + KeyCode::BrowserFavorites => Some(0xE066), + KeyCode::BrowserForward => Some(0xE069), + KeyCode::BrowserHome => Some(0xE032), + KeyCode::BrowserRefresh => Some(0xE067), + KeyCode::BrowserSearch => Some(0xE065), + KeyCode::BrowserStop => Some(0xE068), + KeyCode::LaunchApp1 => Some(0xE06B), + KeyCode::LaunchApp2 => Some(0xE021), + KeyCode::LaunchMail => Some(0xE06C), + KeyCode::MediaPlayPause => Some(0xE022), + KeyCode::MediaSelect => Some(0xE06D), + KeyCode::MediaStop => Some(0xE024), + KeyCode::MediaTrackNext => Some(0xE019), + KeyCode::MediaTrackPrevious => Some(0xE010), + KeyCode::Power => Some(0xE05E), + KeyCode::AudioVolumeDown => Some(0xE02E), + KeyCode::AudioVolumeMute => Some(0xE020), + KeyCode::AudioVolumeUp => Some(0xE030), + KeyCode::Unidentified(NativeKeyCode::Windows(scancode)) => Some(scancode as u32), + _ => None, + } +} + +pub fn keycode_from_scancode(scancode: u32) -> KeyCode { + // See: https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html + // and: https://www.w3.org/TR/uievents-code/ + // and: The widget/NativeKeyToDOMCodeName.h file in the firefox source + + match scancode { + 0x0029 => KeyCode::Backquote, + 0x002B => KeyCode::Backslash, + 0x000E => KeyCode::Backspace, + 0x001A => KeyCode::BracketLeft, + 0x001B => KeyCode::BracketRight, + 0x0033 => KeyCode::Comma, + 0x000B => KeyCode::Digit0, + 0x0002 => KeyCode::Digit1, + 0x0003 => KeyCode::Digit2, + 0x0004 => KeyCode::Digit3, + 0x0005 => KeyCode::Digit4, + 0x0006 => KeyCode::Digit5, + 0x0007 => KeyCode::Digit6, + 0x0008 => KeyCode::Digit7, + 0x0009 => KeyCode::Digit8, + 0x000A => KeyCode::Digit9, + 0x000D => KeyCode::Equal, + 0x0056 => KeyCode::IntlBackslash, + 0x0073 => KeyCode::IntlRo, + 0x007D => KeyCode::IntlYen, + 0x001E => KeyCode::KeyA, + 0x0030 => KeyCode::KeyB, + 0x002E => KeyCode::KeyC, + 0x0020 => KeyCode::KeyD, + 0x0012 => KeyCode::KeyE, + 0x0021 => KeyCode::KeyF, + 0x0022 => KeyCode::KeyG, + 0x0023 => KeyCode::KeyH, + 0x0017 => KeyCode::KeyI, + 0x0024 => KeyCode::KeyJ, + 0x0025 => KeyCode::KeyK, + 0x0026 => KeyCode::KeyL, + 0x0032 => KeyCode::KeyM, + 0x0031 => KeyCode::KeyN, + 0x0018 => KeyCode::KeyO, + 0x0019 => KeyCode::KeyP, + 0x0010 => KeyCode::KeyQ, + 0x0013 => KeyCode::KeyR, + 0x001F => KeyCode::KeyS, + 0x0014 => KeyCode::KeyT, + 0x0016 => KeyCode::KeyU, + 0x002F => KeyCode::KeyV, + 0x0011 => KeyCode::KeyW, + 0x002D => KeyCode::KeyX, + 0x0015 => KeyCode::KeyY, + 0x002C => KeyCode::KeyZ, + 0x000C => KeyCode::Minus, + 0x0034 => KeyCode::Period, + 0x0028 => KeyCode::Quote, + 0x0027 => KeyCode::Semicolon, + 0x0035 => KeyCode::Slash, + 0x0038 => KeyCode::AltLeft, + 0xE038 => KeyCode::AltRight, + 0x003A => KeyCode::CapsLock, + 0xE05D => KeyCode::ContextMenu, + 0x001D => KeyCode::ControlLeft, + 0xE01D => KeyCode::ControlRight, + 0x001C => KeyCode::Enter, + 0xE05B => KeyCode::SuperLeft, + 0xE05C => KeyCode::SuperRight, + 0x002A => KeyCode::ShiftLeft, + 0x0036 => KeyCode::ShiftRight, + 0x0039 => KeyCode::Space, + 0x000F => KeyCode::Tab, + 0x0079 => KeyCode::Convert, + 0x0072 => KeyCode::Lang1, // for non-Korean layout + 0xE0F2 => KeyCode::Lang1, // for Korean layout + 0x0071 => KeyCode::Lang2, // for non-Korean layout + 0xE0F1 => KeyCode::Lang2, // for Korean layout + 0x0070 => KeyCode::KanaMode, + 0x007B => KeyCode::NonConvert, + 0xE053 => KeyCode::Delete, + 0xE04F => KeyCode::End, + 0xE047 => KeyCode::Home, + 0xE052 => KeyCode::Insert, + 0xE051 => KeyCode::PageDown, + 0xE049 => KeyCode::PageUp, + 0xE050 => KeyCode::ArrowDown, + 0xE04B => KeyCode::ArrowLeft, + 0xE04D => KeyCode::ArrowRight, + 0xE048 => KeyCode::ArrowUp, + 0xE045 => KeyCode::NumLock, + 0x0052 => KeyCode::Numpad0, + 0x004F => KeyCode::Numpad1, + 0x0050 => KeyCode::Numpad2, + 0x0051 => KeyCode::Numpad3, + 0x004B => KeyCode::Numpad4, + 0x004C => KeyCode::Numpad5, + 0x004D => KeyCode::Numpad6, + 0x0047 => KeyCode::Numpad7, + 0x0048 => KeyCode::Numpad8, + 0x0049 => KeyCode::Numpad9, + 0x004E => KeyCode::NumpadAdd, + 0x007E => KeyCode::NumpadComma, + 0x0053 => KeyCode::NumpadDecimal, + 0xE035 => KeyCode::NumpadDivide, + 0xE01C => KeyCode::NumpadEnter, + 0x0059 => KeyCode::NumpadEqual, + 0x0037 => KeyCode::NumpadMultiply, + 0x004A => KeyCode::NumpadSubtract, + 0x0001 => KeyCode::Escape, + 0x003B => KeyCode::F1, + 0x003C => KeyCode::F2, + 0x003D => KeyCode::F3, + 0x003E => KeyCode::F4, + 0x003F => KeyCode::F5, + 0x0040 => KeyCode::F6, + 0x0041 => KeyCode::F7, + 0x0042 => KeyCode::F8, + 0x0043 => KeyCode::F9, + 0x0044 => KeyCode::F10, + 0x0057 => KeyCode::F11, + 0x0058 => KeyCode::F12, + 0x0064 => KeyCode::F13, + 0x0065 => KeyCode::F14, + 0x0066 => KeyCode::F15, + 0x0067 => KeyCode::F16, + 0x0068 => KeyCode::F17, + 0x0069 => KeyCode::F18, + 0x006A => KeyCode::F19, + 0x006B => KeyCode::F20, + 0x006C => KeyCode::F21, + 0x006D => KeyCode::F22, + 0x006E => KeyCode::F23, + 0x0076 => KeyCode::F24, + 0xE037 => KeyCode::PrintScreen, + 0x0054 => KeyCode::PrintScreen, // Alt + PrintScreen + 0x0046 => KeyCode::ScrollLock, + 0x0045 => KeyCode::Pause, + 0xE046 => KeyCode::Pause, // Ctrl + Pause + 0xE06A => KeyCode::BrowserBack, + 0xE066 => KeyCode::BrowserFavorites, + 0xE069 => KeyCode::BrowserForward, + 0xE032 => KeyCode::BrowserHome, + 0xE067 => KeyCode::BrowserRefresh, + 0xE065 => KeyCode::BrowserSearch, + 0xE068 => KeyCode::BrowserStop, + 0xE06B => KeyCode::LaunchApp1, + 0xE021 => KeyCode::LaunchApp2, + 0xE06C => KeyCode::LaunchMail, + 0xE022 => KeyCode::MediaPlayPause, + 0xE06D => KeyCode::MediaSelect, + 0xE024 => KeyCode::MediaStop, + 0xE019 => KeyCode::MediaTrackNext, + 0xE010 => KeyCode::MediaTrackPrevious, + 0xE05E => KeyCode::Power, + 0xE02E => KeyCode::AudioVolumeDown, + 0xE020 => KeyCode::AudioVolumeMute, + 0xE030 => KeyCode::AudioVolumeUp, + _ => KeyCode::Unidentified(NativeKeyCode::Windows(scancode as u16)), + } +} diff --git a/src/platform_impl/windows/menu.rs b/src/platform_impl/windows/menu.rs index 11343ca87..ef3a7aae8 100644 --- a/src/platform_impl/windows/menu.rs +++ b/src/platform_impl/windows/menu.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use raw_window_handle::RawWindowHandle; -use std::{ffi::CString, os::windows::ffi::OsStrExt, sync::Mutex}; +use std::{collections::HashMap, ffi::CString, fmt, os::windows::ffi::OsStrExt, sync::Mutex}; use winapi::{ shared::{basetsd, minwindef, windef}, @@ -10,12 +10,23 @@ use winapi::{ }; use crate::{ + accelerator::Accelerator, event::{Event, WindowEvent}, + keyboard::{KeyCode, ModifiersState}, menu::{CustomMenuItem, MenuId, MenuItem, MenuType}, - platform_impl::platform::WindowId, window::WindowId as RootWindowId, }; +use super::{accelerator::register_accel, keyboard::key_to_vk, WindowId}; + +#[derive(Copy, Clone)] +struct AccelWrapper(winuser::ACCEL); +impl fmt::Debug for AccelWrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.pad(&format!("")) + } +} + const CUT_ID: usize = 5001; const COPY_ID: usize = 5002; const PASTE_ID: usize = 5003; @@ -40,7 +51,7 @@ impl MenuHandler { menu_type, } } - pub fn send_click_event(&self, menu_id: u32) { + pub fn send_click_event(&self, menu_id: u16) { (self.send_event)(Event::MenuEvent { menu_id: MenuId(menu_id), origin: self.menu_type, @@ -53,7 +64,7 @@ impl MenuHandler { } #[derive(Debug, Clone)] -pub struct MenuItemAttributes(pub(crate) u32, windef::HMENU); +pub struct MenuItemAttributes(pub(crate) u16, windef::HMENU); impl MenuItemAttributes { pub fn id(&self) -> MenuId { @@ -63,7 +74,7 @@ impl MenuItemAttributes { unsafe { winuser::EnableMenuItem( self.1, - self.0, + self.0 as u32, match enabled { true => winuser::MF_ENABLED, false => winuser::MF_DISABLED, @@ -81,14 +92,14 @@ impl MenuItemAttributes { let c_str = CString::new(title).unwrap(); info.dwTypeData = c_str.as_ptr() as _; - winuser::SetMenuItemInfoA(self.1, self.0, minwindef::FALSE, &info); + winuser::SetMenuItemInfoA(self.1, self.0 as u32, minwindef::FALSE, &info); } } pub fn set_selected(&mut self, selected: bool) { unsafe { winuser::CheckMenuItem( self.1, - self.0, + self.0 as u32, match selected { true => winuser::MF_CHECKED, false => winuser::MF_UNCHECKED, @@ -104,6 +115,7 @@ impl MenuItemAttributes { #[derive(Debug, Clone)] pub struct Menu { hmenu: windef::HMENU, + accels: HashMap, } impl Drop for Menu { @@ -127,14 +139,20 @@ impl Menu { pub fn new() -> Self { unsafe { let hmenu = winuser::CreateMenu(); - Menu { hmenu } + Menu { + hmenu, + accels: HashMap::default(), + } } } pub fn new_popup_menu() -> Menu { unsafe { let hmenu = winuser::CreatePopupMenu(); - Menu { hmenu } + Menu { + hmenu, + accels: HashMap::default(), + } } } @@ -144,11 +162,19 @@ impl Menu { hmenu } + // Get the accels table + pub(crate) fn accels(&self) -> Option> { + if self.accels.is_empty() { + return None; + } + Some(self.accels.values().cloned().map(|d| d.0).collect()) + } + pub fn add_item( &mut self, menu_id: MenuId, title: &str, - _accelerators: Option<&str>, + accelerators: Option, enabled: bool, selected: bool, _menu_type: MenuType, @@ -162,20 +188,36 @@ impl Menu { flags |= winuser::MF_CHECKED; } - // FIXME: add keyboard accelerators + let mut anno_title = title.to_string(); + // format title + if let Some(accelerators) = accelerators.clone() { + anno_title.push('\t'); + format_hotkey(accelerators, &mut anno_title); + } + winuser::AppendMenuW( self.hmenu, flags, menu_id.0 as _, - to_wstring(&title).as_mut_ptr(), + to_wstring(&anno_title).as_mut_ptr(), ); + + // add our accels + if let Some(accelerators) = accelerators { + if let Some(accelerators) = convert_accelerator(menu_id.0, accelerators) { + self.accels.insert(menu_id.0, AccelWrapper(accelerators)); + } + } MENU_IDS.lock().unwrap().push(menu_id.0 as _); CustomMenuItem(MenuItemAttributes(menu_id.0, self.hmenu)) } } - pub fn add_submenu(&mut self, title: &str, enabled: bool, submenu: Menu) { + pub fn add_submenu(&mut self, title: &str, enabled: bool, mut submenu: Menu) { unsafe { + let child_accels = std::mem::take(&mut submenu.accels); + self.accels.extend(child_accels); + let mut flags = winuser::MF_POPUP; if !enabled { flags |= winuser::MF_DISABLED; @@ -273,12 +315,17 @@ pub fn initialize( ) -> Option { if let RawWindowHandle::Windows(handle) = window_handle { let sender: *mut MenuHandler = Box::into_raw(Box::new(menu_handler)); - let menu = menu_builder.into_hmenu(); + let menu = menu_builder.clone().into_hmenu(); unsafe { commctrl::SetWindowSubclass(handle.hwnd as _, Some(subclass_proc), 0, sender as _); winuser::SetMenu(handle.hwnd as _, menu); } + + if let Some(accels) = menu_builder.accels() { + register_accel(handle.hwnd as _, &accels); + } + Some(menu) } else { None @@ -330,7 +377,7 @@ pub(crate) unsafe extern "system" fn subclass_proc( } _ => { if MENU_IDS.lock().unwrap().contains(&wparam) { - proxy.send_click_event(wparam as u32); + proxy.send_click_event(wparam as u16); } } } @@ -379,3 +426,107 @@ fn execute_edit_command(command: EditCommands) { ); } } + +// Convert a hotkey to an accelerator. +fn convert_accelerator(id: u16, key: Accelerator) -> Option { + let mut virt_key = winuser::FVIRTKEY; + let key_mods: ModifiersState = key.mods.into(); + if key_mods.control_key() { + virt_key |= winuser::FCONTROL; + } + if key_mods.alt_key() { + virt_key |= winuser::FALT; + } + if key_mods.shift_key() { + virt_key |= winuser::FSHIFT; + } + + let raw_key = if let Some(vk_code) = key_to_vk(&key.key) { + let mod_code = vk_code >> 8; + if mod_code & 0x1 != 0 { + virt_key |= winuser::FSHIFT; + } + if mod_code & 0x02 != 0 { + virt_key |= winuser::FCONTROL; + } + if mod_code & 0x04 != 0 { + virt_key |= winuser::FALT; + } + vk_code & 0x00ff + } else { + dbg!("Failed to convert key {:?} into virtual key code", key.key); + return None; + }; + + Some(winuser::ACCEL { + fVirt: virt_key, + key: raw_key as u16, + cmd: id, + }) +} + +// Format the hotkey in a Windows-native way. +fn format_hotkey(key: Accelerator, s: &mut String) { + let key_mods: ModifiersState = key.mods.into(); + if key_mods.control_key() { + s.push_str("Ctrl+"); + } + if key_mods.shift_key() { + s.push_str("Shift+"); + } + if key_mods.alt_key() { + s.push_str("Alt+"); + } + if key_mods.super_key() { + s.push_str("Windows+"); + } + match &key.key { + KeyCode::KeyA => s.push_str("A"), + KeyCode::KeyB => s.push_str("B"), + KeyCode::KeyC => s.push_str("C"), + KeyCode::KeyD => s.push_str("D"), + KeyCode::KeyE => s.push_str("E"), + KeyCode::KeyF => s.push_str("F"), + KeyCode::KeyG => s.push_str("G"), + KeyCode::KeyH => s.push_str("H"), + KeyCode::KeyI => s.push_str("I"), + KeyCode::KeyJ => s.push_str("J"), + KeyCode::KeyK => s.push_str("K"), + KeyCode::KeyL => s.push_str("L"), + KeyCode::KeyM => s.push_str("M"), + KeyCode::KeyN => s.push_str("N"), + KeyCode::KeyO => s.push_str("O"), + KeyCode::KeyP => s.push_str("P"), + KeyCode::KeyQ => s.push_str("Q"), + KeyCode::KeyR => s.push_str("R"), + KeyCode::KeyS => s.push_str("S"), + KeyCode::KeyT => s.push_str("T"), + KeyCode::KeyU => s.push_str("U"), + KeyCode::KeyV => s.push_str("V"), + KeyCode::KeyW => s.push_str("W"), + KeyCode::KeyX => s.push_str("X"), + KeyCode::KeyY => s.push_str("Y"), + KeyCode::KeyZ => s.push_str("Z"), + KeyCode::Digit0 => s.push_str("0"), + KeyCode::Digit1 => s.push_str("1"), + KeyCode::Digit2 => s.push_str("2"), + KeyCode::Digit3 => s.push_str("3"), + KeyCode::Digit4 => s.push_str("4"), + KeyCode::Digit5 => s.push_str("5"), + KeyCode::Digit6 => s.push_str("6"), + KeyCode::Digit7 => s.push_str("7"), + KeyCode::Digit8 => s.push_str("8"), + KeyCode::Digit9 => s.push_str("9"), + KeyCode::Escape => s.push_str("Esc"), + KeyCode::Delete => s.push_str("Del"), + KeyCode::Insert => s.push_str("Ins"), + KeyCode::PageUp => s.push_str("PgUp"), + KeyCode::PageDown => s.push_str("PgDn"), + // These names match LibreOffice. + KeyCode::ArrowLeft => s.push_str("Left"), + KeyCode::ArrowRight => s.push_str("Right"), + KeyCode::ArrowUp => s.push_str("Up"), + KeyCode::ArrowDown => s.push_str("Down"), + _ => s.push_str(&format!("{:?}", key.key)), + } +} diff --git a/src/platform_impl/windows/minimal_ime.rs b/src/platform_impl/windows/minimal_ime.rs new file mode 100644 index 000000000..10e6fef24 --- /dev/null +++ b/src/platform_impl/windows/minimal_ime.rs @@ -0,0 +1,93 @@ +use std::mem::MaybeUninit; + +use winapi::{ + shared::{ + minwindef::{LPARAM, WPARAM}, + windef::HWND, + }, + um::winuser, +}; + +use crate::platform_impl::platform::event_loop::ProcResult; + +pub fn is_msg_ime_related(msg_kind: u32) -> bool { + match msg_kind { + winuser::WM_IME_COMPOSITION + | winuser::WM_IME_COMPOSITIONFULL + | winuser::WM_IME_STARTCOMPOSITION + | winuser::WM_IME_ENDCOMPOSITION + | winuser::WM_IME_CHAR + | winuser::WM_CHAR + | winuser::WM_SYSCHAR => true, + _ => false, + } +} + +pub struct MinimalIme { + // True if we're currently receiving messages belonging to a finished IME session. + getting_ime_text: bool, + + utf16parts: Vec, +} +impl Default for MinimalIme { + fn default() -> Self { + MinimalIme { + getting_ime_text: false, + utf16parts: Vec::with_capacity(16), + } + } +} +impl MinimalIme { + pub(crate) fn process_message( + &mut self, + hwnd: HWND, + msg_kind: u32, + wparam: WPARAM, + _lparam: LPARAM, + result: &mut ProcResult, + ) -> Option { + match msg_kind { + winuser::WM_IME_ENDCOMPOSITION => { + self.getting_ime_text = true; + } + winuser::WM_CHAR | winuser::WM_SYSCHAR => { + if self.getting_ime_text { + *result = ProcResult::Value(0); + self.utf16parts.push(wparam as u16); + + let more_char_coming; + unsafe { + let mut next_msg = MaybeUninit::uninit(); + let has_message = winuser::PeekMessageW( + next_msg.as_mut_ptr(), + hwnd, + winuser::WM_KEYFIRST, + winuser::WM_KEYLAST, + winuser::PM_NOREMOVE, + ); + let has_message = has_message != 0; + if !has_message { + more_char_coming = false; + } else { + let next_msg = next_msg.assume_init().message; + if next_msg == winuser::WM_CHAR || next_msg == winuser::WM_SYSCHAR { + more_char_coming = true; + } else { + more_char_coming = false; + } + } + } + if !more_char_coming { + let result = String::from_utf16(&self.utf16parts).ok(); + self.utf16parts.clear(); + self.getting_ime_text = false; + return result; + } + } + } + _ => (), + } + + None + } +} diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index c09b7b17f..3b286da47 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -11,7 +11,9 @@ use winapi::{ pub use self::{ clipboard::Clipboard, event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}, + global_shortcut::{GlobalShortcut, ShortcutManager}, icon::WinIcon, + keycode::{keycode_from_scancode, keycode_to_scancode}, menu::{Menu, MenuItemAttributes}, monitor::{MonitorHandle, VideoMode}, window::Window, @@ -19,7 +21,10 @@ pub use self::{ pub use self::icon::WinIcon as PlatformIcon; -use crate::{event::DeviceId as RootDeviceId, icon::Icon, window::Theme}; +use crate::{event::DeviceId as RootDeviceId, icon::Icon, keyboard::Key, window::Theme}; +mod accelerator; +mod global_shortcut; +mod keycode; mod menu; #[cfg(feature = "tray")] @@ -108,6 +113,12 @@ fn wrap_device_id(id: u32) -> RootDeviceId { RootDeviceId(DeviceId(id)) } +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifiers: Option<&'static str>, + pub key_without_modifiers: Key<'static>, +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct WindowId(HWND); unsafe impl Send for WindowId {} @@ -127,9 +138,11 @@ mod clipboard; mod dark_mode; mod dpi; mod drop_handler; -mod event; mod event_loop; mod icon; +mod keyboard; +mod keyboard_layout; +mod minimal_ime; mod monitor; mod raw_input; mod window; diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 6e6e462d5..ef3e3e57d 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -3,6 +3,7 @@ #![cfg(target_os = "windows")] +use mem::MaybeUninit; use parking_lot::Mutex; use raw_window_handle::{windows::WindowsHandle, RawWindowHandle}; use std::{ @@ -765,6 +766,26 @@ impl Window { } } } + + #[inline] + pub fn reset_dead_keys(&self) { + // `ToUnicode` consumes the dead-key by default, so we are constructing a fake (but valid) + // key input which we can call `ToUnicode` with. + unsafe { + let vk = winuser::VK_SPACE as u32; + let scancode = winuser::MapVirtualKeyW(vk, winuser::MAPVK_VK_TO_VSC); + let kbd_state = [0; 256]; + let mut char_buff = [MaybeUninit::uninit(); 8]; + winuser::ToUnicode( + vk, + scancode, + kbd_state.as_ptr(), + char_buff[0].as_mut_ptr(), + char_buff.len() as i32, + 0, + ); + } + } } impl Drop for Window { diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index c5c598db5..34bc7dc3d 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -3,9 +3,9 @@ use crate::{ dpi::{PhysicalPosition, Size}, - event::ModifiersState, icon::Icon, - platform_impl::platform::{event_loop, util}, + keyboard::ModifiersState, + platform_impl::platform::{event_loop, keyboard::KeyEventBuilder, minimal_ime::MinimalIme, util}, window::{CursorIcon, Fullscreen, Theme, WindowAttributes}, }; use parking_lot::MutexGuard; @@ -37,6 +37,10 @@ pub struct WindowState { pub current_theme: Theme, pub preferred_theme: Option, pub high_surrogate: Option, + + pub key_event_builder: KeyEventBuilder, + pub ime_handler: MinimalIme, + pub window_flags: WindowFlags, } @@ -125,6 +129,8 @@ impl WindowState { current_theme, preferred_theme, high_surrogate: None, + key_event_builder: KeyEventBuilder::default(), + ime_handler: MinimalIme::default(), window_flags: WindowFlags::empty(), } } diff --git a/tests/serde_objects.rs b/tests/serde_objects.rs index 1f50eb715..9311e3452 100644 --- a/tests/serde_objects.rs +++ b/tests/serde_objects.rs @@ -6,10 +6,8 @@ use serde::{Deserialize, Serialize}; use tao::{ dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, - event::{ - ElementState, KeyboardInput, ModifiersState, MouseButton, MouseScrollDelta, TouchPhase, - VirtualKeyCode, - }, + event::{ElementState, MouseButton, MouseScrollDelta, TouchPhase}, + keyboard::{Key, KeyCode, KeyLocation, ModifiersState}, window::CursorIcon, }; @@ -23,12 +21,13 @@ fn window_serde() { #[test] fn events_serde() { - needs_serde::(); needs_serde::(); needs_serde::(); needs_serde::(); needs_serde::(); - needs_serde::(); + needs_serde::(); + needs_serde::(); + needs_serde::(); needs_serde::(); }