From 952edcb80467b6bf0a192e6f674c12ecf2f8da8a Mon Sep 17 00:00:00 2001 From: mmacedo Date: Tue, 23 Feb 2021 18:35:38 -0300 Subject: [PATCH 01/26] Android: Add KeyEvent handling (#1839) --- CHANGELOG.md | 1 + src/platform_impl/android/mod.rs | 30 ++++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea4fe474156..e57ea66bed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- On Android, `InputEvent::KeyEvent` is partially implemented providing the key scancode. - Added `is_maximized` method to `Window`. - On Windows, fix bug where clicking the decoration bar would make the cursor blink. - On Windows, fix bug causing newly created windows to erroneously display the "wait" (spinning) cursor. diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index a6d197b644e..0365a40e5eb 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -8,7 +8,7 @@ use crate::{ }; use ndk::{ configuration::Configuration, - event::{InputEvent, MotionAction}, + event::{InputEvent, KeyAction, MotionAction}, looper::{ForeignLooper, Poll, ThreadLooper}, }; use ndk_glue::{Event, Rect}; @@ -239,9 +239,31 @@ impl EventLoop { } } } - InputEvent::KeyEvent(_) => { - // TODO - handled = false; + InputEvent::KeyEvent(key) => { + let state = match key.action() { + KeyAction::Down => event::ElementState::Pressed, + KeyAction::Up => event::ElementState::Released, + _ => event::ElementState::Released, + }; + let event = event::Event::WindowEvent { + window_id, + event: event::WindowEvent::KeyboardInput { + device_id, + input: event::KeyboardInput { + scancode: key.scan_code() as u32, + state, + virtual_keycode: None, + modifiers: event::ModifiersState::default(), + }, + is_synthetic: false, + }, + }; + call_event_handler!( + event_handler, + self.window_target(), + control_flow, + event + ); } }; input_queue.finish_event(event, handled); From 3571dcd68cccc054b7dca4d88cd248bd529ae908 Mon Sep 17 00:00:00 2001 From: leafjolt Date: Sat, 27 Feb 2021 14:25:26 -0600 Subject: [PATCH 02/26] Update window.rs (#1871) --- src/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/window.rs b/src/window.rs index 052f5746e0a..46da474a513 100644 --- a/src/window.rs +++ b/src/window.rs @@ -526,7 +526,7 @@ impl Window { /// /// ## Platform-specific /// - /// - **iOS / Andraid / Web:** Unsupported. + /// - **iOS / Android / Web:** Unsupported. #[inline] pub fn set_max_inner_size>(&self, max_size: Option) { self.window.set_max_inner_size(max_size.map(|s| s.into())) From 4192d04a53202c199f94d1b7d883a34c9ad09272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C3=BAr=20Kov=C3=A1cs?= Date: Sat, 6 Mar 2021 11:17:23 +0100 Subject: [PATCH 03/26] Fix seg-fault when using without a window (#1874) * Fix seg-fault when using without a window #1869 * Update changelog --- CHANGELOG.md | 1 + src/platform_impl/macos/app_state.rs | 19 ++++++++++--------- src/platform_impl/macos/util/async.rs | 5 ++++- src/platform_impl/macos/window.rs | 2 +- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e57ea66bed0..5140ea6e120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- On macOS, fix segmentation fault after dropping the main window. - On Android, `InputEvent::KeyEvent` is partially implemented providing the key scancode. - Added `is_maximized` method to `Window`. - On Windows, fix bug where clicking the decoration bar would make the cursor blink. diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index 29fafbe5fa0..cf2f8280b62 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -341,9 +341,7 @@ impl AppState { unsafe { let app: id = NSApp(); let windows: id = msg_send![app, windows]; - let window: id = msg_send![windows, objectAtIndex:0]; let window_count: usize = msg_send![windows, count]; - assert_ne!(window, nil); let dialog_open = if window_count > 1 { let dialog: id = msg_send![windows, lastObject]; @@ -373,16 +371,19 @@ impl AppState { data2: 0 ]; // To stop event loop immediately, we need to post some event here. - let _: () = msg_send![window, postEvent: dummy_event atStart: YES]; + let _: () = msg_send![app, postEvent: dummy_event atStart: YES]; } pool.drain(); - let window_has_focus = msg_send![window, isKeyWindow]; - if !dialog_open && window_has_focus && dialog_is_closing { - HANDLER.dialog_is_closing.store(false, Ordering::SeqCst); - } - if dialog_open { - HANDLER.dialog_is_closing.store(true, Ordering::SeqCst); + if window_count > 0 { + let window: id = msg_send![windows, objectAtIndex:0]; + let window_has_focus = msg_send![window, isKeyWindow]; + if !dialog_open && window_has_focus && dialog_is_closing { + HANDLER.dialog_is_closing.store(false, Ordering::SeqCst); + } + if dialog_open { + HANDLER.dialog_is_closing.store(true, Ordering::SeqCst); + } } }; } diff --git a/src/platform_impl/macos/util/async.rs b/src/platform_impl/macos/util/async.rs index 96f8e5b0999..7f8e7b87145 100644 --- a/src/platform_impl/macos/util/async.rs +++ b/src/platform_impl/macos/util/async.rs @@ -207,7 +207,10 @@ pub unsafe fn set_title_async(ns_window: id, title: String) { // `close:` is thread-safe, but we want the event to be triggered from the main // thread. Though, it's a good idea to look into that more... -pub unsafe fn close_async(ns_window: id) { +// +// ArturKovacs: It's important that this operation keeps the underlying window alive +// through the `IdRef` because otherwise it would dereference free'd memory +pub unsafe fn close_async(ns_window: IdRef) { let ns_window = MainThreadSafe(ns_window); Queue::main().exec_async(move || { autoreleasepool(move || { diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 94a2f541cf2..82c1b6ae719 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -1158,7 +1158,7 @@ impl Drop for UnownedWindow { trace!("Dropping `UnownedWindow` ({:?})", self as *mut _); // Close the window if it has not yet been closed. if *self.ns_window != nil { - unsafe { util::close_async(*self.ns_window) }; + unsafe { util::close_async(self.ns_window.clone()) }; } } } From 98470393d11b3b670c1e122c26a15c563dcf68a4 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Sun, 7 Mar 2021 10:43:23 +0100 Subject: [PATCH 04/26] Add dragging window with cursor feature (#1840) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * X11 implementation. * Introduce example. * Wayland implementation. * Windows implementation. * Improve Wayland seat passing. * MacOS implementation. * Correct windows implementation per specification. * Update dependency smithay-client-toolkit from branch to master. * Fixed blocking thread in windows implementation. * Add multi-window example. * Move Wayland to a different PR. * Fix CHANGELOG. * Improve example. Co-authored-by: Markus Røyset * Rename `set_drag_window` to `begin_drag`. * Improve example. * Fix CHANGELOG. * Fix CHANGELOG. Co-authored-by: Markus Røyset * Rename to `drag_window`. * Fix typo. * Re-introduce Wayland implementation. * Fixing Wayland build. * Fixing Wayland build. * Move SCTK to 0.12.3. Co-authored-by: Markus Røyset --- CHANGELOG.md | 1 + Cargo.toml | 2 +- FEATURES.md | 1 + examples/drag_window.rs | 73 +++++++++++++++++++ src/platform_impl/android/mod.rs | 6 ++ src/platform_impl/ios/window.rs | 4 + src/platform_impl/linux/mod.rs | 5 ++ .../linux/wayland/seat/pointer/handlers.rs | 4 + .../linux/wayland/seat/pointer/mod.rs | 17 ++++- src/platform_impl/linux/wayland/window/mod.rs | 12 +++ .../linux/wayland/window/shim.rs | 12 +++ src/platform_impl/linux/x11/window.rs | 40 ++++++++++ src/platform_impl/macos/window.rs | 10 +++ src/platform_impl/web/window.rs | 5 ++ src/platform_impl/windows/window.rs | 30 +++++++- src/window.rs | 16 ++++ 16 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 examples/drag_window.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5140ea6e120..5f0b622b7bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - On Android, bump `ndk` and `ndk-glue` to 0.3: use predefined constants for event `ident`. - On Windows, fixed `WindowEvent::ThemeChanged` not properly firing and fixed `Window::theme` returning the wrong theme. - On Web, added support for `DeviceEvent::MouseMotion` to listen for relative mouse movements. +- Added `Window::drag_window`. Implemented on Windows, macOS, X11 and Wayland. # 0.24.0 (2020-12-09) diff --git a/Cargo.toml b/Cargo.toml index 64b1bee10ef..2db7219d41e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,7 @@ features = [ [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] wayland-client = { version = "0.28", features = [ "dlopen"] , optional = true } -sctk = { package = "smithay-client-toolkit", version = "0.12", optional = true } +sctk = { package = "smithay-client-toolkit", version = "0.12.3", optional = true } mio = { version = "0.6", optional = true } mio-extras = { version = "2.0", optional = true } x11-dl = { version = "2.18.5", optional = true } diff --git a/FEATURES.md b/FEATURES.md index 4ebb64291ac..8bea2043fb2 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -209,6 +209,7 @@ Legend: |Raw Device Events |▢[#750] |▢[#750] |▢[#750] |❌ |❌ |❌ |❓ | |Gamepad/Joystick events |❌[#804] |❌ |❌ |❌ |❌ |❌ |❓ | |Device movement events |❓ |❓ |❓ |❓ |❌ |❌ |❓ | +|Drag window with cursor |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A** | ### Pending API Reworks Changes in the API that have been agreed upon but aren't implemented across all platforms. diff --git a/examples/drag_window.rs b/examples/drag_window.rs new file mode 100644 index 00000000000..a408c7c7223 --- /dev/null +++ b/examples/drag_window.rs @@ -0,0 +1,73 @@ +use simple_logger::SimpleLogger; +use winit::{ + event::{ + ElementState, Event, KeyboardInput, MouseButton, StartCause, VirtualKeyCode, WindowEvent, + }, + event_loop::{ControlFlow, EventLoop}, + window::{Window, WindowBuilder, WindowId}, +}; + +fn main() { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new(); + + let window_1 = WindowBuilder::new().build(&event_loop).unwrap(); + let window_2 = WindowBuilder::new().build(&event_loop).unwrap(); + + let mut switched = false; + let mut entered_id = window_2.id(); + + event_loop.run(move |event, _, control_flow| match event { + Event::NewEvents(StartCause::Init) => { + eprintln!("Switch which window is to be dragged by pressing \"x\".") + } + Event::WindowEvent { event, window_id } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Left, + .. + } => { + let window = if (window_id == window_1.id() && switched) + || (window_id == window_2.id() && !switched) + { + &window_2 + } else { + &window_1 + }; + + window.drag_window().unwrap() + } + WindowEvent::CursorEntered { .. } => { + entered_id = window_id; + name_windows(entered_id, switched, &window_1, &window_2) + } + WindowEvent::KeyboardInput { + input: + KeyboardInput { + state: ElementState::Released, + virtual_keycode: Some(VirtualKeyCode::X), + .. + }, + .. + } => { + switched = !switched; + name_windows(entered_id, switched, &window_1, &window_2); + println!("Switched!") + } + _ => (), + }, + _ => (), + }); +} + +fn name_windows(window_id: WindowId, switched: bool, window_1: &Window, window_2: &Window) { + let (drag_target, other) = + if (window_id == window_1.id() && switched) || (window_id == window_2.id() && !switched) { + (&window_2, &window_1) + } else { + (&window_1, &window_2) + }; + drag_target.set_title("drag target"); + other.set_title("winit window"); +} diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 0365a40e5eb..543bca1b033 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -556,6 +556,12 @@ impl Window { pub fn set_cursor_visible(&self, _: bool) {} + pub fn drag_window(&self) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) + } + pub fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { let a_native_window = if let Some(native_window) = ndk_glue::native_window().as_ref() { unsafe { native_window.ptr().as_mut() as *mut _ as *mut _ } diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index 7a3665a2b9b..1f6e5591691 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -182,6 +182,10 @@ impl Inner { debug!("`Window::set_cursor_visible` is ignored on iOS") } + pub fn drag_window(&self) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + pub fn set_minimized(&self, _minimized: bool) { warn!("`Window::set_minimized` is ignored on iOS") } diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index e845b83be33..480fc8af844 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -358,6 +358,11 @@ impl Window { x11_or_wayland!(match self; Window(window) => window.set_cursor_visible(visible)) } + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(window) => window.drag_window()) + } + #[inline] pub fn scale_factor(&self) -> f64 { x11_or_wayland!(match self; Window(w) => w.scale_factor() as f64) diff --git a/src/platform_impl/linux/wayland/seat/pointer/handlers.rs b/src/platform_impl/linux/wayland/seat/pointer/handlers.rs index 7d291713fab..62b2114321a 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/handlers.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/handlers.rs @@ -4,6 +4,7 @@ use std::cell::RefCell; use std::rc::Rc; use sctk::reexports::client::protocol::wl_pointer::{self, Event as PointerEvent}; +use sctk::reexports::client::protocol::wl_seat::WlSeat; use sctk::reexports::protocols::unstable::relative_pointer::v1::client::zwp_relative_pointer_v1::Event as RelativePointerEvent; use sctk::seat::pointer::ThemedPointer; @@ -28,6 +29,7 @@ pub(super) fn handle_pointer( event: PointerEvent, pointer_data: &Rc>, winit_state: &mut WinitState, + seat: WlSeat, ) { let event_sink = &mut winit_state.event_sink; let mut pointer_data = pointer_data.borrow_mut(); @@ -59,6 +61,7 @@ pub(super) fn handle_pointer( confined_pointer: Rc::downgrade(&pointer_data.confined_pointer), pointer_constraints: pointer_data.pointer_constraints.clone(), latest_serial: pointer_data.latest_serial.clone(), + seat, }; window_handle.pointer_entered(winit_pointer); @@ -101,6 +104,7 @@ pub(super) fn handle_pointer( confined_pointer: Rc::downgrade(&pointer_data.confined_pointer), pointer_constraints: pointer_data.pointer_constraints.clone(), latest_serial: pointer_data.latest_serial.clone(), + seat, }; window_handle.pointer_left(winit_pointer); diff --git a/src/platform_impl/linux/wayland/seat/pointer/mod.rs b/src/platform_impl/linux/wayland/seat/pointer/mod.rs index 5debc8cbcf0..7ae1f251b3d 100644 --- a/src/platform_impl/linux/wayland/seat/pointer/mod.rs +++ b/src/platform_impl/linux/wayland/seat/pointer/mod.rs @@ -13,6 +13,7 @@ use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_p use sctk::reexports::protocols::unstable::pointer_constraints::v1::client::zwp_confined_pointer_v1::ZwpConfinedPointerV1; use sctk::seat::pointer::{ThemeManager, ThemedPointer}; +use sctk::window::{ConceptFrame, Window}; use crate::event::ModifiersState; use crate::platform_impl::wayland::event_loop::WinitState; @@ -35,6 +36,9 @@ pub struct WinitPointer { /// Latest observed serial in pointer events. latest_serial: Rc>, + + /// Seat. + seat: WlSeat, } impl PartialEq for WinitPointer { @@ -144,6 +148,10 @@ impl WinitPointer { confined_pointer.destroy(); } } + + pub fn drag_window(&self, window: &Window) { + window.start_interactive_move(&self.seat, self.latest_serial.get()); + } } /// A pointer wrapper for easy releasing and managing pointers. @@ -172,11 +180,18 @@ impl Pointers { pointer_constraints.clone(), modifiers_state, ))); + let pointer_seat = seat.detach(); let pointer = theme_manager.theme_pointer_with_impl( seat, move |event, pointer, mut dispatch_data| { let winit_state = dispatch_data.get::().unwrap(); - handlers::handle_pointer(pointer, event, &pointer_data, winit_state); + handlers::handle_pointer( + pointer, + event, + &pointer_data, + winit_state, + pointer_seat.clone(), + ); }, ); diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index d51f47a7a23..5f59651edbe 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -586,6 +586,18 @@ impl Window { Err(ExternalError::NotSupported(NotSupportedError::new())) } + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + let drag_window_request = WindowRequest::DragWindow; + self.window_requests + .lock() + .unwrap() + .push(drag_window_request); + self.event_loop_awakener.ping(); + + Ok(()) + } + #[inline] pub fn set_ime_position(&self, position: Position) { let scale_factor = self.scale_factor() as f64; diff --git a/src/platform_impl/linux/wayland/window/shim.rs b/src/platform_impl/linux/wayland/window/shim.rs index 59cf921a591..9397559fa13 100644 --- a/src/platform_impl/linux/wayland/window/shim.rs +++ b/src/platform_impl/linux/wayland/window/shim.rs @@ -34,6 +34,9 @@ pub enum WindowRequest { /// Grab cursor. GrabCursor(bool), + /// Drag window. + DragWindow, + /// Maximize the window. Maximize(bool), @@ -268,6 +271,12 @@ impl WindowHandle { pointer.set_cursor(Some(cursor_icon)); } } + + pub fn drag_window(&self) { + for pointer in self.pointers.iter() { + pointer.drag_window(&self.window); + } + } } #[inline] @@ -299,6 +308,9 @@ pub fn handle_window_requests(winit_state: &mut WinitState) { WindowRequest::GrabCursor(grab) => { window_handle.set_cursor_grab(grab); } + WindowRequest::DragWindow => { + window_handle.drag_window(); + } WindowRequest::Maximize(maximize) => { if maximize { window_handle.window.set_maximized(); diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 0fa64cf1644..b4ca7c409c5 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -1276,6 +1276,46 @@ impl UnownedWindow { self.set_cursor_position_physical(x, y) } + pub fn drag_window(&self) -> Result<(), ExternalError> { + let pointer = self + .xconn + .query_pointer(self.xwindow, util::VIRTUAL_CORE_POINTER) + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err))))?; + + let window = self.inner_position().map_err(ExternalError::NotSupported)?; + + let message = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_MOVERESIZE\0") }; + + // we can't use `set_cursor_grab(false)` here because it doesn't run `XUngrabPointer` + // if the cursor isn't currently grabbed + let mut grabbed_lock = self.cursor_grabbed.lock(); + unsafe { + (self.xconn.xlib.XUngrabPointer)(self.xconn.display, ffi::CurrentTime); + } + self.xconn + .flush_requests() + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err))))?; + *grabbed_lock = false; + + // we keep the lock until we are done + self.xconn + .send_client_msg( + self.xwindow, + self.root, + message, + Some(ffi::SubstructureRedirectMask | ffi::SubstructureNotifyMask), + [ + (window.x as c_long + pointer.win_x as c_long), + (window.y as c_long + pointer.win_y as c_long), + 8, // _NET_WM_MOVERESIZE_MOVE + ffi::Button1 as c_long, + 1, + ], + ) + .flush() + .map_err(|err| ExternalError::Os(os_error!(OsError::XError(err)))) + } + pub(crate) fn set_ime_position_physical(&self, x: i32, y: i32) { let _ = self .ime_sender diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 82c1b6ae719..b57fd5ff3cb 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -636,6 +636,16 @@ impl UnownedWindow { Ok(()) } + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + unsafe { + let event: id = msg_send![NSApp(), currentEvent]; + let _: () = msg_send![*self.ns_window, performWindowDragWithEvent: event]; + } + + Ok(()) + } + pub(crate) fn is_zoomed(&self) -> bool { // because `isZoomed` doesn't work if the window's borderless, // we make it resizable temporalily. diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 63e3948128b..a51210caa6f 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -222,6 +222,11 @@ impl Window { } } + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + #[inline] pub fn set_minimized(&self, _minimized: bool) { // Intentionally a no-op, as canvases cannot be 'minimized' diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index cc04b8f7902..775fa7b402a 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -14,8 +14,8 @@ use std::{ use winapi::{ ctypes::c_int, shared::{ - minwindef::{HINSTANCE, UINT}, - windef::{HWND, POINT, RECT}, + minwindef::{HINSTANCE, LPARAM, UINT, WPARAM}, + windef::{HWND, POINT, POINTS, RECT}, }, um::{ combaseapi, dwmapi, @@ -26,7 +26,7 @@ use winapi::{ oleidl::LPDROPTARGET, shobjidl_core::{CLSID_TaskbarList, ITaskbarList2}, wingdi::{CreateRectRgn, DeleteObject}, - winnt::LPCWSTR, + winnt::{LPCWSTR, SHORT}, winuser, }, }; @@ -357,6 +357,30 @@ impl Window { Ok(()) } + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + unsafe { + let points = { + let mut pos = mem::zeroed(); + winuser::GetCursorPos(&mut pos); + pos + }; + let points = POINTS { + x: points.x as SHORT, + y: points.y as SHORT, + }; + winuser::ReleaseCapture(); + winuser::PostMessageW( + self.window.0, + winuser::WM_NCLBUTTONDOWN, + winuser::HTCAPTION as WPARAM, + &points as *const _ as LPARAM, + ); + } + + Ok(()) + } + #[inline] pub fn id(&self) -> WindowId { WindowId(self.window.0) diff --git a/src/window.rs b/src/window.rs index 46da474a513..fd7f295c154 100644 --- a/src/window.rs +++ b/src/window.rs @@ -764,6 +764,22 @@ impl Window { pub fn set_cursor_visible(&self, visible: bool) { self.window.set_cursor_visible(visible) } + + /// Moves the window with the left mouse button until the button is released. + /// + /// There's no guarantee that this will work unless the left mouse button was pressed + /// immediately before this function is called. + /// + /// ## Platform-specific + /// + /// - **X11:** Un-grabs the cursor. + /// - **Wayland:** Requires the cursor to be inside the window to be dragged. + /// - **macOS:** May prevent the button release event to be triggered. + /// - **iOS / Android / Web:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn drag_window(&self) -> Result<(), ExternalError> { + self.window.drag_window() + } } /// Monitor info functions. From ffe2143d14b1c0877f6d6fb32c2f4896d30162fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C3=BAr=20Kov=C3=A1cs?= Date: Mon, 8 Mar 2021 19:56:39 +0100 Subject: [PATCH 05/26] Fix for closure-captured values not being dropped on panic (#1853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix for #1850 * Update changelog * Fix for compilation warnings * Apply suggestions from code review Co-authored-by: Markus Røyset * Improve code quality * Change Arc to Rc * Panicking in the user callback is now well defined * Address feedback * Fix nightly warning * The panic info is now not a global. * Apply suggestions from code review Co-authored-by: Francesca Lovebloom * Address feedback Co-authored-by: Markus Røyset Co-authored-by: Francesca Lovebloom --- CHANGELOG.md | 1 + Cargo.toml | 1 + src/platform_impl/macos/app_state.rs | 110 +++++++++++--------- src/platform_impl/macos/event_loop.rs | 139 ++++++++++++++++++++++++-- src/platform_impl/macos/observer.rs | 107 +++++++++++++++----- 5 files changed, 275 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f0b622b7bd..3e5000e7ce4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - On Android, unimplemented events are marked as unhandled on the native event loop. - On Windows, added `WindowBuilderExtWindows::with_menu` to set a custom menu at window creation time. - On Android, bump `ndk` and `ndk-glue` to 0.3: use predefined constants for event `ident`. +- On macOS, fix objects captured by the event loop closure not being dropped on panic. - On Windows, fixed `WindowEvent::ThemeChanged` not properly firing and fixed `Window::theme` returning the wrong theme. - On Web, added support for `DeviceEvent::MouseMotion` to listen for relative mouse movements. - Added `Window::drag_window`. Implemented on Windows, macOS, X11 and Wayland. diff --git a/Cargo.toml b/Cargo.toml index 2db7219d41e..68e85a1ca7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ cocoa = "0.24" core-foundation = "0.9" core-graphics = "0.22" dispatch = "0.2.0" +scopeguard = "1.1" [target.'cfg(target_os = "macos")'.dependencies.core-video-sys] version = "0.1.4" diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index cf2f8280b62..b72cc57b568 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -1,9 +1,10 @@ use std::{ + cell::{RefCell, RefMut}, collections::VecDeque, fmt::{self, Debug}, hint::unreachable_unchecked, mem, - rc::Rc, + rc::{Rc, Weak}, sync::{ atomic::{AtomicBool, Ordering}, Mutex, MutexGuard, @@ -12,19 +13,18 @@ use std::{ }; use cocoa::{ - appkit::{NSApp, NSEventType::NSApplicationDefined, NSWindow}, + appkit::{NSApp, NSWindow}, base::{id, nil}, - foundation::{NSAutoreleasePool, NSPoint, NSSize}, + foundation::{NSAutoreleasePool, NSSize}, }; -use objc::runtime::YES; - use crate::{ dpi::LogicalSize, event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoopWindowTarget as RootWindowTarget}, platform_impl::platform::{ event::{EventProxy, EventWrapper}, + event_loop::{post_dummy_event, PanicInfo}, observer::EventLoopWaker, util::{IdRef, Never}, window::get_window_id, @@ -52,11 +52,31 @@ pub trait EventHandler: Debug { } struct EventLoopHandler { - callback: Box, &RootWindowTarget, &mut ControlFlow)>, + callback: Weak, &RootWindowTarget, &mut ControlFlow)>>, will_exit: bool, window_target: Rc>, } +impl EventLoopHandler { + fn with_callback(&mut self, f: F) + where + F: FnOnce( + &mut EventLoopHandler, + RefMut<'_, dyn FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow)>, + ), + { + if let Some(callback) = self.callback.upgrade() { + let callback = callback.borrow_mut(); + (f)(self, callback); + } else { + panic!( + "Tried to dispatch an event, but the event loop that \ + owned the event handler callback seems to be destroyed" + ); + } + } +} + impl Debug for EventLoopHandler { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter @@ -68,23 +88,27 @@ impl Debug for EventLoopHandler { impl EventHandler for EventLoopHandler { fn handle_nonuser_event(&mut self, event: Event<'_, Never>, control_flow: &mut ControlFlow) { - (self.callback)(event.userify(), &self.window_target, control_flow); - self.will_exit |= *control_flow == ControlFlow::Exit; - if self.will_exit { - *control_flow = ControlFlow::Exit; - } + self.with_callback(|this, mut callback| { + (callback)(event.userify(), &this.window_target, control_flow); + this.will_exit |= *control_flow == ControlFlow::Exit; + if this.will_exit { + *control_flow = ControlFlow::Exit; + } + }); } fn handle_user_events(&mut self, control_flow: &mut ControlFlow) { - let mut will_exit = self.will_exit; - for event in self.window_target.p.receiver.try_iter() { - (self.callback)(Event::UserEvent(event), &self.window_target, control_flow); - will_exit |= *control_flow == ControlFlow::Exit; - if will_exit { - *control_flow = ControlFlow::Exit; + self.with_callback(|this, mut callback| { + let mut will_exit = this.will_exit; + for event in this.window_target.p.receiver.try_iter() { + (callback)(Event::UserEvent(event), &this.window_target, control_flow); + will_exit |= *control_flow == ControlFlow::Exit; + if will_exit { + *control_flow = ControlFlow::Exit; + } } - } - self.will_exit = will_exit; + this.will_exit = will_exit; + }); } } @@ -229,20 +253,12 @@ pub static INTERRUPT_EVENT_LOOP_EXIT: AtomicBool = AtomicBool::new(false); pub enum AppState {} impl AppState { - // This function extends lifetime of `callback` to 'static as its side effect - pub unsafe fn set_callback(callback: F, window_target: Rc>) - where - F: FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow), - { + pub fn set_callback( + callback: Weak, &RootWindowTarget, &mut ControlFlow)>>, + window_target: Rc>, + ) { *HANDLER.callback.lock().unwrap() = Some(Box::new(EventLoopHandler { - // This transmute is always safe, in case it was reached through `run`, since our - // lifetime will be already 'static. In other cases caller should ensure that all data - // they passed to callback will actually outlive it, some apps just can't move - // everything to event loop, so this is something that they should care about. - callback: mem::transmute::< - Box, &RootWindowTarget, &mut ControlFlow)>, - Box, &RootWindowTarget, &mut ControlFlow)>, - >(Box::new(callback)), + callback, will_exit: false, window_target, })); @@ -265,8 +281,11 @@ impl AppState { HANDLER.set_in_callback(false); } - pub fn wakeup() { - if !HANDLER.is_ready() { + pub fn wakeup(panic_info: Weak) { + let panic_info = panic_info + .upgrade() + .expect("The panic info must exist here. This failure indicates a developer error."); + if panic_info.is_panicking() || !HANDLER.is_ready() { return; } let start = HANDLER.get_start_time().unwrap(); @@ -318,8 +337,11 @@ impl AppState { HANDLER.events().append(&mut wrappers); } - pub fn cleared() { - if !HANDLER.is_ready() { + pub fn cleared(panic_info: Weak) { + let panic_info = panic_info + .upgrade() + .expect("The panic info must exist here. This failure indicates a developer error."); + if panic_info.is_panicking() || !HANDLER.is_ready() { return; } if !HANDLER.get_in_callback() { @@ -357,21 +379,9 @@ impl AppState { && !dialog_open && !dialog_is_closing { - let _: () = msg_send![app, stop: nil]; - - let dummy_event: id = msg_send![class!(NSEvent), - otherEventWithType: NSApplicationDefined - location: NSPoint::new(0.0, 0.0) - modifierFlags: 0 - timestamp: 0 - windowNumber: 0 - context: nil - subtype: 0 - data1: 0 - data2: 0 - ]; + let () = msg_send![app, stop: nil]; // To stop event loop immediately, we need to post some event here. - let _: () = msg_send![app, postEvent: dummy_event atStart: YES]; + post_dummy_event(app); } pool.drain(); diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index 5b3d96b068f..afebcd47fd7 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -1,14 +1,24 @@ use std::{ - collections::VecDeque, marker::PhantomData, mem, os::raw::c_void, process, ptr, rc::Rc, + any::Any, + cell::{Cell, RefCell}, + collections::VecDeque, + marker::PhantomData, + mem, + os::raw::c_void, + panic::{catch_unwind, resume_unwind, RefUnwindSafe, UnwindSafe}, + process, ptr, + rc::{Rc, Weak}, sync::mpsc, }; use cocoa::{ - appkit::NSApp, - base::{id, nil}, - foundation::NSAutoreleasePool, + appkit::{NSApp, NSEventType::NSApplicationDefined}, + base::{id, nil, YES}, + foundation::{NSAutoreleasePool, NSPoint}, }; +use scopeguard::defer; + use crate::{ event::Event, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootWindowTarget}, @@ -23,6 +33,34 @@ use crate::{ }, }; +#[derive(Default)] +pub struct PanicInfo { + inner: Cell>>, +} + +// WARNING: +// As long as this struct is used through its `impl`, it is UnwindSafe. +// (If `get_mut` is called on `inner`, unwind safety may get broken.) +impl UnwindSafe for PanicInfo {} +impl RefUnwindSafe for PanicInfo {} +impl PanicInfo { + pub fn is_panicking(&self) -> bool { + let inner = self.inner.take(); + let result = inner.is_some(); + self.inner.set(inner); + result + } + /// Overwrites the curret state if the current state is not panicking + pub fn set_panic(&self, p: Box) { + if !self.is_panicking() { + self.inner.set(Some(p)); + } + } + pub fn take(&self) -> Option> { + self.inner.take() + } +} + pub struct EventLoopWindowTarget { pub sender: mpsc::Sender, // this is only here to be cloned elsewhere pub receiver: mpsc::Receiver, @@ -50,6 +88,15 @@ impl EventLoopWindowTarget { pub struct EventLoop { window_target: Rc>, + panic_info: Rc, + + /// We make sure that the callback closure is dropped during a panic + /// by making the event loop own it. + /// + /// Every other reference should be a Weak reference which is only upgraded + /// into a strong reference in order to call the callback but then the + /// strong reference should be dropped as soon as possible. + _callback: Option, &RootWindowTarget, &mut ControlFlow)>>>, _delegate: IdRef, } @@ -72,12 +119,15 @@ impl EventLoop { let _: () = msg_send![pool, drain]; delegate }; - setup_control_flow_observers(); + let panic_info: Rc = Default::default(); + setup_control_flow_observers(Rc::downgrade(&panic_info)); EventLoop { window_target: Rc::new(RootWindowTarget { p: Default::default(), _marker: PhantomData, }), + panic_info, + _callback: None, _delegate: delegate, } } @@ -98,14 +148,37 @@ impl EventLoop { where F: FnMut(Event<'_, T>, &RootWindowTarget, &mut ControlFlow), { + // This transmute is always safe, in case it was reached through `run`, since our + // lifetime will be already 'static. In other cases caller should ensure that all data + // they passed to callback will actually outlive it, some apps just can't move + // everything to event loop, so this is something that they should care about. + let callback = unsafe { + mem::transmute::< + Rc, &RootWindowTarget, &mut ControlFlow)>>, + Rc, &RootWindowTarget, &mut ControlFlow)>>, + >(Rc::new(RefCell::new(callback))) + }; + + self._callback = Some(Rc::clone(&callback)); + unsafe { let pool = NSAutoreleasePool::new(nil); + defer!(pool.drain()); let app = NSApp(); assert_ne!(app, nil); - AppState::set_callback(callback, Rc::clone(&self.window_target)); - let _: () = msg_send![app, run]; + + // A bit of juggling with the callback references to make sure + // that `self.callback` is the only owner of the callback. + let weak_cb: Weak<_> = Rc::downgrade(&callback); + mem::drop(callback); + + AppState::set_callback(weak_cb, Rc::clone(&self.window_target)); + let () = msg_send![app, run]; + + if let Some(panic) = self.panic_info.take() { + resume_unwind(panic); + } AppState::exit(); - pool.drain(); } } @@ -114,6 +187,56 @@ impl EventLoop { } } +#[inline] +pub unsafe fn post_dummy_event(target: id) { + let event_class = class!(NSEvent); + let dummy_event: id = msg_send![ + event_class, + otherEventWithType: NSApplicationDefined + location: NSPoint::new(0.0, 0.0) + modifierFlags: 0 + timestamp: 0 + windowNumber: 0 + context: nil + subtype: 0 + data1: 0 + data2: 0 + ]; + let () = msg_send![target, postEvent: dummy_event atStart: YES]; +} + +/// Catches panics that happen inside `f` and when a panic +/// happens, stops the `sharedApplication` +#[inline] +pub fn stop_app_on_panic R + UnwindSafe, R>( + panic_info: Weak, + f: F, +) -> Option { + match catch_unwind(f) { + Ok(r) => Some(r), + Err(e) => { + // It's important that we set the panic before requesting a `stop` + // because some callback are still called during the `stop` message + // and we need to know in those callbacks if the application is currently + // panicking + { + let panic_info = panic_info.upgrade().unwrap(); + panic_info.set_panic(e); + } + unsafe { + let app_class = class!(NSApplication); + let app: id = msg_send![app_class, sharedApplication]; + let () = msg_send![app, stop: nil]; + + // Posting a dummy event to get `stop` to take effect immediately. + // See: https://stackoverflow.com/questions/48041279/stopping-the-nsapplication-main-event-loop/48064752#48064752 + post_dummy_event(app); + } + None + } + } +} + pub struct Proxy { sender: mpsc::Sender, source: CFRunLoopSourceRef, diff --git a/src/platform_impl/macos/observer.rs b/src/platform_impl/macos/observer.rs index aa7f5362c8d..5f92c55bfbe 100644 --- a/src/platform_impl/macos/observer.rs +++ b/src/platform_impl/macos/observer.rs @@ -1,6 +1,17 @@ -use std::{self, os::raw::*, ptr, time::Instant}; +use std::{ + self, + os::raw::*, + panic::{AssertUnwindSafe, UnwindSafe}, + ptr, + rc::Weak, + time::Instant, +}; -use crate::platform_impl::platform::{app_state::AppState, ffi}; +use crate::platform_impl::platform::{ + app_state::AppState, + event_loop::{stop_app_on_panic, PanicInfo}, + ffi, +}; #[link(name = "CoreFoundation", kind = "framework")] extern "C" { @@ -85,9 +96,20 @@ pub type CFRunLoopObserverCallBack = extern "C" fn(observer: CFRunLoopObserverRef, activity: CFRunLoopActivity, info: *mut c_void); pub type CFRunLoopTimerCallBack = extern "C" fn(timer: CFRunLoopTimerRef, info: *mut c_void); -pub enum CFRunLoopObserverContext {} pub enum CFRunLoopTimerContext {} +/// This mirrors the struct with the same name from Core Foundation. +/// https://developer.apple.com/documentation/corefoundation/cfrunloopobservercontext?language=objc +#[allow(non_snake_case)] +#[repr(C)] +pub struct CFRunLoopObserverContext { + pub version: CFIndex, + pub info: *mut c_void, + pub retain: Option *const c_void>, + pub release: Option, + pub copyDescription: Option CFStringRef>, +} + #[allow(non_snake_case)] #[repr(C)] pub struct CFRunLoopSourceContext { @@ -103,21 +125,42 @@ pub struct CFRunLoopSourceContext { pub perform: Option, } +unsafe fn control_flow_handler(panic_info: *mut c_void, f: F) +where + F: FnOnce(Weak) + UnwindSafe, +{ + let info_from_raw = Weak::from_raw(panic_info as *mut PanicInfo); + // Asserting unwind safety on this type should be fine because `PanicInfo` is + // `RefUnwindSafe` and `Rc` is `UnwindSafe` if `T` is `RefUnwindSafe`. + let panic_info = AssertUnwindSafe(Weak::clone(&info_from_raw)); + // `from_raw` takes ownership of the data behind the pointer. + // But if this scope takes ownership of the weak pointer, then + // the weak pointer will get free'd at the end of the scope. + // However we want to keep that weak reference around after the function. + std::mem::forget(info_from_raw); + + stop_app_on_panic(Weak::clone(&panic_info), move || f(panic_info.0)); +} + // begin is queued with the highest priority to ensure it is processed before other observers extern "C" fn control_flow_begin_handler( _: CFRunLoopObserverRef, activity: CFRunLoopActivity, - _: *mut c_void, + panic_info: *mut c_void, ) { - #[allow(non_upper_case_globals)] - match activity { - kCFRunLoopAfterWaiting => { - //trace!("Triggered `CFRunLoopAfterWaiting`"); - AppState::wakeup(); - //trace!("Completed `CFRunLoopAfterWaiting`"); - } - kCFRunLoopEntry => unimplemented!(), // not expected to ever happen - _ => unreachable!(), + unsafe { + control_flow_handler(panic_info, |panic_info| { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopAfterWaiting => { + //trace!("Triggered `CFRunLoopAfterWaiting`"); + AppState::wakeup(panic_info); + //trace!("Completed `CFRunLoopAfterWaiting`"); + } + kCFRunLoopEntry => unimplemented!(), // not expected to ever happen + _ => unreachable!(), + } + }); } } @@ -126,17 +169,21 @@ extern "C" fn control_flow_begin_handler( extern "C" fn control_flow_end_handler( _: CFRunLoopObserverRef, activity: CFRunLoopActivity, - _: *mut c_void, + panic_info: *mut c_void, ) { - #[allow(non_upper_case_globals)] - match activity { - kCFRunLoopBeforeWaiting => { - //trace!("Triggered `CFRunLoopBeforeWaiting`"); - AppState::cleared(); - //trace!("Completed `CFRunLoopBeforeWaiting`"); - } - kCFRunLoopExit => (), //unimplemented!(), // not expected to ever happen - _ => unreachable!(), + unsafe { + control_flow_handler(panic_info, |panic_info| { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => { + //trace!("Triggered `CFRunLoopBeforeWaiting`"); + AppState::cleared(panic_info); + //trace!("Completed `CFRunLoopBeforeWaiting`"); + } + kCFRunLoopExit => (), //unimplemented!(), // not expected to ever happen + _ => unreachable!(), + } + }); } } @@ -152,6 +199,7 @@ impl RunLoop { flags: CFOptionFlags, priority: CFIndex, handler: CFRunLoopObserverCallBack, + context: *mut CFRunLoopObserverContext, ) { let observer = CFRunLoopObserverCreate( ptr::null_mut(), @@ -159,24 +207,33 @@ impl RunLoop { ffi::TRUE, // Indicates we want this to run repeatedly priority, // The lower the value, the sooner this will run handler, - ptr::null_mut(), + context, ); CFRunLoopAddObserver(self.0, observer, kCFRunLoopCommonModes); } } -pub fn setup_control_flow_observers() { +pub fn setup_control_flow_observers(panic_info: Weak) { unsafe { + let mut context = CFRunLoopObserverContext { + info: Weak::into_raw(panic_info) as *mut _, + version: 0, + retain: None, + release: None, + copyDescription: None, + }; let run_loop = RunLoop::get(); run_loop.add_observer( kCFRunLoopEntry | kCFRunLoopAfterWaiting, CFIndex::min_value(), control_flow_begin_handler, + &mut context as *mut _, ); run_loop.add_observer( kCFRunLoopExit | kCFRunLoopBeforeWaiting, CFIndex::max_value(), control_flow_end_handler, + &mut context as *mut _, ); } } From 889258f538010d4fa222e5e897fc0affd389dccf Mon Sep 17 00:00:00 2001 From: daxpedda Date: Tue, 9 Mar 2021 17:50:15 +0100 Subject: [PATCH 06/26] Upgrade `mio` to 0.7 (#1875) * Upgrade `mio` to 0.7 Replaced `mio-extras` with `mio-misc`. * Possible improvement * Remove leftover * Wrong rebase * Fix typo --- CHANGELOG.md | 1 + Cargo.toml | 6 ++-- src/platform_impl/linux/x11/mod.rs | 47 ++++++++++----------------- src/platform_impl/linux/x11/window.rs | 2 +- 4 files changed, 22 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e5000e7ce4..cdd48cce4c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - On Windows, fixed `WindowEvent::ThemeChanged` not properly firing and fixed `Window::theme` returning the wrong theme. - On Web, added support for `DeviceEvent::MouseMotion` to listen for relative mouse movements. - Added `Window::drag_window`. Implemented on Windows, macOS, X11 and Wayland. +- On X11, bump `mio` to 0.7. # 0.24.0 (2020-12-09) diff --git a/Cargo.toml b/Cargo.toml index 68e85a1ca7d..1af7d4d23e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ targets = ["i686-pc-windows-msvc", "x86_64-pc-windows-msvc", "i686-unknown-linux default = ["x11", "wayland"] web-sys = ["web_sys", "wasm-bindgen", "instant/wasm-bindgen"] stdweb = ["std_web", "instant/stdweb"] -x11 = ["x11-dl", "mio", "mio-extras", "percent-encoding", "parking_lot"] +x11 = ["x11-dl", "mio", "mio-misc", "percent-encoding", "parking_lot"] wayland = ["wayland-client", "sctk"] [dependencies] @@ -87,8 +87,8 @@ features = [ [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] wayland-client = { version = "0.28", features = [ "dlopen"] , optional = true } sctk = { package = "smithay-client-toolkit", version = "0.12.3", optional = true } -mio = { version = "0.6", optional = true } -mio-extras = { version = "2.0", optional = true } +mio = { version = "0.7", features = ["os-ext"], optional = true } +mio-misc = { version = "1.0", optional = true } x11-dl = { version = "2.18.5", optional = true } percent-encoding = { version = "2.0", optional = true } parking_lot = { version = "0.11.0", optional = true } diff --git a/src/platform_impl/linux/x11/mod.rs b/src/platform_impl/linux/x11/mod.rs index d06936c55b8..11b5d9ecb84 100644 --- a/src/platform_impl/linux/x11/mod.rs +++ b/src/platform_impl/linux/x11/mod.rs @@ -32,15 +32,20 @@ use std::{ ptr, rc::Rc, slice, + sync::mpsc::Receiver, sync::{mpsc, Arc, Weak}, time::{Duration, Instant}, }; use libc::{self, setlocale, LC_CTYPE}; -use mio::{unix::EventedFd, Events, Poll, PollOpt, Ready, Token}; +use mio::{unix::SourceFd, Events, Interest, Poll, Token, Waker}; -use mio_extras::channel::{channel, Receiver, SendError, Sender}; +use mio_misc::{ + channel::{channel, SendError, Sender}, + queue::NotificationQueue, + NotificationId, +}; use self::{ dnd::{Dnd, DndState}, @@ -57,8 +62,7 @@ use crate::{ }; const X_TOKEN: Token = Token(0); -const USER_TOKEN: Token = Token(1); -const REDRAW_TOKEN: Token = Token(2); +const USER_REDRAW_TOKEN: Token = Token(1); pub struct EventLoopWindowTarget { xconn: Arc, @@ -180,33 +184,16 @@ impl EventLoop { mod_keymap.reset_from_x_connection(&xconn); let poll = Poll::new().unwrap(); + let waker = Arc::new(Waker::new(poll.registry(), USER_REDRAW_TOKEN).unwrap()); + let queue = Arc::new(NotificationQueue::new(waker)); + + poll.registry() + .register(&mut SourceFd(&xconn.x11_fd), X_TOKEN, Interest::READABLE) + .unwrap(); + + let (user_sender, user_channel) = channel(queue.clone(), NotificationId::gen_next()); - let (user_sender, user_channel) = channel(); - let (redraw_sender, redraw_channel) = channel(); - - poll.register( - &EventedFd(&xconn.x11_fd), - X_TOKEN, - Ready::readable(), - PollOpt::level(), - ) - .unwrap(); - - poll.register( - &user_channel, - USER_TOKEN, - Ready::readable(), - PollOpt::level(), - ) - .unwrap(); - - poll.register( - &redraw_channel, - REDRAW_TOKEN, - Ready::readable(), - PollOpt::level(), - ) - .unwrap(); + let (redraw_sender, redraw_channel) = channel(queue, NotificationId::gen_next()); let target = Rc::new(RootELW { p: super::EventLoopWindowTarget::X(EventLoopWindowTarget { diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index b4ca7c409c5..3f720f0b4a6 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -10,7 +10,7 @@ use std::{ }; use libc; -use mio_extras::channel::Sender; +use mio_misc::channel::Sender; use parking_lot::Mutex; use crate::{ From 599477d754db296016e50cb28f86f25eb7d6a4a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C3=BAr=20Kov=C3=A1cs?= Date: Wed, 10 Mar 2021 23:10:35 +0100 Subject: [PATCH 07/26] Only try publishing when a version tag is pushed (#1876) --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b7c059e6f65..20ec0fce6c1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,8 +2,8 @@ name: Publish on: push: - branches: [master] - paths: "Cargo.toml" + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 jobs: Publish: From 86748fbc681ea070bc37e90a569c192555b402cd Mon Sep 17 00:00:00 2001 From: Norbert Nemec Date: Thu, 11 Mar 2021 22:08:29 +0100 Subject: [PATCH 08/26] Fix communication of fractional RI_MOUSE_WHEEL events (Windows) (#1877) --- CHANGELOG.md | 1 + examples/mouse_wheel.rs | 48 +++++++++++++++++++++++++ src/platform_impl/windows/event_loop.rs | 5 +-- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 examples/mouse_wheel.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd48cce4c3..d155e71469f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- On Windows, fix fractional deltas for mouse wheel device events. - On macOS, fix segmentation fault after dropping the main window. - On Android, `InputEvent::KeyEvent` is partially implemented providing the key scancode. - Added `is_maximized` method to `Window`. diff --git a/examples/mouse_wheel.rs b/examples/mouse_wheel.rs new file mode 100644 index 00000000000..e61b64af8fd --- /dev/null +++ b/examples/mouse_wheel.rs @@ -0,0 +1,48 @@ +use simple_logger::SimpleLogger; +use winit::{ + event::{DeviceEvent, Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +fn main() { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new() + .with_title("Mouse Wheel events") + .build(&event_loop) + .unwrap(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + _ => (), + }, + Event::DeviceEvent { event, .. } => match event { + DeviceEvent::MouseWheel { delta } => match delta { + winit::event::MouseScrollDelta::LineDelta(x, y) => { + println!("mouse wheel Line Delta: ({},{})", x, y); + let pixels_per_line = 120.0; + let mut pos = window.outer_position().unwrap(); + pos.x -= (x * pixels_per_line) as i32; + pos.y -= (y * pixels_per_line) as i32; + window.set_outer_position(pos) + } + winit::event::MouseScrollDelta::PixelDelta(p) => { + println!("mouse wheel Pixel Delta: ({},{})", p.x, p.y); + let mut pos = window.outer_position().unwrap(); + pos.x -= p.x as i32; + pos.y -= p.y as i32; + window.set_outer_position(pos) + } + }, + _ => (), + }, + _ => (), + } + }); +} diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 5b49cbe8090..795d840dd95 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -2101,11 +2101,12 @@ unsafe extern "system" fn thread_event_target_callback( } if util::has_flag(mouse.usButtonFlags, winuser::RI_MOUSE_WHEEL) { - let delta = mouse.usButtonData as SHORT / winuser::WHEEL_DELTA; + 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 as f32), + delta: LineDelta(0.0, delta), }, }); } From 0d634a0061224a4accb51b031d1a85320d690a98 Mon Sep 17 00:00:00 2001 From: Michal Srb Date: Thu, 25 Mar 2021 21:18:51 +0300 Subject: [PATCH 09/26] Add WindowBuilder::with_outer_position (#1866) --- CHANGELOG.md | 1 + src/platform_impl/linux/x11/util/hint.rs | 18 ++++++++++++ src/platform_impl/linux/x11/util/mod.rs | 8 +++++ src/platform_impl/linux/x11/window.rs | 15 ++++++++-- src/platform_impl/macos/util/mod.rs | 13 ++++++++- src/platform_impl/macos/window.rs | 28 +++++++++--------- src/platform_impl/windows/window.rs | 4 +++ src/window.rs | 37 ++++++++++++++++++++++++ 8 files changed, 108 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d155e71469f..7e972bedb24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - On macOS, fix objects captured by the event loop closure not being dropped on panic. - On Windows, fixed `WindowEvent::ThemeChanged` not properly firing and fixed `Window::theme` returning the wrong theme. - On Web, added support for `DeviceEvent::MouseMotion` to listen for relative mouse movements. +- Added `WindowBuilder::with_position` to allow setting the position of a `Window` on creation. Supported on Windows, macOS and X11. - Added `Window::drag_window`. Implemented on Windows, macOS, X11 and Wayland. - On X11, bump `mio` to 0.7. diff --git a/src/platform_impl/linux/x11/util/hint.rs b/src/platform_impl/linux/x11/util/hint.rs index 809f3063b04..222d81748ea 100644 --- a/src/platform_impl/linux/x11/util/hint.rs +++ b/src/platform_impl/linux/x11/util/hint.rs @@ -190,6 +190,24 @@ impl<'a> NormalHints<'a> { } } + pub fn get_position(&self) -> Option<(i32, i32)> { + if has_flag(self.size_hints.flags, ffi::PPosition) { + Some((self.size_hints.x as i32, self.size_hints.y as i32)) + } else { + None + } + } + + pub fn set_position(&mut self, position: Option<(i32, i32)>) { + if let Some((x, y)) = position { + self.size_hints.flags |= ffi::PPosition; + self.size_hints.x = x as c_int; + self.size_hints.y = y as c_int; + } else { + self.size_hints.flags &= !ffi::PPosition; + } + } + // WARNING: This hint is obsolete pub fn set_size(&mut self, size: Option<(u32, u32)>) { if let Some((width, height)) = size { diff --git a/src/platform_impl/linux/x11/util/mod.rs b/src/platform_impl/linux/x11/util/mod.rs index 4bd14206551..df3d89138ab 100644 --- a/src/platform_impl/linux/x11/util/mod.rs +++ b/src/platform_impl/linux/x11/util/mod.rs @@ -23,6 +23,7 @@ pub use self::{ use std::{ mem::{self, MaybeUninit}, + ops::BitAnd, os::raw::*, ptr, }; @@ -39,6 +40,13 @@ pub fn maybe_change(field: &mut Option, value: T) -> bool { } } +pub fn has_flag(bitset: T, flag: T) -> bool +where + T: Copy + PartialEq + BitAnd, +{ + bitset & flag == flag +} + #[must_use = "This request was made asynchronously, and is still in the output buffer. You must explicitly choose to either `.flush()` (empty the output buffer, sending the request now) or `.queue()` (wait to send the request, allowing you to continue to add more requests without additional round-trips). For more information, see the documentation for `util::flush_requests`."] pub struct Flusher<'a> { xconn: &'a XConnection, diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 3f720f0b4a6..26d3b5dd1ec 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -146,6 +146,10 @@ impl UnownedWindow { .min_inner_size .map(|size| size.to_physical::(scale_factor).into()); + let position = window_attrs + .position + .map(|position| position.to_physical::(scale_factor).into()); + let dimensions = { // x11 only applies constraints when the window is actively resized // by the user, so we have to manually apply the initial constraints @@ -211,8 +215,8 @@ impl UnownedWindow { (xconn.xlib.XCreateWindow)( xconn.display, root, - 0, - 0, + position.map_or(0, |p: PhysicalPosition| p.x as c_int), + position.map_or(0, |p: PhysicalPosition| p.y as c_int), dimensions.0 as c_uint, dimensions.1 as c_uint, 0, @@ -344,6 +348,7 @@ impl UnownedWindow { } let mut normal_hints = util::NormalHints::new(xconn); + normal_hints.set_position(position.map(|PhysicalPosition { x, y }| (x, y))); normal_hints.set_size(Some(dimensions)); normal_hints.set_min_size(min_inner_size.map(Into::into)); normal_hints.set_max_size(max_inner_size.map(Into::into)); @@ -439,6 +444,12 @@ impl UnownedWindow { window .set_fullscreen_inner(window_attrs.fullscreen.clone()) .map(|flusher| flusher.queue()); + + if let Some(PhysicalPosition { x, y }) = position { + let shared_state = window.shared_state.get_mut(); + + shared_state.restore_position = Some((x, y)); + } } if window_attrs.always_on_top { window diff --git a/src/platform_impl/macos/util/mod.rs b/src/platform_impl/macos/util/mod.rs index 39a97c98322..a6187a50cec 100644 --- a/src/platform_impl/macos/util/mod.rs +++ b/src/platform_impl/macos/util/mod.rs @@ -8,11 +8,12 @@ use std::ops::{BitAnd, Deref}; use cocoa::{ appkit::{NSApp, NSWindowStyleMask}, base::{id, nil}, - foundation::{NSAutoreleasePool, NSRect, NSString, NSUInteger}, + foundation::{NSAutoreleasePool, NSPoint, NSRect, NSString, NSUInteger}, }; use core_graphics::display::CGDisplay; use objc::runtime::{Class, Object, Sel, BOOL, YES}; +use crate::dpi::LogicalPosition; use crate::platform_impl::platform::ffi; // Replace with `!` once stable @@ -91,6 +92,16 @@ pub fn bottom_left_to_top_left(rect: NSRect) -> f64 { CGDisplay::main().pixels_high() as f64 - (rect.origin.y + rect.size.height) } +/// Converts from winit screen-coordinates to macOS screen-coordinates. +/// Winit: top-left is (0, 0) and y increasing downwards +/// macOS: bottom-left is (0, 0) and y increasing upwards +pub fn window_position(position: LogicalPosition) -> NSPoint { + NSPoint::new( + position.x, + CGDisplay::main().pixels_high() as f64 - position.y, + ) +} + pub unsafe fn ns_string_id_ref(s: &str) -> IdRef { IdRef::new(NSString::alloc(nil).init_str(s)) } diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index b57fd5ff3cb..4fa85f7de6c 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -166,7 +166,17 @@ fn create_window( } None => (800.0, 600.0), }; - NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(width, height)) + let (left, bottom) = match attrs.position { + Some(position) => { + let logical = util::window_position(position.to_logical(scale_factor)); + // macOS wants the position of the bottom left corner, + // but caller is setting the position of top left corner + (logical.x, logical.y - height) + } + // This value is ignored by calling win.center() below + None => (0.0, 0.0), + }; + NSRect::new(NSPoint::new(left, bottom), NSSize::new(width, height)) } }; @@ -249,8 +259,9 @@ fn create_window( if !pl_attrs.has_shadow { ns_window.setHasShadow_(NO); } - - ns_window.center(); + if attrs.position.is_none() { + ns_window.center(); + } ns_window }); pool.drain(); @@ -496,17 +507,8 @@ impl UnownedWindow { pub fn set_outer_position(&self, position: Position) { let scale_factor = self.scale_factor(); let position = position.to_logical(scale_factor); - let dummy = NSRect::new( - NSPoint::new( - position.x, - // While it's true that we're setting the top-left position, - // it still needs to be in a bottom-left coordinate system. - CGDisplay::main().pixels_high() as f64 - position.y, - ), - NSSize::new(0f64, 0f64), - ); unsafe { - util::set_frame_top_left_point_async(*self.ns_window, dummy.origin); + util::set_frame_top_left_point_async(*self.ns_window, util::window_position(position)); } } diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 775fa7b402a..21249593eb3 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -832,6 +832,10 @@ unsafe fn init( force_window_active(win.window.0); } + if let Some(position) = attributes.position { + win.set_outer_position(position); + } + Ok(win) } diff --git a/src/window.rs b/src/window.rs index fd7f295c154..079c7112401 100644 --- a/src/window.rs +++ b/src/window.rs @@ -116,6 +116,31 @@ pub struct WindowAttributes { /// The default is `None`. pub max_inner_size: Option, + /// The desired position of the window. If this is `None`, some platform-specific position + /// will be chosen. + /// + /// The default is `None`. + /// + /// ## Platform-specific + /// + /// - **macOS**: The top left corner position of the window content, the window's "inner" + /// position. The window title bar will be placed above it. + /// The window will be positioned such that it fits on screen, maintaining + /// set `inner_size` if any. + /// If you need to precisely position the top left corner of the whole window you have to + /// use [`Window::set_outer_position`] after creating the window. + /// - **Windows**: The top left corner position of the window title bar, the window's "outer" + /// position. + /// There may be a small gap between this position and the window due to the specifics of the + /// Window Manager. + /// - **X11**: The top left corner of the window, the window's "outer" position. + /// - **Others**: Ignored. + /// + /// See [`Window::set_outer_position`]. + /// + /// [`Window::set_outer_position`]: crate::window::Window::set_outer_position + pub position: Option, + /// Whether the window is resizable or not. /// /// The default is `true`. @@ -170,6 +195,7 @@ impl Default for WindowAttributes { inner_size: None, min_inner_size: None, max_inner_size: None, + position: None, resizable: true, title: "winit window".to_owned(), maximized: false, @@ -223,6 +249,17 @@ impl WindowBuilder { self } + /// Sets a desired initial position for the window. + /// + /// See [`WindowAttributes::position`] for details. + /// + /// [`WindowAttributes::position`]: crate::window::WindowAttributes::position + #[inline] + pub fn with_position>(mut self, position: P) -> Self { + self.window.position = Some(position.into()); + self + } + /// Sets whether the window is resizable or not. /// /// See [`Window::set_resizable`] for details. From ca9c05368e76f70a06cdc34b864711584e5744c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20R=C3=B8yset?= Date: Tue, 30 Mar 2021 21:27:32 +0200 Subject: [PATCH 10/26] Fix CI warnings (#1898) * Fix CI warnings * Use the panic! macro rather than format! + panic_any --- src/platform/windows.rs | 2 +- src/platform_impl/android/mod.rs | 1 + src/platform_impl/linux/mod.rs | 3 +-- src/platform_impl/linux/x11/mod.rs | 2 +- src/platform_impl/linux/x11/util/atom.rs | 3 +-- src/platform_impl/web/stdweb/canvas.rs | 1 + src/platform_impl/web/web_sys/canvas.rs | 1 + src/platform_impl/web/web_sys/canvas/mouse_handler.rs | 1 + src/platform_impl/web/web_sys/canvas/pointer_handler.rs | 1 + 9 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index dd58d758cd9..f2752aa0bda 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -133,7 +133,7 @@ pub trait WindowBuilderExtWindows { /// `COINIT_APARTMENTTHREADED`) on the same thread. Note that winit may still attempt to initialize /// COM API regardless of this option. Currently only fullscreen mode does that, but there may be more in the future. /// If you need COM API with `COINIT_MULTITHREADED` you must initialize it before calling any winit functions. - /// See https://docs.microsoft.com/en-us/windows/win32/api/objbase/nf-objbase-coinitialize#remarks for more information. + /// See for more information. fn with_drag_and_drop(self, flag: bool) -> WindowBuilder; /// Forces a theme or uses the system settings if `None` was provided. diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 543bca1b033..4dc3bb4affc 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -245,6 +245,7 @@ impl EventLoop { KeyAction::Up => event::ElementState::Released, _ => event::ElementState::Released, }; + #[allow(deprecated)] let event = event::Event::WindowEvent { window_id, event: event::WindowEvent::KeyboardInput { diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 480fc8af844..3bf2b04744b 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -609,11 +609,10 @@ impl EventLoop { #[cfg(not(feature = "x11"))] let x11_err = "backend disabled"; - let err_string = format!( + panic!( "Failed to initialize any backend! Wayland status: {:?} X11 status: {:?}", wayland_err, x11_err, ); - panic!(err_string); } #[cfg(feature = "wayland")] diff --git a/src/platform_impl/linux/x11/mod.rs b/src/platform_impl/linux/x11/mod.rs index 11b5d9ecb84..a784e8b2f10 100644 --- a/src/platform_impl/linux/x11/mod.rs +++ b/src/platform_impl/linux/x11/mod.rs @@ -135,7 +135,7 @@ impl EventLoop { let ime = RefCell::new({ let result = Ime::new(Arc::clone(&xconn)); if let Err(ImeCreationError::OpenFailure(ref state)) = result { - panic!(format!("Failed to open input method: {:#?}", state)); + panic!("Failed to open input method: {:#?}", state); } result.expect("Failed to set input method destruction callback") }); diff --git a/src/platform_impl/linux/x11/util/atom.rs b/src/platform_impl/linux/x11/util/atom.rs index c311c5d237e..41387224835 100644 --- a/src/platform_impl/linux/x11/util/atom.rs +++ b/src/platform_impl/linux/x11/util/atom.rs @@ -27,12 +27,11 @@ impl XConnection { (self.xlib.XInternAtom)(self.display, name.as_ptr() as *const c_char, ffi::False) }; if atom == 0 { - let msg = format!( + panic!( "`XInternAtom` failed, which really shouldn't happen. Atom: {:?}, Error: {:#?}", name, self.check_errors(), ); - panic!(msg); } /*println!( "XInternAtom name:{:?} atom:{:?}", diff --git a/src/platform_impl/web/stdweb/canvas.rs b/src/platform_impl/web/stdweb/canvas.rs index 5bc8991f7f2..cea976e2379 100644 --- a/src/platform_impl/web/stdweb/canvas.rs +++ b/src/platform_impl/web/stdweb/canvas.rs @@ -17,6 +17,7 @@ use stdweb::web::event::{ use stdweb::web::html_element::CanvasElement; use stdweb::web::{document, EventListenerHandle, IElement, IEventTarget, IHtmlElement}; +#[allow(dead_code)] pub struct Canvas { /// Note: resizing the CanvasElement should go through `backend::set_canvas_size` to ensure the DPI factor is maintained. raw: CanvasElement, diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 8013e7b1607..9725e8b6b81 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -18,6 +18,7 @@ use web_sys::{ mod mouse_handler; mod pointer_handler; +#[allow(dead_code)] pub struct Canvas { common: Common, on_focus: Option>, diff --git a/src/platform_impl/web/web_sys/canvas/mouse_handler.rs b/src/platform_impl/web/web_sys/canvas/mouse_handler.rs index 65450980b81..81e2aead028 100644 --- a/src/platform_impl/web/web_sys/canvas/mouse_handler.rs +++ b/src/platform_impl/web/web_sys/canvas/mouse_handler.rs @@ -8,6 +8,7 @@ use std::rc::Rc; use web_sys::{EventTarget, MouseEvent}; +#[allow(dead_code)] pub(super) struct MouseHandler { on_mouse_leave: Option>, on_mouse_enter: Option>, diff --git a/src/platform_impl/web/web_sys/canvas/pointer_handler.rs b/src/platform_impl/web/web_sys/canvas/pointer_handler.rs index 046732f713c..5f6f3210166 100644 --- a/src/platform_impl/web/web_sys/canvas/pointer_handler.rs +++ b/src/platform_impl/web/web_sys/canvas/pointer_handler.rs @@ -5,6 +5,7 @@ use crate::event::{ModifiersState, MouseButton}; use web_sys::PointerEvent; +#[allow(dead_code)] pub(super) struct PointerHandler { on_cursor_leave: Option>, on_cursor_enter: Option>, From 0487876826819936f52f26d192627bd552867886 Mon Sep 17 00:00:00 2001 From: Aleksandr Ovchinnikov Date: Tue, 6 Apr 2021 10:19:25 +0300 Subject: [PATCH 11/26] On macOS, wake up the event loop immediately when a redraw is requested. (#1812) We allow to have RunLoop running only on the main thread. Which means if we call Window::request_redraw() from other the thread then we have to wait until some other event arrives on the main thread. That situation is even worse when we have ControlFlow set to the `Wait` mode then user will not ever render anything. --- CHANGELOG.md | 1 + src/platform_impl/macos/app_state.rs | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e972bedb24..13a8b4c01ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added `is_maximized` method to `Window`. - On Windows, fix bug where clicking the decoration bar would make the cursor blink. - On Windows, fix bug causing newly created windows to erroneously display the "wait" (spinning) cursor. +- On macOS, wake up the event loop immediately when a redraw is requested. - On Windows, change the default window size (1024x768) to match the default on other desktop platforms (800x600). - On Windows, fix bug causing mouse capture to not be released. - On Windows, fix fullscreen not preserving minimized/maximized state. diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index b72cc57b568..fb67b4d6dbb 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -25,7 +25,7 @@ use crate::{ platform_impl::platform::{ event::{EventProxy, EventWrapper}, event_loop::{post_dummy_event, PanicInfo}, - observer::EventLoopWaker, + observer::{CFRunLoopGetMain, CFRunLoopWakeUp, EventLoopWaker}, util::{IdRef, Never}, window::get_window_id, }, @@ -321,6 +321,10 @@ impl AppState { if !pending_redraw.contains(&window_id) { pending_redraw.push(window_id); } + unsafe { + let rl = CFRunLoopGetMain(); + CFRunLoopWakeUp(rl); + } } pub fn queue_event(wrapper: EventWrapper) { From ba704c4eb49638da7516250d26adee35ee0bc9a4 Mon Sep 17 00:00:00 2001 From: Xiaopeng Li Date: Tue, 6 Apr 2021 15:22:38 +0800 Subject: [PATCH 12/26] Mac: Redraw immediately to prevent shaking on window resize (#1901) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Mac: Redraw immediately to prevent shaking on window resize * Update CHANGELOG.md * Update CHANGELOG.md Co-authored-by: 李小鹏 Co-authored-by: Markus Røyset --- CHANGELOG.md | 1 + src/platform_impl/macos/app_state.rs | 4 ++++ src/platform_impl/macos/view.rs | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13a8b4c01ae..662a58f17df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Added `WindowBuilder::with_position` to allow setting the position of a `Window` on creation. Supported on Windows, macOS and X11. - Added `Window::drag_window`. Implemented on Windows, macOS, X11 and Wayland. - On X11, bump `mio` to 0.7. +- On macOS, emit `RedrawRequested` events immediately while the window is being resized. # 0.24.0 (2020-12-09) diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index fb67b4d6dbb..59cf0f2aaca 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -327,6 +327,10 @@ impl AppState { } } + pub fn handle_redraw(window_id: WindowId) { + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::RedrawRequested(window_id))); + } + pub fn queue_event(wrapper: EventWrapper) { if !unsafe { msg_send![class!(NSThread), isMainThread] } { panic!("Event queued from different thread: {:#?}", wrapper); diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index 523cb665d57..aa7109f8e17 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -346,7 +346,7 @@ extern "C" fn draw_rect(this: &Object, _sel: Sel, rect: NSRect) { let state_ptr: *mut c_void = *this.get_ivar("winitState"); let state = &mut *(state_ptr as *mut ViewState); - AppState::queue_redraw(WindowId(get_window_id(state.ns_window))); + AppState::handle_redraw(WindowId(get_window_id(state.ns_window))); let superclass = util::superclass(this); let () = msg_send![super(this, superclass), drawRect: rect]; From 629cd86c7cc674c40c1f20b992bdd13784f357dd Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 7 Apr 2021 22:24:49 +0200 Subject: [PATCH 13/26] Stop calling NSApplication.finishLaunching on window creation (#1902) This is called internally by NSApplication.run, and is not something we should call - I couldn't find the reasoning behind this being there in the first place, git blame reveals c38110cac from 2014, so probably a piece of legacy code. Removing this fixes creating new windows when you have assigned a main menu to the application. --- CHANGELOG.md | 1 + src/platform_impl/macos/window.rs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 662a58f17df..5f18446b78f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- On macOS, fix creating new windows when the application has a main menu. - On Windows, fix fractional deltas for mouse wheel device events. - On macOS, fix segmentation fault after dropping the main window. - On Android, `InputEvent::KeyEvent` is partially implemented providing the key scancode. diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 4fa85f7de6c..11c652c5589 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -100,13 +100,13 @@ fn create_app(activation_policy: ActivationPolicy) -> Option { if ns_app == nil { None } else { + // TODO: Move ActivationPolicy from an attribute on the window to something on the EventLoop use self::NSApplicationActivationPolicy::*; ns_app.setActivationPolicy_(match activation_policy { ActivationPolicy::Regular => NSApplicationActivationPolicyRegular, ActivationPolicy::Accessory => NSApplicationActivationPolicyAccessory, ActivationPolicy::Prohibited => NSApplicationActivationPolicyProhibited, }); - ns_app.finishLaunching(); Some(ns_app) } } @@ -357,6 +357,7 @@ impl UnownedWindow { panic!("Windows can only be created on the main thread on macOS"); } } + trace!("Creating new window"); let pool = unsafe { NSAutoreleasePool::new(nil) }; From dabcb1834d4f2c6761854d911ec17f1f01e3cc10 Mon Sep 17 00:00:00 2001 From: Rodrigodd <51273772+Rodrigodd@users.noreply.github.com> Date: Sat, 10 Apr 2021 10:47:19 -0300 Subject: [PATCH 14/26] On Windows, allow the creation of popup window (#1895) Add with_owner_window to WindowBuilderExtWindows. Add set_enable to WindowExtWindows. --- CHANGELOG.md | 2 + src/platform/windows.rs | 48 ++++++++++++++++++++++- src/platform_impl/windows/mod.rs | 11 +++++- src/platform_impl/windows/window.rs | 26 ++++++++---- src/platform_impl/windows/window_state.rs | 4 ++ 5 files changed, 80 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f18446b78f..d88de9d9fd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ - Added `WindowBuilder::with_position` to allow setting the position of a `Window` on creation. Supported on Windows, macOS and X11. - Added `Window::drag_window`. Implemented on Windows, macOS, X11 and Wayland. - On X11, bump `mio` to 0.7. +- On Windows, added `WindowBuilderExtWindows::with_owner_window` to allow creating popup windows. +- On Windows, added `WindowExtWindows::set_enable` to allow creating modal popup windows. - On macOS, emit `RedrawRequested` events immediately while the window is being resized. # 0.24.0 (2020-12-09) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index f2752aa0bda..83a33c63732 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -12,7 +12,7 @@ use crate::{ event::DeviceId, event_loop::EventLoop, monitor::MonitorHandle, - platform_impl::{EventLoop as WindowsEventLoop, WinIcon}, + platform_impl::{EventLoop as WindowsEventLoop, Parent, WinIcon}, window::{BadIcon, Icon, Theme, Window, WindowBuilder}, }; @@ -78,6 +78,21 @@ pub trait WindowExtWindows { /// The pointer will become invalid when the native window was destroyed. fn hwnd(&self) -> *mut libc::c_void; + /// Enables or disables mouse and keyboard input to the specified window. + /// + /// A window must be enabled before it can be activated. + /// If an application has create a modal dialog box by disabling its owner window + /// (as described in [`WindowBuilderExtWindows::with_owner_window`]), the application must enable + /// the owner window before destroying the dialog box. + /// Otherwise, another window will receive the keyboard focus and be activated. + /// + /// If a child window is disabled, it is ignored when the system tries to determine which + /// window should receive mouse messages. + /// + /// For more information, see + /// and + fn set_enable(&self, enabled: bool); + /// This sets `ICON_BIG`. A good ceiling here is 256x256. fn set_taskbar_icon(&self, taskbar_icon: Option); @@ -96,6 +111,13 @@ impl WindowExtWindows for Window { self.window.hwnd() as *mut _ } + #[inline] + fn set_enable(&self, enabled: bool) { + unsafe { + winapi::um::winuser::EnableWindow(self.hwnd() as _, enabled as _); + } + } + #[inline] fn set_taskbar_icon(&self, taskbar_icon: Option) { self.window.set_taskbar_icon(taskbar_icon) @@ -110,8 +132,24 @@ impl WindowExtWindows for Window { /// Additional methods on `WindowBuilder` that are specific to Windows. pub trait WindowBuilderExtWindows { /// Sets a parent to the window to be created. + /// + /// A child window has the WS_CHILD style and is confined to the client area of its parent window. + /// + /// For more information, see fn with_parent_window(self, parent: HWND) -> WindowBuilder; + /// Set an owner to the window to be created. Can be used to create a dialog box, for example. + /// Can be used in combination with [`WindowExtWindows::set_enable(false)`](WindowExtWindows::set_enable) + /// on the owner window to create a modal dialog box. + /// + /// From MSDN: + /// - An owned window is always above its owner in the z-order. + /// - The system automatically destroys an owned window when its owner is destroyed. + /// - An owned window is hidden when its owner is minimized. + /// + /// For more information, see + fn with_owner_window(self, parent: HWND) -> WindowBuilder; + /// Sets a menu on the window to be created. /// /// Parent and menu are mutually exclusive; a child window cannot have a menu! @@ -143,7 +181,13 @@ pub trait WindowBuilderExtWindows { impl WindowBuilderExtWindows for WindowBuilder { #[inline] fn with_parent_window(mut self, parent: HWND) -> WindowBuilder { - self.platform_specific.parent = Some(parent); + self.platform_specific.parent = Parent::ChildOf(parent); + self + } + + #[inline] + fn with_owner_window(mut self, parent: HWND) -> WindowBuilder { + self.platform_specific.parent = Parent::OwnedBy(parent); self } diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index d748e6f01f7..07629e6dfb8 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -15,9 +15,16 @@ use crate::event::DeviceId as RootDeviceId; use crate::icon::Icon; use crate::window::Theme; +#[derive(Clone)] +pub enum Parent { + None, + ChildOf(HWND), + OwnedBy(HWND), +} + #[derive(Clone)] pub struct PlatformSpecificWindowBuilderAttributes { - pub parent: Option, + pub parent: Parent, pub menu: Option, pub taskbar_icon: Option, pub no_redirection_bitmap: bool, @@ -28,7 +35,7 @@ pub struct PlatformSpecificWindowBuilderAttributes { impl Default for PlatformSpecificWindowBuilderAttributes { fn default() -> Self { Self { - parent: None, + parent: Parent::None, menu: None, taskbar_icon: None, no_redirection_bitmap: false, diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 21249593eb3..2e5e8e57278 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -44,7 +44,7 @@ use crate::{ icon::{self, IconType}, monitor, util, window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState}, - PlatformSpecificWindowBuilderAttributes, WindowId, + Parent, PlatformSpecificWindowBuilderAttributes, WindowId, }, window::{CursorIcon, Fullscreen, Theme, UserAttentionType, WindowAttributes}, }; @@ -733,12 +733,24 @@ unsafe fn init( window_flags.set(WindowFlags::TRANSPARENT, attributes.transparent); // WindowFlags::VISIBLE and MAXIMIZED are set down below after the window has been configured. window_flags.set(WindowFlags::RESIZABLE, attributes.resizable); - window_flags.set(WindowFlags::CHILD, pl_attribs.parent.is_some()); - window_flags.set(WindowFlags::ON_TASKBAR, true); - if pl_attribs.parent.is_some() && pl_attribs.menu.is_some() { - warn!("Setting a menu on windows that have a parent is unsupported"); - } + let parent = match pl_attribs.parent { + Parent::ChildOf(parent) => { + window_flags.set(WindowFlags::CHILD, true); + if pl_attribs.menu.is_some() { + warn!("Setting a menu on a child window is unsupported"); + } + Some(parent) + } + Parent::OwnedBy(parent) => { + window_flags.set(WindowFlags::POPUP, true); + Some(parent) + } + Parent::None => { + window_flags.set(WindowFlags::ON_TASKBAR, true); + None + } + }; // creating the real window this time, by using the functions in `extra_functions` let real_window = { @@ -752,7 +764,7 @@ unsafe fn init( winuser::CW_USEDEFAULT, winuser::CW_USEDEFAULT, winuser::CW_USEDEFAULT, - pl_attribs.parent.unwrap_or(ptr::null_mut()), + parent.unwrap_or(ptr::null_mut()), pl_attribs.menu.unwrap_or(ptr::null_mut()), libloaderapi::GetModuleHandleW(ptr::null()), ptr::null_mut(), diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index 9ef55232390..9e41bd7925f 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -68,6 +68,7 @@ bitflags! { const TRANSPARENT = 1 << 6; const CHILD = 1 << 7; const MAXIMIZED = 1 << 8; + const POPUP = 1 << 14; /// Marker flag for fullscreen. Should always match `WindowState::fullscreen`, but is /// included here to make masking easier. @@ -213,6 +214,9 @@ impl WindowFlags { if self.contains(WindowFlags::CHILD) { style |= WS_CHILD; // This is incompatible with WS_POPUP if that gets added eventually. } + if self.contains(WindowFlags::POPUP) { + style |= WS_POPUP; + } if self.contains(WindowFlags::MINIMIZED) { style |= WS_MINIMIZE; } From 04b4e482659366b042ea2cae462e45f72271438c Mon Sep 17 00:00:00 2001 From: LoganDark Date: Mon, 12 Apr 2021 14:12:39 -0700 Subject: [PATCH 15/26] Derive `Default`, `Hash`, and `Eq` for some dpi types (#1833) * Derive more things * Changelog entry --- CHANGELOG.md | 1 + src/dpi.rs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d88de9d9fd9..a14bc047d4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - On Windows, added `WindowBuilderExtWindows::with_owner_window` to allow creating popup windows. - On Windows, added `WindowExtWindows::set_enable` to allow creating modal popup windows. - On macOS, emit `RedrawRequested` events immediately while the window is being resized. +- Implement `Default`, `Hash`, and `Eq` for `LogicalPosition`, `PhysicalPosition`, `LogicalSize`, and `PhysicalSize`. # 0.24.0 (2020-12-09) diff --git a/src/dpi.rs b/src/dpi.rs index ed30abb79b6..ff0b0c95794 100644 --- a/src/dpi.rs +++ b/src/dpi.rs @@ -163,7 +163,7 @@ pub fn validate_scale_factor(scale_factor: f64) -> bool { /// The position is stored as floats, so please be careful. Casting floats to integers truncates the /// fractional part, which can cause noticable issues. To help with that, an `Into<(i32, i32)>` /// implementation is provided which does the rounding for you. -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct LogicalPosition

{ pub x: P, @@ -228,7 +228,7 @@ impl Into<[X; 2]> for LogicalPosition

{ } /// A position represented in physical pixels. -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct PhysicalPosition

{ pub x: P, @@ -293,7 +293,7 @@ impl Into<[X; 2]> for PhysicalPosition

{ } /// A size represented in logical pixels. -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct LogicalSize

{ pub width: P, @@ -358,7 +358,7 @@ impl Into<[X; 2]> for LogicalSize

{ } /// A size represented in physical pixels. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct PhysicalSize

{ pub width: P, From 1c4d6e7613c3a3870cecb4cfa0eecc97409d45ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C3=BAr=20Kov=C3=A1cs?= Date: Tue, 13 Apr 2021 21:31:41 +0200 Subject: [PATCH 16/26] Correct the false documentation about macOS dpi (#1905) --- src/dpi.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/dpi.rs b/src/dpi.rs index ff0b0c95794..35458452f4f 100644 --- a/src/dpi.rs +++ b/src/dpi.rs @@ -69,9 +69,10 @@ //! selection of "nice" scale factors, i.e. 1.0, 1.25, 1.5... on Windows 7, the scale factor is //! global and changing it requires logging out. See [this article][windows_1] for technical //! details. -//! - **macOS:** "retina displays" have a scale factor of 2.0. Otherwise, the scale factor is 1.0. -//! Intermediate scale factors are never used. It's possible for any display to use that 2.0 scale -//! factor, given the use of the command line. +//! - **macOS:** Recent versions of macOS allow the user to change the scaling factor for certain +//! displays. When this is available, the user may pick a per-monitor scaling factor from a set +//! of pre-defined settings. All "retina displays" have a scaling factor above 1.0 by default but +//! the specific value varies across devices. //! - **X11:** Many man-hours have been spent trying to figure out how to handle DPI in X11. Winit //! currently uses a three-pronged approach: //! + Use the value in the `WINIT_X11_SCALE_FACTOR` environment variable, if present. From e8cdf8b092ed3ed2c1dae95606664a30168d4028 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm <2248455+casperstorm@users.noreply.github.com> Date: Sat, 24 Apr 2021 16:56:46 +0200 Subject: [PATCH 17/26] Add MacOS menu (#1583) * feat: added MacOS menu * fix: ran fmt * extracted function into variable * idiomatic formatting * Set the default menu only during app startup * Don't set the activation policy in the menu init Co-authored-by: Artur Kovacs --- CHANGELOG.md | 1 + src/platform_impl/macos/app_state.rs | 4 + src/platform_impl/macos/menu.rs | 117 +++++++++++++++++++++++++++ src/platform_impl/macos/mod.rs | 1 + 4 files changed, 123 insertions(+) create mode 100644 src/platform_impl/macos/menu.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a14bc047d4b..8e62cb684e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - On Windows, added `WindowExtWindows::set_enable` to allow creating modal popup windows. - On macOS, emit `RedrawRequested` events immediately while the window is being resized. - Implement `Default`, `Hash`, and `Eq` for `LogicalPosition`, `PhysicalPosition`, `LogicalSize`, and `PhysicalSize`. +- On macOS, initialize the Menu Bar with minimal defaults. # 0.24.0 (2020-12-09) diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index 59cf0f2aaca..4f20e21694c 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -25,6 +25,7 @@ use crate::{ platform_impl::platform::{ event::{EventProxy, EventWrapper}, event_loop::{post_dummy_event, PanicInfo}, + menu, observer::{CFRunLoopGetMain, CFRunLoopWakeUp, EventLoopWaker}, util::{IdRef, Never}, window::get_window_id, @@ -274,6 +275,9 @@ impl AppState { pub fn launched() { HANDLER.set_ready(); HANDLER.waker().start(); + // The menubar initialization should be before the `NewEvents` event, to allow overriding + // of the default menu in the event + menu::initialize(); HANDLER.set_in_callback(true); HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::NewEvents( StartCause::Init, diff --git a/src/platform_impl/macos/menu.rs b/src/platform_impl/macos/menu.rs new file mode 100644 index 00000000000..a0d61edd741 --- /dev/null +++ b/src/platform_impl/macos/menu.rs @@ -0,0 +1,117 @@ +use cocoa::appkit::{ + NSApp, NSApplication, NSApplicationActivationPolicyRegular, NSEventModifierFlags, NSMenu, + NSMenuItem, +}; +use cocoa::base::{nil, selector}; +use cocoa::foundation::{NSAutoreleasePool, NSProcessInfo, NSString}; +use objc::{ + rc::autoreleasepool, + runtime::{Object, Sel}, +}; + +struct KeyEquivalent<'a> { + key: &'a str, + masks: Option, +} + +pub fn initialize() { + autoreleasepool(|| unsafe { + let menubar = NSMenu::new(nil).autorelease(); + let app_menu_item = NSMenuItem::new(nil).autorelease(); + menubar.addItem_(app_menu_item); + let app = NSApp(); + app.setMainMenu_(menubar); + + let app_menu = NSMenu::new(nil); + let process_name = NSProcessInfo::processInfo(nil).processName(); + + // About menu item + let about_item_prefix = NSString::alloc(nil).init_str("About "); + let about_item_title = about_item_prefix.stringByAppendingString_(process_name); + let about_item = menu_item( + about_item_title, + selector("orderFrontStandardAboutPanel:"), + None, + ); + + // Seperator menu item + let sep_first = NSMenuItem::separatorItem(nil); + + // Hide application menu item + let hide_item_prefix = NSString::alloc(nil).init_str("Hide "); + let hide_item_title = hide_item_prefix.stringByAppendingString_(process_name); + let hide_item = menu_item( + hide_item_title, + selector("hide:"), + Some(KeyEquivalent { + key: "h", + masks: None, + }), + ); + + // Hide other applications menu item + let hide_others_item_title = NSString::alloc(nil).init_str("Hide Others"); + let hide_others_item = menu_item( + hide_others_item_title, + selector("hideOtherApplications:"), + Some(KeyEquivalent { + key: "h", + masks: Some( + NSEventModifierFlags::NSAlternateKeyMask + | NSEventModifierFlags::NSCommandKeyMask, + ), + }), + ); + + // Show applications menu item + let show_all_item_title = NSString::alloc(nil).init_str("Show All"); + let show_all_item = menu_item( + show_all_item_title, + selector("unhideAllApplications:"), + None, + ); + + // Seperator menu item + let sep = NSMenuItem::separatorItem(nil); + + // Quit application menu item + let quit_item_prefix = NSString::alloc(nil).init_str("Quit "); + let quit_item_title = quit_item_prefix.stringByAppendingString_(process_name); + let quit_item = menu_item( + quit_item_title, + selector("terminate:"), + Some(KeyEquivalent { + key: "q", + masks: None, + }), + ); + + app_menu.addItem_(about_item); + app_menu.addItem_(sep_first); + app_menu.addItem_(hide_item); + app_menu.addItem_(hide_others_item); + app_menu.addItem_(show_all_item); + app_menu.addItem_(sep); + app_menu.addItem_(quit_item); + app_menu_item.setSubmenu_(app_menu); + }); +} + +fn menu_item( + title: *mut Object, + selector: Sel, + key_equivalent: Option>, +) -> *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), + }; + let item = NSMenuItem::alloc(nil).initWithTitle_action_keyEquivalent_(title, selector, key); + if let Some(masks) = masks { + item.setKeyEquivalentModifierMask_(masks) + } + + item + } +} diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 72b8e0a332f..09b61a5d6d4 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -7,6 +7,7 @@ mod app_state; mod event; mod event_loop; mod ffi; +mod menu; mod monitor; mod observer; mod util; From ab17bc1e348a98f80230e4de0145b0763add37e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C3=BAr=20Kov=C3=A1cs?= Date: Sun, 25 Apr 2021 18:29:55 +0200 Subject: [PATCH 18/26] New keyboard API for Windows (#1788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introducing the new `KeyEvent` and renaming old stuff * Implemented physical_key on Windows * Ran cargo fmt * Progress with the keyboard's windows implementation * Add proper handling of dead keys * Add translation for non-printable virtual keys * Run `cargo fmt` * Fix for AltGraph not being reported * Synthesize key events when focus enters or leaves * Minor improvements * Remove obsolete API * Fix numlock and pause not being reported correctly * Ran `cargo fmt` * Fix numpad keys being reported incorrectly * Update examples * Ran `cargo fmt` * Add documentation for `ScanCode` * Add key binding example * Use consistent modifier key names #1343 * WONT COMPILE transitioning to new keyboard API * WONT COMPILE Implement new keyboard layout preparation * WONT COMPILE new keyboard API progress * Main compile errors fixed for keyboard * Fix bugs in new keyboard handling * Remove obsolete code * Fix examples * Ran `cargo fmt` * Fix build error with serde * Ran `cargo fmt` * Tweaks in the Windows keyboard implementation * Add `KeyCodeExtScancode` * Add `reset_dead_keys` * Improve the documentation for `Key` and `KeyCode` * Rename the meta key to super * Address feedback for the keyboard API * Fix for rustdoc Co-authored-by: Markus Røyset * Improve documentation Co-authored-by: Markus Røyset * Fix for arrow keys being reported as unidentified. And minor improvements * Fix media keys reporting Unidentified * Don't report text on key release events * Fix for NumLock being reported as Pause in raw input * Fix for strange behaviour around NumLock and Pause * Fix for NumLock being ineffective * Fix for location not being reported correctly * `RawKeyEvent`s now report repeat * Don't report text for synthetic key releases * Address feedback - Add the `Space` variant to the `to_text` function. - Mark `get_kbd_state` safe. - Change `[MaybeUninit; 256]` to `MaybeUninit<[u8; 256]>` * Filter `Unidentified` from PrtSc key device events * Don't report incorrect `RawKeyEvent` for shift + numpad * AltGraph is not reported again * Document Windows specific behaviour for shift+numpad * Fix typo * Dead keys now affect characters from logical_key * Prevent Pause and NumLock mappings in window events * Apply suggestions from code review Co-authored-by: Markus Røyset * Ran `cargo fmt` * Add W3C license for `Key` and `KeyCode` * Extend documentation according to feedback * Ignore NumLock in `key_without_modifiers` * Remove unused `key_code_to_non_char_key` * Remove empty event.rs file * Use space for resetting dead keys * Fix reporting multiple graphemes in logical_key * Avoid incorrect synthetic keypress during setfocus * Fixed the AltGr keypress not being reported when the AltGr key is pressed and released in a very quick succession * Filter fake Ctrl events when pressing AltGr * Improve code quality * Remove `repeat` from `RawKeyEvent` * Allow fractional scroll in raw mouse events * Fix typo Co-authored-by: Markus Siglreithmaier * Remove unused imports * Remove unused variable * Remove unnecessary `unwrap()` Co-authored-by: Markus Siglreithmaier * Avoid using the deprecated `into_rgba()` * Fix IME crash Co-authored-by: Markus Røyset Co-authored-by: Markus Siglreithmaier --- Cargo.toml | 3 + examples/control_flow.rs | 25 +- examples/cursor.rs | 6 +- examples/cursor_grab.rs | 21 +- examples/fullscreen.rs | 26 +- examples/handling_close.rs | 23 +- examples/key_binding.rs | 58 + examples/minimize.rs | 12 +- examples/multithreaded.rs | 163 +- examples/multiwindow.rs | 8 +- examples/resizable.rs | 9 +- examples/window_debug.rs | 121 +- src/event.rs | 417 ++---- src/keyboard.rs | 1417 ++++++++++++++++++ src/lib.rs | 1 + src/platform/mod.rs | 2 + src/platform/modifier_supplement.rs | 32 + src/platform/scancode.rs | 31 + src/platform/windows.rs | 362 ++++- src/platform_impl/windows/event.rs | 417 ------ src/platform_impl/windows/event_loop.rs | 653 ++++---- src/platform_impl/windows/keyboard.rs | 802 ++++++++++ src/platform_impl/windows/keyboard_layout.rs | 993 ++++++++++++ src/platform_impl/windows/minimal_ime.rs | 93 ++ src/platform_impl/windows/mod.rs | 11 +- src/platform_impl/windows/window.rs | 21 + src/platform_impl/windows/window_state.rs | 12 +- src/window.rs | 16 + tests/serde_objects.rs | 11 +- 29 files changed, 4506 insertions(+), 1260 deletions(-) create mode 100644 examples/key_binding.rs create mode 100644 src/keyboard.rs create mode 100644 src/platform/modifier_supplement.rs create mode 100644 src/platform/scancode.rs delete mode 100644 src/platform_impl/windows/event.rs create mode 100644 src/platform_impl/windows/keyboard.rs create mode 100644 src/platform_impl/windows/keyboard_layout.rs create mode 100644 src/platform_impl/windows/minimal_ime.rs diff --git a/Cargo.toml b/Cargo.toml index 64b1bee10ef..807fa9c1092 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ log = "0.4" serde = { version = "1", optional = true, features = ["serde_derive"] } raw-window-handle = "0.3" bitflags = "1" +nameof = "1" [dev-dependencies] image = "0.23.12" @@ -57,6 +58,7 @@ features = ["display_link"] [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.6" @@ -80,6 +82,7 @@ features = [ "winerror", "wingdi", "winnt", + "winnls", "winuser", ] diff --git a/examples/control_flow.rs b/examples/control_flow.rs index 64372a9bc80..6e876a3eafd 100644 --- a/examples/control_flow.rs +++ b/examples/control_flow.rs @@ -2,8 +2,9 @@ use std::{thread, time}; use simple_logger::SimpleLogger; use winit::{ - event::{Event, KeyboardInput, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::Key, window::WindowBuilder, }; @@ -38,7 +39,7 @@ fn main() { let mut close_requested = false; event_loop.run(move |event, _, control_flow| { - use winit::event::{ElementState, StartCause, VirtualKeyCode}; + use winit::event::StartCause; println!("{:?}", event); match event { Event::NewEvents(start_cause) => { @@ -52,31 +53,33 @@ fn main() { close_requested = true; } WindowEvent::KeyboardInput { - input: - KeyboardInput { - virtual_keycode: Some(virtual_code), + event: + KeyEvent { + logical_key: key, state: ElementState::Pressed, .. }, .. - } => match virtual_code { - VirtualKeyCode::Key1 => { + } => match key { + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + Key::Character("1") => { mode = Mode::Wait; println!("\nmode: {:?}\n", mode); } - VirtualKeyCode::Key2 => { + Key::Character("2") => { mode = Mode::WaitUntil; println!("\nmode: {:?}\n", mode); } - VirtualKeyCode::Key3 => { + Key::Character("3") => { mode = Mode::Poll; println!("\nmode: {:?}\n", mode); } - VirtualKeyCode::R => { + Key::Character("r") => { request_redraw = !request_redraw; println!("\nrequest_redraw: {}\n", request_redraw); } - VirtualKeyCode::Escape => { + Key::Escape => { close_requested = true; } _ => (), diff --git a/examples/cursor.rs b/examples/cursor.rs index a466e889a83..2b6fe5b2071 100644 --- a/examples/cursor.rs +++ b/examples/cursor.rs @@ -1,6 +1,6 @@ use simple_logger::SimpleLogger; use winit::{ - event::{ElementState, Event, KeyboardInput, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::{CursorIcon, WindowBuilder}, }; @@ -21,8 +21,8 @@ fn main() { Event::WindowEvent { event: WindowEvent::KeyboardInput { - input: - KeyboardInput { + event: + KeyEvent { state: ElementState::Pressed, .. }, diff --git a/examples/cursor_grab.rs b/examples/cursor_grab.rs index 90a94764de1..746bf426ba1 100644 --- a/examples/cursor_grab.rs +++ b/examples/cursor_grab.rs @@ -1,7 +1,8 @@ use simple_logger::SimpleLogger; use winit::{ - event::{DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, WindowEvent}, + event::{DeviceEvent, ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::{Key, ModifiersState}, window::WindowBuilder, }; @@ -23,19 +24,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 winit::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/fullscreen.rs b/examples/fullscreen.rs index 83fbde30db6..4566632be6e 100644 --- a/examples/fullscreen.rs +++ b/examples/fullscreen.rs @@ -1,8 +1,9 @@ use std::io::{stdin, stdout, Write}; use simple_logger::SimpleLogger; -use winit::event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}; +use winit::event::{ElementState, Event, KeyEvent, WindowEvent}; use winit::event_loop::{ControlFlow, EventLoop}; +use winit::keyboard::Key; use winit::monitor::{MonitorHandle, VideoMode}; use winit::window::{Fullscreen, WindowBuilder}; @@ -38,30 +39,33 @@ 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: key, + state: ElementState::Pressed, .. }, .. - } => match (virtual_code, state) { - (VirtualKeyCode::Escape, _) => *control_flow = ControlFlow::Exit, - (VirtualKeyCode::F, ElementState::Pressed) => { + } => match key { + Key::Escape => *control_flow = ControlFlow::Exit, + + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + Key::Character("f") => { if window.fullscreen().is_some() { window.set_fullscreen(None); } else { window.set_fullscreen(fullscreen.clone()); } } - (VirtualKeyCode::S, ElementState::Pressed) => { + Key::Character("s") => { println!("window.fullscreen {:?}", window.fullscreen()); } - (VirtualKeyCode::M, ElementState::Pressed) => { + Key::Character("m") => { let is_maximized = window.is_maximized(); window.set_maximized(!is_maximized); } - (VirtualKeyCode::D, ElementState::Pressed) => { + Key::Character("d") => { decorations = !decorations; window.set_decorations(decorations); } diff --git a/examples/handling_close.rs b/examples/handling_close.rs index 8334c1773f5..283cf48195c 100644 --- a/examples/handling_close.rs +++ b/examples/handling_close.rs @@ -1,7 +1,8 @@ use simple_logger::SimpleLogger; use winit::{ - event::{Event, KeyboardInput, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::Key, window::WindowBuilder, }; @@ -17,10 +18,6 @@ fn main() { let mut close_requested = false; event_loop.run(move |event, _, control_flow| { - use winit::event::{ - ElementState::Released, - VirtualKeyCode::{N, Y}, - }; *control_flow = ControlFlow::Wait; match event { @@ -44,16 +41,18 @@ fn main() { // the Y key. } WindowEvent::KeyboardInput { - input: - KeyboardInput { - virtual_keycode: Some(virtual_code), - state: Released, + event: + KeyEvent { + logical_key: key, + state: ElementState::Released, .. }, .. } => { - match virtual_code { - Y => { + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + match key { + Key::Character("y") => { if close_requested { // This is where you'll want to do any cleanup you need. println!("Buh-bye!"); @@ -66,7 +65,7 @@ fn main() { *control_flow = ControlFlow::Exit; } } - N => { + Key::Character("n") => { if close_requested { println!("Your window will continue to stay by your side."); close_requested = false; diff --git a/examples/key_binding.rs b/examples/key_binding.rs new file mode 100644 index 00000000000..6f9bb1fe83b --- /dev/null +++ b/examples/key_binding.rs @@ -0,0 +1,58 @@ +use simple_logger::SimpleLogger; +use winit::{ + dpi::LogicalSize, + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::{Key, ModifiersState}, + window::WindowBuilder, +}; + +///////////////////////////////////////////////////////////////////////////// +// WARNING: This is not available on all platforms (for example on the web). +use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; +///////////////////////////////////////////////////////////////////////////// + +fn main() { + SimpleLogger::new().init().unwrap(); + let event_loop = EventLoop::new(); + + let _window = WindowBuilder::new() + .with_inner_size(LogicalSize::new(400.0, 200.0)) + .build(&event_loop) + .unwrap(); + + let mut modifiers = ModifiersState::default(); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, + WindowEvent::ModifiersChanged(new_state) => { + modifiers = new_state; + } + WindowEvent::KeyboardInput { event, .. } => { + handle_key_event(modifiers, event); + } + _ => (), + }, + _ => (), + }; + }); +} + +fn handle_key_event(modifiers: ModifiersState, event: KeyEvent) { + if event.state == ElementState::Pressed && !event.repeat { + match event.key_without_modifiers() { + Key::Character("1") => { + if modifiers.shift_key() { + println!("Shift + 1 | logical_key: {:?}", event.logical_key); + } else { + println!("1"); + } + } + _ => (), + } + } +} diff --git a/examples/minimize.rs b/examples/minimize.rs index eb02a752c99..3c8b7f84ed0 100644 --- a/examples/minimize.rs +++ b/examples/minimize.rs @@ -1,8 +1,10 @@ extern crate winit; use simple_logger::SimpleLogger; -use winit::event::{Event, VirtualKeyCode, WindowEvent}; + +use winit::event::{Event, WindowEvent}; use winit::event_loop::{ControlFlow, EventLoop}; +use winit::keyboard::Key; use winit::window::WindowBuilder; fn main() { @@ -25,12 +27,14 @@ 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) { + // Pressing the 'm' key will minimize the window + // WARNING: Consider using `key_without_modifers()` if available on your platform. + // See the `key_binding` example + if let Key::Character("m") = event.logical_key { window.set_minimized(true); } } diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs index 68cdb60b784..00235b114c3 100644 --- a/examples/multithreaded.rs +++ b/examples/multithreaded.rs @@ -5,8 +5,9 @@ fn main() { use simple_logger::SimpleLogger; use winit::{ 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}, }; @@ -27,6 +28,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 { @@ -49,32 +51,92 @@ 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!( @@ -82,61 +144,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); - } _ => (), } } @@ -155,10 +162,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 ec97eee0974..924a55296bd 100644 --- a/examples/multiwindow.rs +++ b/examples/multiwindow.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use simple_logger::SimpleLogger; use winit::{ - event::{ElementState, Event, KeyboardInput, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::Window, }; @@ -34,9 +34,9 @@ fn main() { } } WindowEvent::KeyboardInput { - input: - KeyboardInput { - state: ElementState::Pressed, + event: + KeyEvent { + state: ElementState::Released, .. }, .. diff --git a/examples/resizable.rs b/examples/resizable.rs index 17892d87412..8d3387cabb8 100644 --- a/examples/resizable.rs +++ b/examples/resizable.rs @@ -1,8 +1,9 @@ use simple_logger::SimpleLogger; use winit::{ dpi::LogicalSize, - event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, + event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::{ControlFlow, EventLoop}, + keyboard::KeyCode, window::WindowBuilder, }; @@ -26,9 +27,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/window_debug.rs b/examples/window_debug.rs index 577ad5cd734..4a12cf47b94 100644 --- a/examples/window_debug.rs +++ b/examples/window_debug.rs @@ -3,8 +3,9 @@ use simple_logger::SimpleLogger; use winit::{ 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}, }; @@ -34,22 +35,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); @@ -58,61 +61,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/event.rs b/src/event.rs index f39ddaac500..6bd9009a0b3 100644 --- a/src/event.rs +++ b/src/event.rs @@ -38,6 +38,7 @@ use std::path::PathBuf; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, + keyboard::{self, ModifiersState}, platform_impl, window::{Theme, WindowId}, }; @@ -239,8 +240,12 @@ pub enum WindowEvent<'a> { /// hovered. HoveredFileCancelled, - /// The window received a unicode character. - ReceivedCharacter(char), + /// The user commited an IME string for this window. + /// + /// This is a temporary API until [#1497] gets completed. + /// + /// [#1497]: https://github.com/rust-windowing/winit/issues/1497 + ReceivedImeText(String), /// The window gained or lost focus. /// @@ -248,9 +253,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 winit /// in one of the following circumstances: /// @@ -365,18 +376,17 @@ impl Clone for WindowEvent<'static> { DroppedFile(file) => DroppedFile(file.clone()), HoveredFile(file) => HoveredFile(file.clone()), HoveredFileCancelled => HoveredFileCancelled, - ReceivedCharacter(c) => ReceivedCharacter(*c), + ReceivedImeText(s) => ReceivedImeText(s.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, }, - ModifiersChanged(modifiers) => ModifiersChanged(modifiers.clone()), #[allow(deprecated)] CursorMoved { @@ -456,15 +466,15 @@ impl<'a> WindowEvent<'a> { DroppedFile(file) => Some(DroppedFile(file)), HoveredFile(file) => Some(HoveredFile(file)), HoveredFileCancelled => Some(HoveredFileCancelled), - ReceivedCharacter(c) => Some(ReceivedCharacter(c)), + ReceivedImeText(s) => Some(ReceivedImeText(s)), 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)), @@ -589,38 +599,82 @@ 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 - /// - /// 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, - +pub struct RawKeyEvent { + pub physical_key: keyboard::KeyCode, pub state: ElementState, +} - /// Identifies the semantic meaning of the key +/// 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.) /// - /// 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, + /// Note that `Fn` and `FnLock` key events are not guaranteed to be emitted by `winit`. These + /// keys are usually handled at the hardware or OS level. + pub physical_key: keyboard::KeyCode, - /// Modifier keys active at the time of this input. + /// 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`]. /// - /// 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, + /// ## Platform-specific + /// - **Web:** Dead keys might be reported as the real key instead + /// of `Dead` depending on the browser/OS. + /// + /// [`key_without_modifiers`]: crate::platform::modifier_supplement::KeyEventExtModifierSupplement::key_without_modifiers + 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, } /// Describes touch-screen input state. @@ -721,9 +775,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; @@ -766,303 +817,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 00000000000..28983cfaf4f --- /dev/null +++ b/src/keyboard.rs @@ -0,0 +1,1417 @@ +//! 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 --------------------------------------------------------- +// +// winit: https://github.com/rust-windowing/winit +// +// 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 nameof::name_of; + +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(Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum NativeKeyCode { + Unidentified, + Windows(u16), + MacOS(u32), + XKB(u32), +} +impl std::fmt::Debug for NativeKeyCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use NativeKeyCode::{MacOS, Unidentified, Windows, XKB}; + let mut debug_tuple; + match self { + Unidentified => { + debug_tuple = f.debug_tuple(name_of!(Unidentified)); + } + Windows(v) => { + debug_tuple = f.debug_tuple(name_of!(Windows)); + debug_tuple.field(&format_args!("0x{:04X}", v)); + } + MacOS(v) => { + debug_tuple = f.debug_tuple(name_of!(MacOS)); + debug_tuple.field(v); + } + XKB(v) => { + debug_tuple = f.debug_tuple(name_of!(XKB)); + debug_tuple.field(v); + } + } + debug_tuple.finish() + } +} + +/// Represents the location 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 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. + BrowserBack, + BrowserFavorites, + /// Some laptops place this key to the right of the key. + BrowserForward, + 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, +} + +/// 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, Copy, 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, + } + } +} + +#[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 51f4a8634a3..06556e88f23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -155,6 +155,7 @@ pub mod error; pub mod event; pub mod event_loop; mod icon; +pub mod keyboard; pub mod monitor; mod platform_impl; pub mod window; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 1ee5fce2bb2..84f016c2d22 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -21,5 +21,7 @@ pub mod macos; pub mod unix; pub mod windows; +pub mod modifier_supplement; pub mod run_return; +pub mod scancode; pub mod web; diff --git a/src/platform/modifier_supplement.rs b/src/platform/modifier_supplement.rs new file mode 100644 index 00000000000..361ed05d040 --- /dev/null +++ b/src/platform/modifier_supplement.rs @@ -0,0 +1,32 @@ +#![cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] + +use crate::keyboard::Key; + +/// Additional methods for the `KeyEvent` which cannot be implemented on all +/// platforms. +pub trait KeyEventExtModifierSupplement { + /// Identical to `KeyEvent::text` but this is affected by Ctrl. + /// + /// For example, pressing Ctrl+a produces `Some("\x01")`. + fn text_with_all_modifiers(&self) -> Option<&str>; + + /// 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. + /// + /// 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`. + fn key_without_modifiers(&self) -> Key<'static>; +} diff --git a/src/platform/scancode.rs b/src/platform/scancode.rs new file mode 100644 index 00000000000..078fe74deaa --- /dev/null +++ b/src/platform/scancode.rs @@ -0,0 +1,31 @@ +#![cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] + +// TODO: Maybe merge this with `modifier_supplement` if the two are indeed supported on the same +// set of platforms + +use crate::keyboard::KeyCode; + +pub trait KeyCodeExtScancode { + /// The raw value of the platform-specific physical key identifier. + /// + /// Returns `Some(key_id)` if the conversion was succesful; returns `None` otherwise. + /// + /// ## Platform-specific + /// - **Windows:** A 16bit extended scancode + // TODO: Describe what this value contains for each platform + fn to_scancode(self) -> Option; + + /// Constructs a `KeyCode` from a platform-specific physical key identifier. + /// + /// Note that this conversion may be lossy, i.e. converting the returned `KeyCode` back + /// using `to_scancode` might not yield the original value. + fn from_scancode(scancode: u32) -> KeyCode; +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index dd58d758cd9..f163a200bf7 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -4,14 +4,24 @@ use std::os::raw::c_void; use std::path::Path; use libc; -use winapi::shared::minwindef::WORD; -use winapi::shared::windef::{HMENU, HWND}; +use winapi::{ + shared::{ + minwindef::{LOWORD, WORD}, + windef::{HMENU, HWND}, + }, + um::{ + winnt::{LANG_KOREAN, PRIMARYLANGID}, + winuser::GetKeyboardLayout, + }, +}; use crate::{ dpi::PhysicalSize, - event::DeviceId, + event::{DeviceId, KeyEvent}, event_loop::EventLoop, + keyboard::{Key, KeyCode, NativeKeyCode}, monitor::MonitorHandle, + platform::{modifier_supplement::KeyEventExtModifierSupplement, scancode::KeyCodeExtScancode}, platform_impl::{EventLoop as WindowsEventLoop, WinIcon}, window::{BadIcon, Icon, Theme, Window, WindowBuilder}, }; @@ -250,3 +260,349 @@ impl IconExtWindows for Icon { Ok(Icon { inner: win_icon }) } } + +impl KeyEventExtModifierSupplement for KeyEvent { + #[inline] + fn text_with_all_modifiers(&self) -> Option<&str> { + self.platform_specific.text_with_all_modifers + } + + #[inline] + fn key_without_modifiers(&self) -> Key<'static> { + self.platform_specific.key_without_modifiers + } +} + +impl KeyCodeExtScancode for KeyCode { + fn to_scancode(self) -> 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 self { + 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, + } + } + + fn 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/event.rs b/src/platform_impl/windows/event.rs deleted file mode 100644 index 161cf6b2de0..00000000000 --- a/src/platform_impl/windows/event.rs +++ /dev/null @@ -1,417 +0,0 @@ -use std::{ - char, - os::raw::c_int, - ptr, - sync::atomic::{AtomicBool, AtomicPtr, Ordering}, -}; - -use crate::event::{ModifiersState, ScanCode, VirtualKeyCode}; - -use winapi::{ - shared::minwindef::{HKL, HKL__, LPARAM, UINT, WPARAM}, - um::winuser, -}; - -fn key_pressed(vkey: c_int) -> bool { - unsafe { (winuser::GetKeyState(vkey) & (1 << 15)) == (1 << 15) } -} - -pub fn get_key_mods() -> ModifiersState { - let filter_out_altgr = layout_uses_altgr() && key_pressed(winuser::VK_RMENU); - - let mut mods = ModifiersState::empty(); - mods.set(ModifiersState::SHIFT, key_pressed(winuser::VK_SHIFT)); - mods.set( - ModifiersState::CTRL, - key_pressed(winuser::VK_CONTROL) && !filter_out_altgr, - ); - mods.set( - ModifiersState::ALT, - key_pressed(winuser::VK_MENU) && !filter_out_altgr, - ); - mods.set( - ModifiersState::LOGO, - key_pressed(winuser::VK_LWIN) || key_pressed(winuser::VK_RWIN), - ); - mods -} - -bitflags! { - #[derive(Default)] - pub struct ModifiersStateSide: u32 { - const LSHIFT = 0b010 << 0; - const RSHIFT = 0b001 << 0; - - const LCTRL = 0b010 << 3; - const RCTRL = 0b001 << 3; - - const LALT = 0b010 << 6; - const RALT = 0b001 << 6; - - const LLOGO = 0b010 << 9; - const RLOGO = 0b001 << 9; - } -} - -impl ModifiersStateSide { - pub fn filter_out_altgr(&self) -> ModifiersStateSide { - match layout_uses_altgr() && self.contains(Self::RALT) { - false => *self, - true => *self & !(Self::LCTRL | Self::RCTRL | Self::LALT | Self::RALT), - } - } -} - -impl From for ModifiersState { - fn from(side: ModifiersStateSide) -> Self { - let mut state = ModifiersState::default(); - state.set( - Self::SHIFT, - side.intersects(ModifiersStateSide::LSHIFT | ModifiersStateSide::RSHIFT), - ); - state.set( - Self::CTRL, - side.intersects(ModifiersStateSide::LCTRL | ModifiersStateSide::RCTRL), - ); - state.set( - Self::ALT, - side.intersects(ModifiersStateSide::LALT | ModifiersStateSide::RALT), - ); - state.set( - Self::LOGO, - side.intersects(ModifiersStateSide::LLOGO | ModifiersStateSide::RLOGO), - ); - state - } -} - -pub fn get_pressed_keys() -> impl Iterator { - let mut keyboard_state = vec![0u8; 256]; - unsafe { winuser::GetKeyboardState(keyboard_state.as_mut_ptr()) }; - keyboard_state - .into_iter() - .enumerate() - .filter(|(_, p)| (*p & (1 << 7)) != 0) // whether or not a key is pressed is communicated via the high-order bit - .map(|(i, _)| i as c_int) -} - -unsafe fn get_char(keyboard_state: &[u8; 256], v_key: u32, hkl: HKL) -> Option { - let mut unicode_bytes = [0u16; 5]; - let len = winuser::ToUnicodeEx( - v_key, - 0, - keyboard_state.as_ptr(), - unicode_bytes.as_mut_ptr(), - unicode_bytes.len() as _, - 0, - hkl, - ); - if len >= 1 { - char::decode_utf16(unicode_bytes.iter().cloned()) - .next() - .and_then(|c| c.ok()) - } else { - None - } -} - -/// Figures out if the keyboard layout has an AltGr key instead of an Alt key. -/// -/// Unfortunately, the Windows API doesn't give a way for us to conveniently figure that out. So, -/// we use a technique blatantly stolen from [the Firefox source code][source]: iterate over every -/// possible virtual key and compare the `char` output when AltGr is pressed vs when it isn't. If -/// pressing AltGr outputs characters that are different from the standard characters, the layout -/// uses AltGr. Otherwise, it doesn't. -/// -/// [source]: https://github.com/mozilla/gecko-dev/blob/265e6721798a455604328ed5262f430cfcc37c2f/widget/windows/KeyboardLayout.cpp#L4356-L4416 -fn layout_uses_altgr() -> bool { - unsafe { - static ACTIVE_LAYOUT: AtomicPtr = AtomicPtr::new(ptr::null_mut()); - static USES_ALTGR: AtomicBool = AtomicBool::new(false); - - let hkl = winuser::GetKeyboardLayout(0); - let old_hkl = ACTIVE_LAYOUT.swap(hkl, Ordering::SeqCst); - - if hkl == old_hkl { - return USES_ALTGR.load(Ordering::SeqCst); - } - - let mut keyboard_state_altgr = [0u8; 256]; - // AltGr is an alias for Ctrl+Alt for... some reason. Whatever it is, those are the keypresses - // we have to emulate to do an AltGr test. - keyboard_state_altgr[winuser::VK_MENU as usize] = 0x80; - keyboard_state_altgr[winuser::VK_CONTROL as usize] = 0x80; - - let keyboard_state_empty = [0u8; 256]; - - for v_key in 0..=255 { - let key_noaltgr = get_char(&keyboard_state_empty, v_key, hkl); - let key_altgr = get_char(&keyboard_state_altgr, v_key, hkl); - if let (Some(noaltgr), Some(altgr)) = (key_noaltgr, key_altgr) { - if noaltgr != altgr { - USES_ALTGR.store(true, Ordering::SeqCst); - return true; - } - } - } - - USES_ALTGR.store(false, Ordering::SeqCst); - false - } -} - -pub fn vkey_to_winit_vkey(vkey: c_int) -> Option { - // VK_* codes are documented here https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx - match vkey { - //winuser::VK_LBUTTON => Some(VirtualKeyCode::Lbutton), - //winuser::VK_RBUTTON => Some(VirtualKeyCode::Rbutton), - //winuser::VK_CANCEL => Some(VirtualKeyCode::Cancel), - //winuser::VK_MBUTTON => Some(VirtualKeyCode::Mbutton), - //winuser::VK_XBUTTON1 => Some(VirtualKeyCode::Xbutton1), - //winuser::VK_XBUTTON2 => Some(VirtualKeyCode::Xbutton2), - winuser::VK_BACK => Some(VirtualKeyCode::Back), - winuser::VK_TAB => Some(VirtualKeyCode::Tab), - //winuser::VK_CLEAR => Some(VirtualKeyCode::Clear), - winuser::VK_RETURN => Some(VirtualKeyCode::Return), - winuser::VK_LSHIFT => Some(VirtualKeyCode::LShift), - winuser::VK_RSHIFT => Some(VirtualKeyCode::RShift), - winuser::VK_LCONTROL => Some(VirtualKeyCode::LControl), - winuser::VK_RCONTROL => Some(VirtualKeyCode::RControl), - winuser::VK_LMENU => Some(VirtualKeyCode::LAlt), - winuser::VK_RMENU => Some(VirtualKeyCode::RAlt), - winuser::VK_PAUSE => Some(VirtualKeyCode::Pause), - winuser::VK_CAPITAL => Some(VirtualKeyCode::Capital), - winuser::VK_KANA => Some(VirtualKeyCode::Kana), - //winuser::VK_HANGUEL => Some(VirtualKeyCode::Hanguel), - //winuser::VK_HANGUL => Some(VirtualKeyCode::Hangul), - //winuser::VK_JUNJA => Some(VirtualKeyCode::Junja), - //winuser::VK_FINAL => Some(VirtualKeyCode::Final), - //winuser::VK_HANJA => Some(VirtualKeyCode::Hanja), - winuser::VK_KANJI => Some(VirtualKeyCode::Kanji), - winuser::VK_ESCAPE => Some(VirtualKeyCode::Escape), - winuser::VK_CONVERT => Some(VirtualKeyCode::Convert), - winuser::VK_NONCONVERT => Some(VirtualKeyCode::NoConvert), - //winuser::VK_ACCEPT => Some(VirtualKeyCode::Accept), - //winuser::VK_MODECHANGE => Some(VirtualKeyCode::Modechange), - winuser::VK_SPACE => Some(VirtualKeyCode::Space), - winuser::VK_PRIOR => Some(VirtualKeyCode::PageUp), - winuser::VK_NEXT => Some(VirtualKeyCode::PageDown), - winuser::VK_END => Some(VirtualKeyCode::End), - winuser::VK_HOME => Some(VirtualKeyCode::Home), - winuser::VK_LEFT => Some(VirtualKeyCode::Left), - winuser::VK_UP => Some(VirtualKeyCode::Up), - winuser::VK_RIGHT => Some(VirtualKeyCode::Right), - winuser::VK_DOWN => Some(VirtualKeyCode::Down), - //winuser::VK_SELECT => Some(VirtualKeyCode::Select), - //winuser::VK_PRINT => Some(VirtualKeyCode::Print), - //winuser::VK_EXECUTE => Some(VirtualKeyCode::Execute), - winuser::VK_SNAPSHOT => Some(VirtualKeyCode::Snapshot), - winuser::VK_INSERT => Some(VirtualKeyCode::Insert), - winuser::VK_DELETE => Some(VirtualKeyCode::Delete), - //winuser::VK_HELP => Some(VirtualKeyCode::Help), - 0x30 => Some(VirtualKeyCode::Key0), - 0x31 => Some(VirtualKeyCode::Key1), - 0x32 => Some(VirtualKeyCode::Key2), - 0x33 => Some(VirtualKeyCode::Key3), - 0x34 => Some(VirtualKeyCode::Key4), - 0x35 => Some(VirtualKeyCode::Key5), - 0x36 => Some(VirtualKeyCode::Key6), - 0x37 => Some(VirtualKeyCode::Key7), - 0x38 => Some(VirtualKeyCode::Key8), - 0x39 => Some(VirtualKeyCode::Key9), - 0x41 => Some(VirtualKeyCode::A), - 0x42 => Some(VirtualKeyCode::B), - 0x43 => Some(VirtualKeyCode::C), - 0x44 => Some(VirtualKeyCode::D), - 0x45 => Some(VirtualKeyCode::E), - 0x46 => Some(VirtualKeyCode::F), - 0x47 => Some(VirtualKeyCode::G), - 0x48 => Some(VirtualKeyCode::H), - 0x49 => Some(VirtualKeyCode::I), - 0x4A => Some(VirtualKeyCode::J), - 0x4B => Some(VirtualKeyCode::K), - 0x4C => Some(VirtualKeyCode::L), - 0x4D => Some(VirtualKeyCode::M), - 0x4E => Some(VirtualKeyCode::N), - 0x4F => Some(VirtualKeyCode::O), - 0x50 => Some(VirtualKeyCode::P), - 0x51 => Some(VirtualKeyCode::Q), - 0x52 => Some(VirtualKeyCode::R), - 0x53 => Some(VirtualKeyCode::S), - 0x54 => Some(VirtualKeyCode::T), - 0x55 => Some(VirtualKeyCode::U), - 0x56 => Some(VirtualKeyCode::V), - 0x57 => Some(VirtualKeyCode::W), - 0x58 => Some(VirtualKeyCode::X), - 0x59 => Some(VirtualKeyCode::Y), - 0x5A => Some(VirtualKeyCode::Z), - winuser::VK_LWIN => Some(VirtualKeyCode::LWin), - winuser::VK_RWIN => Some(VirtualKeyCode::RWin), - winuser::VK_APPS => Some(VirtualKeyCode::Apps), - winuser::VK_SLEEP => Some(VirtualKeyCode::Sleep), - winuser::VK_NUMPAD0 => Some(VirtualKeyCode::Numpad0), - winuser::VK_NUMPAD1 => Some(VirtualKeyCode::Numpad1), - winuser::VK_NUMPAD2 => Some(VirtualKeyCode::Numpad2), - winuser::VK_NUMPAD3 => Some(VirtualKeyCode::Numpad3), - winuser::VK_NUMPAD4 => Some(VirtualKeyCode::Numpad4), - winuser::VK_NUMPAD5 => Some(VirtualKeyCode::Numpad5), - winuser::VK_NUMPAD6 => Some(VirtualKeyCode::Numpad6), - winuser::VK_NUMPAD7 => Some(VirtualKeyCode::Numpad7), - winuser::VK_NUMPAD8 => Some(VirtualKeyCode::Numpad8), - winuser::VK_NUMPAD9 => Some(VirtualKeyCode::Numpad9), - winuser::VK_MULTIPLY => Some(VirtualKeyCode::NumpadMultiply), - winuser::VK_ADD => Some(VirtualKeyCode::NumpadAdd), - //winuser::VK_SEPARATOR => Some(VirtualKeyCode::Separator), - winuser::VK_SUBTRACT => Some(VirtualKeyCode::NumpadSubtract), - winuser::VK_DECIMAL => Some(VirtualKeyCode::NumpadDecimal), - winuser::VK_DIVIDE => Some(VirtualKeyCode::NumpadDivide), - winuser::VK_F1 => Some(VirtualKeyCode::F1), - winuser::VK_F2 => Some(VirtualKeyCode::F2), - winuser::VK_F3 => Some(VirtualKeyCode::F3), - winuser::VK_F4 => Some(VirtualKeyCode::F4), - winuser::VK_F5 => Some(VirtualKeyCode::F5), - winuser::VK_F6 => Some(VirtualKeyCode::F6), - winuser::VK_F7 => Some(VirtualKeyCode::F7), - winuser::VK_F8 => Some(VirtualKeyCode::F8), - winuser::VK_F9 => Some(VirtualKeyCode::F9), - winuser::VK_F10 => Some(VirtualKeyCode::F10), - winuser::VK_F11 => Some(VirtualKeyCode::F11), - winuser::VK_F12 => Some(VirtualKeyCode::F12), - winuser::VK_F13 => Some(VirtualKeyCode::F13), - winuser::VK_F14 => Some(VirtualKeyCode::F14), - winuser::VK_F15 => Some(VirtualKeyCode::F15), - winuser::VK_F16 => Some(VirtualKeyCode::F16), - winuser::VK_F17 => Some(VirtualKeyCode::F17), - winuser::VK_F18 => Some(VirtualKeyCode::F18), - winuser::VK_F19 => Some(VirtualKeyCode::F19), - winuser::VK_F20 => Some(VirtualKeyCode::F20), - winuser::VK_F21 => Some(VirtualKeyCode::F21), - winuser::VK_F22 => Some(VirtualKeyCode::F22), - winuser::VK_F23 => Some(VirtualKeyCode::F23), - winuser::VK_F24 => Some(VirtualKeyCode::F24), - winuser::VK_NUMLOCK => Some(VirtualKeyCode::Numlock), - winuser::VK_SCROLL => Some(VirtualKeyCode::Scroll), - winuser::VK_BROWSER_BACK => Some(VirtualKeyCode::NavigateBackward), - winuser::VK_BROWSER_FORWARD => Some(VirtualKeyCode::NavigateForward), - winuser::VK_BROWSER_REFRESH => Some(VirtualKeyCode::WebRefresh), - winuser::VK_BROWSER_STOP => Some(VirtualKeyCode::WebStop), - winuser::VK_BROWSER_SEARCH => Some(VirtualKeyCode::WebSearch), - winuser::VK_BROWSER_FAVORITES => Some(VirtualKeyCode::WebFavorites), - winuser::VK_BROWSER_HOME => Some(VirtualKeyCode::WebHome), - winuser::VK_VOLUME_MUTE => Some(VirtualKeyCode::Mute), - winuser::VK_VOLUME_DOWN => Some(VirtualKeyCode::VolumeDown), - winuser::VK_VOLUME_UP => Some(VirtualKeyCode::VolumeUp), - winuser::VK_MEDIA_NEXT_TRACK => Some(VirtualKeyCode::NextTrack), - winuser::VK_MEDIA_PREV_TRACK => Some(VirtualKeyCode::PrevTrack), - winuser::VK_MEDIA_STOP => Some(VirtualKeyCode::MediaStop), - winuser::VK_MEDIA_PLAY_PAUSE => Some(VirtualKeyCode::PlayPause), - winuser::VK_LAUNCH_MAIL => Some(VirtualKeyCode::Mail), - winuser::VK_LAUNCH_MEDIA_SELECT => Some(VirtualKeyCode::MediaSelect), - /*winuser::VK_LAUNCH_APP1 => Some(VirtualKeyCode::Launch_app1), - winuser::VK_LAUNCH_APP2 => Some(VirtualKeyCode::Launch_app2),*/ - winuser::VK_OEM_PLUS => Some(VirtualKeyCode::Equals), - winuser::VK_OEM_COMMA => Some(VirtualKeyCode::Comma), - winuser::VK_OEM_MINUS => Some(VirtualKeyCode::Minus), - winuser::VK_OEM_PERIOD => Some(VirtualKeyCode::Period), - winuser::VK_OEM_1 => map_text_keys(vkey), - winuser::VK_OEM_2 => map_text_keys(vkey), - winuser::VK_OEM_3 => map_text_keys(vkey), - winuser::VK_OEM_4 => map_text_keys(vkey), - winuser::VK_OEM_5 => map_text_keys(vkey), - winuser::VK_OEM_6 => map_text_keys(vkey), - winuser::VK_OEM_7 => map_text_keys(vkey), - /* winuser::VK_OEM_8 => Some(VirtualKeyCode::Oem_8), */ - winuser::VK_OEM_102 => Some(VirtualKeyCode::OEM102), - /*winuser::VK_PROCESSKEY => Some(VirtualKeyCode::Processkey), - winuser::VK_PACKET => Some(VirtualKeyCode::Packet), - winuser::VK_ATTN => Some(VirtualKeyCode::Attn), - winuser::VK_CRSEL => Some(VirtualKeyCode::Crsel), - winuser::VK_EXSEL => Some(VirtualKeyCode::Exsel), - winuser::VK_EREOF => Some(VirtualKeyCode::Ereof), - winuser::VK_PLAY => Some(VirtualKeyCode::Play), - winuser::VK_ZOOM => Some(VirtualKeyCode::Zoom), - winuser::VK_NONAME => Some(VirtualKeyCode::Noname), - winuser::VK_PA1 => Some(VirtualKeyCode::Pa1), - winuser::VK_OEM_CLEAR => Some(VirtualKeyCode::Oem_clear),*/ - _ => None, - } -} - -pub fn handle_extended_keys( - vkey: c_int, - mut scancode: UINT, - extended: bool, -) -> Option<(c_int, UINT)> { - // Welcome to hell https://blog.molecular-matters.com/2011/09/05/properly-handling-keyboard-input/ - scancode = if extended { 0xE000 } else { 0x0000 } | scancode; - let vkey = match vkey { - winuser::VK_SHIFT => unsafe { - winuser::MapVirtualKeyA(scancode, winuser::MAPVK_VSC_TO_VK_EX) as _ - }, - winuser::VK_CONTROL => { - if extended { - winuser::VK_RCONTROL - } else { - winuser::VK_LCONTROL - } - } - winuser::VK_MENU => { - if extended { - winuser::VK_RMENU - } else { - winuser::VK_LMENU - } - } - _ => { - match scancode { - // When VK_PAUSE is pressed it emits a LeftControl + NumLock scancode event sequence, but reports VK_PAUSE - // as the virtual key on both events, or VK_PAUSE on the first event or 0xFF when using raw input. - // Don't emit anything for the LeftControl event in the pair... - 0xE01D if vkey == winuser::VK_PAUSE => return None, - // ...and emit the Pause event for the second event in the pair. - 0x45 if vkey == winuser::VK_PAUSE || vkey == 0xFF as _ => { - scancode = 0xE059; - winuser::VK_PAUSE - } - // VK_PAUSE has an incorrect vkey value when used with modifiers. VK_PAUSE also reports a different - // scancode when used with modifiers than when used without - 0xE046 => { - scancode = 0xE059; - winuser::VK_PAUSE - } - // VK_SCROLL has an incorrect vkey value when used with modifiers. - 0x46 => winuser::VK_SCROLL, - _ => vkey, - } - } - }; - Some((vkey, scancode)) -} - -pub fn process_key_params( - wparam: WPARAM, - lparam: LPARAM, -) -> Option<(ScanCode, Option)> { - let scancode = ((lparam >> 16) & 0xff) as UINT; - let extended = (lparam & 0x01000000) != 0; - handle_extended_keys(wparam as _, scancode, extended) - .map(|(vkey, scancode)| (scancode, vkey_to_winit_vkey(vkey))) -} - -// This is needed as windows doesn't properly distinguish -// some virtual key codes for different keyboard layouts -fn map_text_keys(win_virtual_key: i32) -> Option { - let char_key = - unsafe { winuser::MapVirtualKeyA(win_virtual_key as u32, winuser::MAPVK_VK_TO_CHAR) } - & 0x7FFF; - match char::from_u32(char_key) { - Some(';') => Some(VirtualKeyCode::Semicolon), - Some('/') => Some(VirtualKeyCode::Slash), - Some('`') => Some(VirtualKeyCode::Grave), - Some('[') => Some(VirtualKeyCode::LBracket), - Some(']') => Some(VirtualKeyCode::RBracket), - Some('\'') => Some(VirtualKeyCode::Apostrophe), - Some('\\') => Some(VirtualKeyCode::Backslash), - _ => None, - } -} diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 5b49cbe8090..b9726a4e2cf 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -19,6 +19,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}, @@ -27,20 +28,24 @@ use winapi::{ um::{ commctrl, libloaderapi, ole2, processthreadsapi, winbase, winnt::{HANDLE, LONG, LPCSTR, SHORT}, - winuser, + winuser::{self, RAWINPUT}, }, }; use crate::{ 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}, monitor::MonitorHandle as RootMonitorHandle, + platform::scancode::KeyCodeExtScancode, platform_impl::platform::{ 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_winit_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}, @@ -108,6 +113,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, @@ -745,10 +757,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; @@ -763,6 +780,7 @@ fn update_modifiers(window: HWND, subclass_input: &SubclassInput) { }); } } + modifiers } /// Any window whose callback is configured to this function will have its events propagated @@ -819,6 +837,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. @@ -828,7 +915,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 => { @@ -836,18 +923,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 => { @@ -856,7 +941,7 @@ unsafe fn public_window_callback_inner( window_id: RootWindowId(WindowId(window)), event: CloseRequested, }); - 0 + result = ProcResult::Value(0); } winuser::WM_DESTROY => { @@ -867,13 +952,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 => { @@ -895,8 +980,6 @@ unsafe fn public_window_callback_inner( process_control_flow(&subclass_input.event_loop_runner); } } - - commctrl::DefSubclassProc(window, msg, wparam, lparam) } winuser::WM_WINDOWPOSCHANGING => { @@ -944,7 +1027,7 @@ unsafe fn public_window_callback_inner( } } - 0 + result = ProcResult::Value(0); } // WM_MOVE supplies client area positions, so we send Moved here instead. @@ -962,7 +1045,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 => { @@ -989,40 +1072,7 @@ unsafe fn public_window_callback_inner( } subclass_input.send_event(event); - 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 + result = ProcResult::Value(0); } // this is necessary for us to maintain minimize/restore state @@ -1040,11 +1090,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 => { @@ -1089,19 +1140,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 => { @@ -1120,7 +1170,7 @@ unsafe fn public_window_callback_inner( }, }); - 0 + result = ProcResult::Value(0); } winuser::WM_MOUSEWHEEL => { @@ -1130,7 +1180,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)), @@ -1138,11 +1188,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 => { @@ -1152,7 +1202,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)), @@ -1160,77 +1210,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)), @@ -1238,10 +1236,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 => { @@ -1251,7 +1249,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)), @@ -1259,10 +1257,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 => { @@ -1272,7 +1270,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)), @@ -1280,10 +1278,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 => { @@ -1293,7 +1291,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)), @@ -1301,10 +1299,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 => { @@ -1314,7 +1312,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)), @@ -1322,10 +1320,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 => { @@ -1335,7 +1333,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)), @@ -1343,10 +1341,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 => { @@ -1357,7 +1355,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)), @@ -1365,10 +1363,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 => { @@ -1379,7 +1377,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)), @@ -1387,10 +1385,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 => { @@ -1401,7 +1399,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 => { @@ -1450,7 +1448,7 @@ unsafe fn public_window_callback_inner( } } winuser::CloseTouchInputHandle(htouch); - 0 + result = ProcResult::Value(0); } winuser::WM_POINTERDOWN | winuser::WM_POINTERUPDATE | winuser::WM_POINTERUP => { @@ -1473,7 +1471,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; @@ -1486,7 +1485,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 @@ -1590,68 +1590,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_winit_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_winit_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 { @@ -1663,7 +1618,7 @@ unsafe fn public_window_callback_inner( window_id: RootWindowId(WindowId(window)), event: Focused(false), }); - 0 + result = ProcResult::Value(0); } winuser::WM_SETCURSOR => { @@ -1684,17 +1639,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; @@ -1719,7 +1669,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 @@ -1741,7 +1691,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() @@ -1936,7 +1887,7 @@ unsafe fn public_window_callback_inner( winuser::SWP_NOZORDER | winuser::SWP_NOACTIVATE, ); - 0 + result = ProcResult::Value(0); } winuser::WM_SETTINGCHANGE => { @@ -1957,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); } } }; @@ -1980,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( @@ -2062,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 / winuser::WHEEL_DELTA; - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: MouseWheel { - delta: LineDelta(0.0, delta as f32), - }, - }); - } - - 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 { - 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_winit_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) @@ -2228,3 +2088,184 @@ 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/keyboard.rs b/src/platform_impl/windows/keyboard.rs new file mode 100644 index 00000000000..00235ed018e --- /dev/null +++ b/src/platform_impl/windows/keyboard.rs @@ -0,0 +1,802 @@ +use std::{ + char, collections::HashSet, ffi::OsString, mem::MaybeUninit, os::raw::c_int, + os::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::scancode::KeyCodeExtScancode, + 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 Winit `KeyEvent` contains information which the Windows API passes to the application +/// in multiple window messages. In other words: a Winit `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), + 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), + }; + + let mut event = event_info.finalize(&mut layouts.strings); + event.logical_key = logical_key; + event.platform_specific.text_with_all_modifers = 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 { + 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 { + 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_modifers: 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, + } +} diff --git a/src/platform_impl/windows/keyboard_layout.rs b/src/platform_impl/windows/keyboard_layout.rs new file mode 100644 index 00000000000..3f1100b9b4f --- /dev/null +++ b/src/platform_impl/windows/keyboard_layout.rs @@ -0,0 +1,993 @@ +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 crate::{ + keyboard::{Key, KeyCode, ModifiersState, NativeKeyCode}, + platform::scancode::KeyCodeExtScancode, + platform_impl::platform::keyboard::ExScancode, +}; + +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; + } + } else { + if let Some(key) = self.numlock_off_keys.get(&vkey) { + return *key; + } + } + if let Some(keys) = self.keys.get(&mods) { + if let Some(key) = keys.get(&keycode) { + return *key; + } + } + 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 { + 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/minimal_ime.rs b/src/platform_impl/windows/minimal_ime.rs new file mode 100644 index 00000000000..cbf754eecce --- /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 d748e6f01f7..a72099db88b 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -13,6 +13,7 @@ pub use self::icon::WinIcon as PlatformIcon; use crate::event::DeviceId as RootDeviceId; use crate::icon::Icon; +use crate::keyboard::Key; use crate::window::Theme; #[derive(Clone)] @@ -75,6 +76,12 @@ fn wrap_device_id(id: u32) -> RootDeviceId { pub type OsError = std::io::Error; +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct KeyEventExtra { + pub text_with_all_modifers: 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 {} @@ -93,9 +100,11 @@ mod util; 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 cc04b8f7902..e56a472a028 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -1,5 +1,6 @@ #![cfg(target_os = "windows")] +use mem::MaybeUninit; use parking_lot::Mutex; use raw_window_handle::{windows::WindowsHandle, RawWindowHandle}; use std::{ @@ -661,6 +662,26 @@ impl Window { pub fn theme(&self) -> Theme { self.window_state.lock().current_theme } + + #[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 9ef55232390..19587d2880a 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -1,8 +1,10 @@ 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; @@ -34,6 +36,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, } @@ -121,6 +127,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/src/window.rs b/src/window.rs index 052f5746e0a..efa9ffa4030 100644 --- a/src/window.rs +++ b/src/window.rs @@ -406,6 +406,22 @@ impl Window { pub fn request_redraw(&self) { self.window.request_redraw() } + + /// 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. + /// + /// ## Platform-specific + /// - **Web:** Does nothing + // --------------------------- + // Developers' Note: If this cannot be implemented on every desktop platform + // at least, then this function should be provided through a platform specific + // extension trait + pub fn reset_dead_keys(&self) { + self.window.reset_dead_keys(); + } } /// Position and size functions. diff --git a/tests/serde_objects.rs b/tests/serde_objects.rs index ad729dcd1ba..b0333fa4103 100644 --- a/tests/serde_objects.rs +++ b/tests/serde_objects.rs @@ -3,10 +3,8 @@ use serde::{Deserialize, Serialize}; use winit::{ 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, }; @@ -20,12 +18,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::(); } From 45aacd840752bc1af07677da44af4911dbd701c8 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 29 Apr 2021 12:52:41 +0200 Subject: [PATCH 19/26] Use `initialFirstResponder` instead of `makeFirstResponder` (#1920) As recommended by the documentation: https://developer.apple.com/documentation/appkit/nswindow/1419366-makefirstresponder?language=objc --- src/platform_impl/macos/window.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 11c652c5589..7d16f2fce34 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -131,8 +131,6 @@ unsafe fn create_view( ns_view.setWantsLayer(YES); } - ns_window.setContentView_(*ns_view); - ns_window.makeFirstResponder_(*ns_view); (ns_view, cursor_state) }) } @@ -377,6 +375,12 @@ impl UnownedWindow { os_error!(OsError::CreationError("Couldn't create `NSView`")) })?; + // Configure the new view as the "key view" for the window + unsafe { + ns_window.setContentView_(*ns_view); + ns_window.setInitialFirstResponder_(*ns_view); + } + let input_context = unsafe { util::create_input_context(*ns_view) }; let scale_factor = unsafe { NSWindow::backingScaleFactor(*ns_window) as f64 }; From 277515636dacf3a7c8b74a1567b9148695c4b43d Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Thu, 29 Apr 2021 19:49:17 +0200 Subject: [PATCH 20/26] MacOS: Only activate after the application has finished launching (#1903) * MacOS: Only activate after the application has finished launching This fixes the main menu not responding until you refocus, at least from what I can tell - though we might have to do something similar to https://github.com/linebender/druid/pull/994 to fix it fully? * MacOS: Remove activation hack * Stop unnecessarily calling `makeKeyWindow` on initially hidden windows You can't make hidden windows the key window * Add new, simpler activation hack For activating multiple windows created before the application finished launching --- CHANGELOG.md | 1 + src/platform_impl/macos/activation_hack.rs | 208 --------------------- src/platform_impl/macos/app.rs | 23 +-- src/platform_impl/macos/app_delegate.rs | 53 +----- src/platform_impl/macos/app_state.rs | 40 +++- src/platform_impl/macos/mod.rs | 1 - src/platform_impl/macos/util/mod.rs | 1 + src/platform_impl/macos/window.rs | 12 +- 8 files changed, 50 insertions(+), 289 deletions(-) delete mode 100644 src/platform_impl/macos/activation_hack.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e62cb684e0..bda668177ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- On macOS, wait with activating the application until the application has initialized. - On macOS, fix creating new windows when the application has a main menu. - On Windows, fix fractional deltas for mouse wheel device events. - On macOS, fix segmentation fault after dropping the main window. diff --git a/src/platform_impl/macos/activation_hack.rs b/src/platform_impl/macos/activation_hack.rs deleted file mode 100644 index 6cf1960cfcf..00000000000 --- a/src/platform_impl/macos/activation_hack.rs +++ /dev/null @@ -1,208 +0,0 @@ -// Normally when you run or distribute a macOS app, it's bundled: it's in one -// of those fun little folders that you have to right click "Show Package -// Contents" on, and usually contains myriad delights including, but not -// limited to, plists, icons, and of course, your beloved executable. However, -// when you use `cargo run`, your app is unbundled - it's just a lonely, bare -// executable. -// -// Apple isn't especially fond of unbundled apps, which is to say, they seem to -// barely be supported. If you move the mouse while opening a winit window from -// an unbundled app, the window will fail to activate and be in a grayed-out -// uninteractable state. Switching to another app and back is the only way to -// get the winit window into a normal state. None of this happens if the app is -// bundled, i.e. when running via Xcode. -// -// To workaround this, we just switch focus to the Dock and then switch back to -// our app. We only do this for unbundled apps, and only when they fail to -// become active on their own. -// -// This solution was derived from this Godot PR: -// https://github.com/godotengine/godot/pull/17187 -// (which appears to be based on https://stackoverflow.com/a/7602677) -// The curious specialness of mouse motions is touched upon here: -// https://github.com/godotengine/godot/issues/8653#issuecomment-358130512 -// -// We omit the 2nd step of the solution used in Godot, since it appears to have -// no effect - I speculate that it's just technical debt picked up from the SO -// answer; the API used is fairly exotic, and was historically used for very -// old versions of macOS that didn't support `activateIgnoringOtherApps`, i.e. -// in previous versions of SDL: -// https://hg.libsdl.org/SDL/file/c0bcc39a3491/src/video/cocoa/SDL_cocoaevents.m#l322 -// -// The `performSelector` delays in the Godot solution are used for sequencing, -// since refocusing the app will fail if the call is made before it finishes -// unfocusing. The delays used there are much smaller than the ones in the -// original SO answer, presumably because they found the fastest delay that -// works reliably through trial and error. Instead of using delays, we just -// handle `applicationDidResignActive`; despite the app not activating reliably, -// that still triggers when we switch focus to the Dock. -// -// The Godot solution doesn't appear to skip the hack when an unbundled app -// activates normally. Checking for this is difficult, since if you call -// `isActive` too early, it will always be `NO`. Even though we receive -// `applicationDidResignActive` when switching focus to the Dock, we never -// receive a preceding `applicationDidBecomeActive` if the app fails to -// activate normally. I wasn't able to find a proper point in time to perform -// the `isActive` check, so we instead check for the cause of the quirk: if -// any mouse motion occurs prior to us receiving `applicationDidResignActive`, -// we assume the app failed to become active. -// -// Fun fact: this issue is still present in GLFW -// (https://github.com/glfw/glfw/issues/1515) -// -// A similar issue was found in SDL, but the resolution doesn't seem to work -// for us: https://bugzilla.libsdl.org/show_bug.cgi?id=3051 - -use super::util; -use cocoa::{ - appkit::{NSApp, NSApplicationActivateIgnoringOtherApps}, - base::id, - foundation::NSUInteger, -}; -use objc::runtime::{Object, Sel, BOOL, NO, YES}; -use std::{ - os::raw::c_void, - sync::atomic::{AtomicBool, Ordering}, -}; - -#[derive(Debug, Default)] -pub struct State { - // Indicates that the hack has either completed or been skipped. - activated: AtomicBool, - // Indicates that the mouse has moved at some point in time. - mouse_moved: AtomicBool, - // Indicates that the hack is in progress, and that we should refocus when - // the app resigns active. - needs_refocus: AtomicBool, -} - -impl State { - pub fn name() -> &'static str { - "activationHackState" - } - - pub fn new() -> *mut c_void { - let this = Box::new(Self::default()); - Box::into_raw(this) as *mut c_void - } - - pub unsafe fn free(this: *mut Self) { - Box::from_raw(this); - } - - pub unsafe fn get_ptr(obj: &Object) -> *mut Self { - let this: *mut c_void = *(*obj).get_ivar(Self::name()); - assert!(!this.is_null(), "`activationHackState` pointer was null"); - this as *mut Self - } - - pub unsafe fn set_activated(obj: &Object, value: bool) { - let this = Self::get_ptr(obj); - (*this).activated.store(value, Ordering::Release); - } - - unsafe fn get_activated(obj: &Object) -> bool { - let this = Self::get_ptr(obj); - (*this).activated.load(Ordering::Acquire) - } - - pub unsafe fn set_mouse_moved(obj: &Object, value: bool) { - let this = Self::get_ptr(obj); - (*this).mouse_moved.store(value, Ordering::Release); - } - - pub unsafe fn get_mouse_moved(obj: &Object) -> bool { - let this = Self::get_ptr(obj); - (*this).mouse_moved.load(Ordering::Acquire) - } - - pub unsafe fn set_needs_refocus(obj: &Object, value: bool) { - let this = Self::get_ptr(obj); - (*this).needs_refocus.store(value, Ordering::Release); - } - - unsafe fn get_needs_refocus(obj: &Object) -> bool { - let this = Self::get_ptr(obj); - (*this).needs_refocus.load(Ordering::Acquire) - } -} - -// This is the entry point for the hack - if the app is unbundled and a mouse -// movement occurs before the app activates, it will trigger the hack. Because -// mouse movements prior to activation are the cause of this quirk, they should -// be a reliable way to determine if the hack needs to be performed. -pub extern "C" fn mouse_moved(this: &Object, _: Sel, _: id) { - trace!("Triggered `activationHackMouseMoved`"); - unsafe { - if !State::get_activated(this) { - // We check if `CFBundleName` is undefined to determine if the - // app is unbundled. - if let None = util::app_name() { - info!("App detected as unbundled"); - unfocus(this); - } else { - info!("App detected as bundled"); - } - } - } - trace!("Completed `activationHackMouseMoved`"); -} - -// Switch focus to the dock. -unsafe fn unfocus(this: &Object) { - // We only perform the hack if the app failed to activate, since otherwise, - // there'd be a gross (but fast) flicker as it unfocused and then refocused. - // However, we only enter this function if we detect mouse movement prior - // to activation, so this should always be `NO`. - // - // Note that this check isn't necessarily reliable in detecting a violation - // of the invariant above, since it's not guaranteed that activation will - // resolve before this point. In other words, it can spuriously return `NO`. - // This is also why the mouse motion approach was chosen, since it's not - // obvious how to sequence this check - if someone knows how to, then that - // would almost surely be a cleaner approach. - let active: BOOL = msg_send![NSApp(), isActive]; - if active == YES { - error!("Unbundled app activation hack triggered on an app that's already active; this shouldn't happen!"); - } else { - info!("Performing unbundled app activation hack"); - let dock_bundle_id = util::ns_string_id_ref("com.apple.dock"); - let dock_array: id = msg_send![ - class!(NSRunningApplication), - runningApplicationsWithBundleIdentifier: *dock_bundle_id - ]; - let dock_array_len: NSUInteger = msg_send![dock_array, count]; - if dock_array_len == 0 { - error!("The Dock doesn't seem to be running, so switching focus to it is impossible"); - } else { - State::set_needs_refocus(this, true); - let dock: id = msg_send![dock_array, objectAtIndex: 0]; - // This will trigger `applicationDidResignActive`, which will in - // turn call `refocus`. - let status: BOOL = msg_send![ - dock, - activateWithOptions: NSApplicationActivateIgnoringOtherApps - ]; - if status == NO { - error!("Failed to switch focus to Dock"); - } - } - } -} - -// Switch focus back to our app, causing the user to rejoice! -pub unsafe fn refocus(this: &Object) { - if State::get_needs_refocus(this) { - State::set_needs_refocus(this, false); - let app: id = msg_send![class!(NSRunningApplication), currentApplication]; - // Simply calling `NSApp activateIgnoringOtherApps` doesn't work. The - // nuanced difference isn't clear to me, but hey, I tried. - let success: BOOL = msg_send![ - app, - activateWithOptions: NSApplicationActivateIgnoringOtherApps - ]; - if success == NO { - error!("Failed to refocus app"); - } - } -} diff --git a/src/platform_impl/macos/app.rs b/src/platform_impl/macos/app.rs index 4cec95124ca..b5a5582cc3c 100644 --- a/src/platform_impl/macos/app.rs +++ b/src/platform_impl/macos/app.rs @@ -2,14 +2,14 @@ use std::collections::VecDeque; use cocoa::{ appkit::{self, NSEvent}, - base::{id, nil}, + base::id, }; use objc::{ declare::ClassDecl, runtime::{Class, Object, Sel}, }; -use super::{activation_hack, app_state::AppState, event::EventWrapper, util, DEVICE_ID}; +use super::{app_state::AppState, event::EventWrapper, util, DEVICE_ID}; use crate::event::{DeviceEvent, ElementState, Event}; pub struct AppClass(pub *const Class); @@ -49,14 +49,14 @@ extern "C" fn send_event(this: &Object, _sel: Sel, event: id) { let key_window: id = msg_send![this, keyWindow]; let _: () = msg_send![key_window, sendEvent: event]; } else { - maybe_dispatch_device_event(this, event); + maybe_dispatch_device_event(event); let superclass = util::superclass(this); let _: () = msg_send![super(this, superclass), sendEvent: event]; } } } -unsafe fn maybe_dispatch_device_event(this: &Object, event: id) { +unsafe fn maybe_dispatch_device_event(event: id) { let event_type = event.eventType(); match event_type { appkit::NSMouseMoved @@ -98,21 +98,6 @@ unsafe fn maybe_dispatch_device_event(this: &Object, event: id) { } AppState::queue_events(events); - - // Notify the delegate when the first mouse move occurs. This is - // used for the unbundled app activation hack, which needs to know - // if any mouse motions occurred prior to the app activating. - let delegate: id = msg_send![this, delegate]; - assert_ne!(delegate, nil); - if !activation_hack::State::get_mouse_moved(&*delegate) { - activation_hack::State::set_mouse_moved(&*delegate, true); - let () = msg_send![ - delegate, - performSelector: sel!(activationHackMouseMoved:) - withObject: nil - afterDelay: 0.0 - ]; - } } appkit::NSLeftMouseDown | appkit::NSRightMouseDown | appkit::NSOtherMouseDown => { let mut events = VecDeque::with_capacity(1); diff --git a/src/platform_impl/macos/app_delegate.rs b/src/platform_impl/macos/app_delegate.rs index 9263cc121fa..df59a2a1f70 100644 --- a/src/platform_impl/macos/app_delegate.rs +++ b/src/platform_impl/macos/app_delegate.rs @@ -1,10 +1,9 @@ -use super::{activation_hack, app_state::AppState}; +use super::app_state::AppState; use cocoa::base::id; use objc::{ declare::ClassDecl, runtime::{Class, Object, Sel}, }; -use std::os::raw::c_void; pub struct AppDelegateClass(pub *const Class); unsafe impl Send for AppDelegateClass {} @@ -15,67 +14,17 @@ lazy_static! { let superclass = class!(NSResponder); let mut decl = ClassDecl::new("WinitAppDelegate", superclass).unwrap(); - decl.add_class_method(sel!(new), new as extern "C" fn(&Class, Sel) -> id); - decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel)); decl.add_method( sel!(applicationDidFinishLaunching:), did_finish_launching as extern "C" fn(&Object, Sel, id), ); - decl.add_method( - sel!(applicationDidBecomeActive:), - did_become_active as extern "C" fn(&Object, Sel, id), - ); - decl.add_method( - sel!(applicationDidResignActive:), - did_resign_active as extern "C" fn(&Object, Sel, id), - ); - - decl.add_ivar::<*mut c_void>(activation_hack::State::name()); - decl.add_method( - sel!(activationHackMouseMoved:), - activation_hack::mouse_moved as extern "C" fn(&Object, Sel, id), - ); AppDelegateClass(decl.register()) }; } -extern "C" fn new(class: &Class, _: Sel) -> id { - unsafe { - let this: id = msg_send![class, alloc]; - let this: id = msg_send![this, init]; - (*this).set_ivar( - activation_hack::State::name(), - activation_hack::State::new(), - ); - this - } -} - -extern "C" fn dealloc(this: &Object, _: Sel) { - unsafe { - activation_hack::State::free(activation_hack::State::get_ptr(this)); - } -} - extern "C" fn did_finish_launching(_: &Object, _: Sel, _: id) { trace!("Triggered `applicationDidFinishLaunching`"); AppState::launched(); trace!("Completed `applicationDidFinishLaunching`"); } - -extern "C" fn did_become_active(this: &Object, _: Sel, _: id) { - trace!("Triggered `applicationDidBecomeActive`"); - unsafe { - activation_hack::State::set_activated(this, true); - } - trace!("Completed `applicationDidBecomeActive`"); -} - -extern "C" fn did_resign_active(this: &Object, _: Sel, _: id) { - trace!("Triggered `applicationDidResignActive`"); - unsafe { - activation_hack::refocus(this); - } - trace!("Completed `applicationDidResignActive`"); -} diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index 4f20e21694c..44525c86920 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -13,10 +13,11 @@ use std::{ }; use cocoa::{ - appkit::{NSApp, NSWindow}, + appkit::{NSApp, NSApplication, NSWindow}, base::{id, nil}, foundation::{NSAutoreleasePool, NSSize}, }; +use objc::runtime::YES; use crate::{ dpi::LogicalSize, @@ -273,6 +274,12 @@ impl AppState { } pub fn launched() { + unsafe { + let ns_app = NSApp(); + window_activation_hack(ns_app); + // TODO: Consider allowing the user to specify they don't want their application activated + ns_app.activateIgnoringOtherApps_(YES); + }; HANDLER.set_ready(); HANDLER.waker().start(); // The menubar initialization should be before the `NewEvents` event, to allow overriding @@ -419,3 +426,34 @@ impl AppState { } } } + +/// A hack to make activation of multiple windows work when creating them before +/// `applicationDidFinishLaunching:` / `Event::Event::NewEvents(StartCause::Init)`. +/// +/// Alternative to this would be the user calling `window.set_visible(true)` in +/// `StartCause::Init`. +/// +/// If this becomes too bothersome to maintain, it can probably be removed +/// without too much damage. +unsafe fn window_activation_hack(ns_app: id) { + // Get the application's windows + // TODO: Proper ordering of the windows + let ns_windows: id = msg_send![ns_app, windows]; + let ns_enumerator: id = msg_send![ns_windows, objectEnumerator]; + loop { + // Enumerate over the windows + let ns_window: id = msg_send![ns_enumerator, nextObject]; + if ns_window == nil { + break; + } + // And call `makeKeyAndOrderFront` if it was called on the window in `UnownedWindow::new` + // This way we preserve the user's desired initial visiblity status + // TODO: Also filter on the type/"level" of the window, and maybe other things? + if ns_window.isVisible() == YES { + trace!("Activating visible window"); + ns_window.makeKeyAndOrderFront_(nil); + } else { + trace!("Skipping activating invisible window"); + } + } +} diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 09b61a5d6d4..8c22e588245 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -1,6 +1,5 @@ #![cfg(target_os = "macos")] -mod activation_hack; mod app; mod app_delegate; mod app_state; diff --git a/src/platform_impl/macos/util/mod.rs b/src/platform_impl/macos/util/mod.rs index a6187a50cec..2ebfe56cb5c 100644 --- a/src/platform_impl/macos/util/mod.rs +++ b/src/platform_impl/macos/util/mod.rs @@ -106,6 +106,7 @@ pub unsafe fn ns_string_id_ref(s: &str) -> IdRef { IdRef::new(NSString::alloc(nil).init_str(s)) } +#[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]; let dict: id = msg_send![bundle, infoDictionary]; diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 7d16f2fce34..435aab43154 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -359,7 +359,7 @@ impl UnownedWindow { let pool = unsafe { NSAutoreleasePool::new(nil) }; - let ns_app = create_app(pl_attribs.activation_policy).ok_or_else(|| { + create_app(pl_attribs.activation_policy).ok_or_else(|| { unsafe { pool.drain() }; os_error!(OsError::CreationError("Couldn't create `NSApplication`")) })?; @@ -391,7 +391,6 @@ impl UnownedWindow { ns_window.setBackgroundColor_(NSColor::clearColor(nil)); } - ns_app.activateIgnoringOtherApps_(YES); win_attribs.min_inner_size.map(|dim| { let logical_dim = dim.to_logical(scale_factor); set_min_inner_size(*ns_window, logical_dim) @@ -441,12 +440,9 @@ impl UnownedWindow { // Setting the window as key has to happen *after* we set the fullscreen // state, since otherwise we'll briefly see the window at normal size // before it transitions. - unsafe { - if visible { - window.ns_window.makeKeyAndOrderFront_(nil); - } else { - window.ns_window.makeKeyWindow(); - } + if visible { + // Tightly linked with `app_state::window_activation_hack` + unsafe { window.ns_window.makeKeyAndOrderFront_(nil) }; } if maximized { From 0986fae06665d8fb0e1866f57c2efabdc7abf4d3 Mon Sep 17 00:00:00 2001 From: z4122 <412213484@qq.com> Date: Fri, 30 Apr 2021 17:30:09 +0800 Subject: [PATCH 21/26] Add accept_first_mouse for macOS (#1882) * feat: add accept_first_mouse for macOS * Update the changelog Co-authored-by: Artur Kovacs --- CHANGELOG.md | 1 + src/platform_impl/macos/view.rs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bda668177ba..ae9c7df5bea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - On macOS, emit `RedrawRequested` events immediately while the window is being resized. - Implement `Default`, `Hash`, and `Eq` for `LogicalPosition`, `PhysicalPosition`, `LogicalSize`, and `PhysicalSize`. - On macOS, initialize the Menu Bar with minimal defaults. +- On macOS, change the default behavior for first click when the window was unfocused. Now the window becomes focused and then emits a `MouseInput` event on a "first mouse click". # 0.24.0 (2020-12-09) diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs index aa7109f8e17..5da5b536974 100644 --- a/src/platform_impl/macos/view.rs +++ b/src/platform_impl/macos/view.rs @@ -255,6 +255,10 @@ lazy_static! { sel!(frameDidChange:), frame_did_change as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(acceptsFirstMouse:), + accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL, + ); decl.add_ivar::<*mut c_void>("winitState"); decl.add_ivar::("markedText"); let protocol = Protocol::get("NSTextInputClient").unwrap(); @@ -1078,3 +1082,7 @@ extern "C" fn pressure_change_with_event(this: &Object, _sel: Sel, event: id) { extern "C" fn wants_key_down_for_event(_this: &Object, _sel: Sel, _event: id) -> BOOL { YES } + +extern "C" fn accepts_first_mouse(_this: &Object, _sel: Sel, _event: id) -> BOOL { + YES +} From cdeb1c3828885852ebcf3119f082fba7f26749a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C3=BAr=20Kov=C3=A1cs?= Date: Fri, 30 Apr 2021 11:31:28 +0200 Subject: [PATCH 22/26] Require setting the activation policy on the event loop (#1922) * Require setting the activation policy on the event loop * Run cargo fmt * Update changelog * Fixes and tweaks from review * Correct comment in app_state.rs Co-authored-by: Mads Marquart --- .gitignore | 1 + CHANGELOG.md | 1 + src/platform/macos.rs | 28 ++++++++----- src/platform_impl/macos/app_delegate.rs | 54 +++++++++++++++++++++++-- src/platform_impl/macos/app_state.rs | 38 +++++++++++++---- src/platform_impl/macos/event_loop.rs | 5 ++- src/platform_impl/macos/menu.rs | 5 +-- src/platform_impl/macos/mod.rs | 1 + src/platform_impl/macos/window.rs | 33 ++------------- 9 files changed, 110 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 5e6640bc9fc..5db37e16d1b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ rls/ *.ts *.js #*# +.DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ae9c7df5bea..cb69ddefcdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- **Breaking:** On macOS, replace `WindowBuilderExtMacOS::with_activation_policy` with `EventLoopExtMacOS::set_activation_policy` - On macOS, wait with activating the application until the application has initialized. - On macOS, fix creating new windows when the application has a main menu. - On Windows, fix fractional deltas for mouse wheel device events. diff --git a/src/platform/macos.rs b/src/platform/macos.rs index f66076b9f56..602806915eb 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -4,8 +4,9 @@ use std::os::raw::c_void; use crate::{ dpi::LogicalSize, - event_loop::EventLoopWindowTarget, + event_loop::{EventLoop, EventLoopWindowTarget}, monitor::MonitorHandle, + platform_impl::get_aux_state_mut, window::{Window, WindowBuilder}, }; @@ -100,8 +101,6 @@ impl Default for ActivationPolicy { /// - `with_titlebar_buttons_hidden` /// - `with_fullsize_content_view` pub trait WindowBuilderExtMacOS { - /// Sets the activation policy for the window being built. - fn with_activation_policy(self, activation_policy: ActivationPolicy) -> WindowBuilder; /// Enables click-and-drag behavior for the entire window, not just the titlebar. fn with_movable_by_window_background(self, movable_by_window_background: bool) -> WindowBuilder; @@ -122,12 +121,6 @@ pub trait WindowBuilderExtMacOS { } impl WindowBuilderExtMacOS for WindowBuilder { - #[inline] - fn with_activation_policy(mut self, activation_policy: ActivationPolicy) -> WindowBuilder { - self.platform_specific.activation_policy = activation_policy; - self - } - #[inline] fn with_movable_by_window_background( mut self, @@ -186,6 +179,23 @@ impl WindowBuilderExtMacOS for WindowBuilder { } } +pub trait EventLoopExtMacOS { + /// Sets the activation policy for the application. It is set to + /// `NSApplicationActivationPolicyRegular` by default. + /// + /// This function only takes effect if it's called before calling [`run`](crate::event_loop::EventLoop::run) or + /// [`run_return`](crate::platform::run_return::EventLoopExtRunReturn::run_return) + fn set_activation_policy(&mut self, activation_policy: ActivationPolicy); +} +impl EventLoopExtMacOS for EventLoop { + #[inline] + fn set_activation_policy(&mut self, activation_policy: ActivationPolicy) { + unsafe { + get_aux_state_mut(&**self.event_loop.delegate).activation_policy = activation_policy; + } + } +} + /// Additional methods on `MonitorHandle` that are specific to MacOS. pub trait MonitorHandleExtMacOS { /// Returns the identifier of the monitor for Cocoa. diff --git a/src/platform_impl/macos/app_delegate.rs b/src/platform_impl/macos/app_delegate.rs index df59a2a1f70..e8891c8252e 100644 --- a/src/platform_impl/macos/app_delegate.rs +++ b/src/platform_impl/macos/app_delegate.rs @@ -1,9 +1,23 @@ -use super::app_state::AppState; +use crate::{platform::macos::ActivationPolicy, platform_impl::platform::app_state::AppState}; + use cocoa::base::id; use objc::{ declare::ClassDecl, runtime::{Class, Object, Sel}, }; +use std::{ + cell::{RefCell, RefMut}, + os::raw::c_void, +}; + +static AUX_DELEGATE_STATE_NAME: &str = "auxState"; + +pub struct AuxDelegateState { + /// We store this value in order to be able to defer setting the activation policy until + /// after the app has finished launching. If the activation policy is set earlier, the + /// menubar is initially unresponsive on macOS 10.15 for example. + pub activation_policy: ActivationPolicy, +} pub struct AppDelegateClass(pub *const Class); unsafe impl Send for AppDelegateClass {} @@ -14,17 +28,51 @@ lazy_static! { let superclass = class!(NSResponder); let mut decl = ClassDecl::new("WinitAppDelegate", superclass).unwrap(); + decl.add_class_method(sel!(new), new as extern "C" fn(&Class, Sel) -> id); + decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel)); + decl.add_method( sel!(applicationDidFinishLaunching:), did_finish_launching as extern "C" fn(&Object, Sel, id), ); + decl.add_ivar::<*mut c_void>(AUX_DELEGATE_STATE_NAME); AppDelegateClass(decl.register()) }; } -extern "C" fn did_finish_launching(_: &Object, _: Sel, _: id) { +/// Safety: Assumes that Object is an instance of APP_DELEGATE_CLASS +pub unsafe fn get_aux_state_mut(this: &Object) -> RefMut<'_, AuxDelegateState> { + let ptr: *mut c_void = *this.get_ivar(AUX_DELEGATE_STATE_NAME); + // Watch out that this needs to be the correct type + (*(ptr as *mut RefCell)).borrow_mut() +} + +extern "C" fn new(class: &Class, _: Sel) -> id { + unsafe { + let this: id = msg_send![class, alloc]; + let this: id = msg_send![this, init]; + (*this).set_ivar( + AUX_DELEGATE_STATE_NAME, + Box::into_raw(Box::new(RefCell::new(AuxDelegateState { + activation_policy: ActivationPolicy::Regular, + }))) as *mut c_void, + ); + this + } +} + +extern "C" fn dealloc(this: &Object, _: Sel) { + unsafe { + let state_ptr: *mut c_void = *(this.get_ivar(AUX_DELEGATE_STATE_NAME)); + // As soon as the box is constructed it is immediately dropped, releasing the underlying + // memory + Box::from_raw(state_ptr as *mut RefCell); + } +} + +extern "C" fn did_finish_launching(this: &Object, _: Sel, _: id) { trace!("Triggered `applicationDidFinishLaunching`"); - AppState::launched(); + AppState::launched(this); trace!("Completed `applicationDidFinishLaunching`"); } diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index 44525c86920..404dc854044 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -19,17 +19,23 @@ use cocoa::{ }; use objc::runtime::YES; +use objc::runtime::Object; + use crate::{ dpi::LogicalSize, event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoopWindowTarget as RootWindowTarget}, - platform_impl::platform::{ - event::{EventProxy, EventWrapper}, - event_loop::{post_dummy_event, PanicInfo}, - menu, - observer::{CFRunLoopGetMain, CFRunLoopWakeUp, EventLoopWaker}, - util::{IdRef, Never}, - window::get_window_id, + platform::macos::ActivationPolicy, + platform_impl::{ + get_aux_state_mut, + platform::{ + event::{EventProxy, EventWrapper}, + event_loop::{post_dummy_event, PanicInfo}, + menu, + observer::{CFRunLoopGetMain, CFRunLoopWakeUp, EventLoopWaker}, + util::{IdRef, Never}, + window::get_window_id, + }, }, window::WindowId, }; @@ -273,7 +279,8 @@ impl AppState { HANDLER.callback.lock().unwrap().take(); } - pub fn launched() { + pub fn launched(app_delegate: &Object) { + apply_activation_policy(app_delegate); unsafe { let ns_app = NSApp(); window_activation_hack(ns_app); @@ -457,3 +464,18 @@ unsafe fn window_activation_hack(ns_app: id) { } } } +fn apply_activation_policy(app_delegate: &Object) { + unsafe { + use cocoa::appkit::NSApplicationActivationPolicy::*; + let ns_app = NSApp(); + // We need to delay setting the activation policy and activating the app + // until `applicationDidFinishLaunching` has been called. Otherwise the + // menu bar won't be interactable. + let act_pol = get_aux_state_mut(app_delegate).activation_policy; + ns_app.setActivationPolicy_(match act_pol { + ActivationPolicy::Regular => NSApplicationActivationPolicyRegular, + ActivationPolicy::Accessory => NSApplicationActivationPolicyAccessory, + ActivationPolicy::Prohibited => NSApplicationActivationPolicyProhibited, + }); + } +} diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index afebcd47fd7..2142c2851b5 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -87,6 +87,8 @@ impl EventLoopWindowTarget { } pub struct EventLoop { + pub(crate) delegate: IdRef, + window_target: Rc>, panic_info: Rc, @@ -97,7 +99,6 @@ pub struct EventLoop { /// into a strong reference in order to call the callback but then the /// strong reference should be dropped as soon as possible. _callback: Option, &RootWindowTarget, &mut ControlFlow)>>>, - _delegate: IdRef, } impl EventLoop { @@ -122,13 +123,13 @@ impl EventLoop { let panic_info: Rc = Default::default(); setup_control_flow_observers(Rc::downgrade(&panic_info)); EventLoop { + delegate, window_target: Rc::new(RootWindowTarget { p: Default::default(), _marker: PhantomData, }), panic_info, _callback: None, - _delegate: delegate, } } diff --git a/src/platform_impl/macos/menu.rs b/src/platform_impl/macos/menu.rs index a0d61edd741..1a8cd2ee7e5 100644 --- a/src/platform_impl/macos/menu.rs +++ b/src/platform_impl/macos/menu.rs @@ -1,7 +1,4 @@ -use cocoa::appkit::{ - NSApp, NSApplication, NSApplicationActivationPolicyRegular, NSEventModifierFlags, NSMenu, - NSMenuItem, -}; +use cocoa::appkit::{NSApp, NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem}; use cocoa::base::{nil, selector}; use cocoa::foundation::{NSAutoreleasePool, NSProcessInfo, NSString}; use objc::{ diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 8c22e588245..5254a9932d8 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -17,6 +17,7 @@ mod window_delegate; use std::{fmt, ops::Deref, sync::Arc}; pub use self::{ + app_delegate::{get_aux_state_mut, AuxDelegateState}, event_loop::{EventLoop, EventLoopWindowTarget, Proxy as EventLoopProxy}, monitor::{MonitorHandle, VideoMode}, window::{Id as WindowId, PlatformSpecificWindowBuilderAttributes, UnownedWindow}, diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 435aab43154..50c11fe8429 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -16,7 +16,7 @@ use crate::{ error::{ExternalError, NotSupportedError, OsError as RootOsError}, icon::Icon, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, - platform::macos::{ActivationPolicy, WindowExtMacOS}, + platform::macos::WindowExtMacOS, platform_impl::platform::{ app_state::AppState, app_state::INTERRUPT_EVENT_LOOP_EXIT, @@ -34,9 +34,8 @@ use crate::{ }; use cocoa::{ appkit::{ - self, CGFloat, NSApp, NSApplication, NSApplicationActivationPolicy, - NSApplicationPresentationOptions, NSColor, NSRequestUserAttentionType, NSScreen, NSView, - NSWindow, NSWindowButton, NSWindowStyleMask, + self, CGFloat, NSApp, NSApplication, NSApplicationPresentationOptions, NSColor, + NSRequestUserAttentionType, NSScreen, NSView, NSWindow, NSWindowButton, NSWindowStyleMask, }, base::{id, nil}, foundation::{NSAutoreleasePool, NSDictionary, NSPoint, NSRect, NSSize}, @@ -64,7 +63,6 @@ pub fn get_window_id(window_cocoa_id: id) -> Id { #[derive(Clone)] pub struct PlatformSpecificWindowBuilderAttributes { - pub activation_policy: ActivationPolicy, pub movable_by_window_background: bool, pub titlebar_transparent: bool, pub title_hidden: bool, @@ -80,7 +78,6 @@ impl Default for PlatformSpecificWindowBuilderAttributes { #[inline] fn default() -> Self { Self { - activation_policy: Default::default(), movable_by_window_background: false, titlebar_transparent: false, title_hidden: false, @@ -94,24 +91,6 @@ impl Default for PlatformSpecificWindowBuilderAttributes { } } -fn create_app(activation_policy: ActivationPolicy) -> Option { - unsafe { - let ns_app = NSApp(); - if ns_app == nil { - None - } else { - // TODO: Move ActivationPolicy from an attribute on the window to something on the EventLoop - use self::NSApplicationActivationPolicy::*; - ns_app.setActivationPolicy_(match activation_policy { - ActivationPolicy::Regular => NSApplicationActivationPolicyRegular, - ActivationPolicy::Accessory => NSApplicationActivationPolicyAccessory, - ActivationPolicy::Prohibited => NSApplicationActivationPolicyProhibited, - }); - Some(ns_app) - } - } -} - unsafe fn create_view( ns_window: id, pl_attribs: &PlatformSpecificWindowBuilderAttributes, @@ -358,12 +337,6 @@ impl UnownedWindow { trace!("Creating new window"); let pool = unsafe { NSAutoreleasePool::new(nil) }; - - create_app(pl_attribs.activation_policy).ok_or_else(|| { - unsafe { pool.drain() }; - os_error!(OsError::CreationError("Couldn't create `NSApplication`")) - })?; - let ns_window = create_window(&win_attribs, &pl_attribs).ok_or_else(|| { unsafe { pool.drain() }; os_error!(OsError::CreationError("Couldn't create `NSWindow`")) From 0152508a393ccb46764a733f489a25cc23885a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C3=BAr=20Kov=C3=A1cs?= Date: Fri, 30 Apr 2021 13:34:50 +0200 Subject: [PATCH 23/26] Allow preventing the creation of the default menu (#1923) * Allow preventing the creation of the default menu * Use more grammar friendly naming * Update the changelog --- CHANGELOG.md | 2 +- src/platform/macos.rs | 16 ++++++++++++++++ src/platform_impl/macos/app_delegate.rs | 3 +++ src/platform_impl/macos/app_state.rs | 9 ++++++--- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb69ddefcdd..c608513c88f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ - On Windows, added `WindowExtWindows::set_enable` to allow creating modal popup windows. - On macOS, emit `RedrawRequested` events immediately while the window is being resized. - Implement `Default`, `Hash`, and `Eq` for `LogicalPosition`, `PhysicalPosition`, `LogicalSize`, and `PhysicalSize`. -- On macOS, initialize the Menu Bar with minimal defaults. +- On macOS, initialize the Menu Bar with minimal defaults. (Can be prevented using `enable_default_menu_creation`) - On macOS, change the default behavior for first click when the window was unfocused. Now the window becomes focused and then emits a `MouseInput` event on a "first mouse click". # 0.24.0 (2020-12-09) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 602806915eb..6e7d9a59642 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -186,6 +186,15 @@ pub trait EventLoopExtMacOS { /// This function only takes effect if it's called before calling [`run`](crate::event_loop::EventLoop::run) or /// [`run_return`](crate::platform::run_return::EventLoopExtRunReturn::run_return) fn set_activation_policy(&mut self, activation_policy: ActivationPolicy); + + /// Used to prevent a default menubar menu from getting created + /// + /// The default menu creation is enabled by default. + /// + /// This function only takes effect if it's called before calling + /// [`run`](crate::event_loop::EventLoop::run) or + /// [`run_return`](crate::platform::run_return::EventLoopExtRunReturn::run_return) + fn enable_default_menu_creation(&mut self, enable: bool); } impl EventLoopExtMacOS for EventLoop { #[inline] @@ -194,6 +203,13 @@ impl EventLoopExtMacOS for EventLoop { get_aux_state_mut(&**self.event_loop.delegate).activation_policy = activation_policy; } } + + #[inline] + fn enable_default_menu_creation(&mut self, enable: bool) { + unsafe { + get_aux_state_mut(&**self.event_loop.delegate).create_default_menu = enable; + } + } } /// Additional methods on `MonitorHandle` that are specific to MacOS. diff --git a/src/platform_impl/macos/app_delegate.rs b/src/platform_impl/macos/app_delegate.rs index e8891c8252e..d9e7937fd5f 100644 --- a/src/platform_impl/macos/app_delegate.rs +++ b/src/platform_impl/macos/app_delegate.rs @@ -17,6 +17,8 @@ pub struct AuxDelegateState { /// after the app has finished launching. If the activation policy is set earlier, the /// menubar is initially unresponsive on macOS 10.15 for example. pub activation_policy: ActivationPolicy, + + pub create_default_menu: bool, } pub struct AppDelegateClass(pub *const Class); @@ -56,6 +58,7 @@ extern "C" fn new(class: &Class, _: Sel) -> id { AUX_DELEGATE_STATE_NAME, Box::into_raw(Box::new(RefCell::new(AuxDelegateState { activation_policy: ActivationPolicy::Regular, + create_default_menu: true, }))) as *mut c_void, ); this diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index 404dc854044..35c6d5844ee 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -289,9 +289,12 @@ impl AppState { }; HANDLER.set_ready(); HANDLER.waker().start(); - // The menubar initialization should be before the `NewEvents` event, to allow overriding - // of the default menu in the event - menu::initialize(); + let create_default_menu = unsafe { get_aux_state_mut(app_delegate).create_default_menu }; + if create_default_menu { + // The menubar initialization should be before the `NewEvents` event, to allow + // overriding of the default menu even if it's created + menu::initialize(); + } HANDLER.set_in_callback(true); HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::NewEvents( StartCause::Init, From 41d9826ee9e2a05eca6805aa94655efd98b0451c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20R=C3=B8yset?= Date: Tue, 4 May 2021 20:00:11 +0200 Subject: [PATCH 24/26] Fix incorrect changelog entry for #1524 (#1916) * Fix incorrect changelog entry for #1524 * Fix incorrect changelog entry for #1524 --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c608513c88f..192ce58bf43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,7 +66,7 @@ - On X11, fix deadlock when calling `set_fullscreen_inner`. - On Web, prevent the webpage from scrolling when the user is focused on a winit canvas - On Web, calling `window.set_cursor_icon` no longer breaks HiDPI scaling -- On Windows, drag and drop is now optional and must be enabled with `WindowBuilderExtWindows::with_drag_and_drop(true)`. +- On Windows, drag and drop is now optional (enabled by default) and can be disabled with `WindowBuilderExtWindows::with_drag_and_drop(false)`. - On Wayland, fix deadlock when calling to `set_inner_size` from a callback. - On macOS, add `hide__other_applications` to `EventLoopWindowTarget` via existing `EventLoopWindowTargetExtMacOS` trait. `hide_other_applications` will hide other applications by calling `-[NSApplication hideOtherApplications: nil]`. - On android added support for `run_return`. @@ -139,7 +139,6 @@ - On Web, replaced zero timeout for `ControlFlow::Poll` with `requestAnimationFrame` - On Web, fix a possible panic during event handling - On macOS, fix `EventLoopProxy` leaking memory for every instance. -- On Windows, drag and drop can now be disabled with `WindowBuilderExtWindows::with_drag_and_drop(false)`. # 0.22.0 (2020-03-09) From 078b9719cc3ba06630291d5bc05c90787bd84c4f Mon Sep 17 00:00:00 2001 From: Luis Wirth <37505890+LU15W1R7H@users.noreply.github.com> Date: Sun, 9 May 2021 00:56:52 +0200 Subject: [PATCH 25/26] implement mint conversions (#1930) Implement conversions for [mint](https://docs.rs/mint) (math interoperability standard types). - `impl From for {Physical, Logical}Position` - `impl From<{Physical, Logical}Position> for mint::Point2` - `impl From for {Physical, Logical}Size` - `impl From<{Physical, Logical}Size> for mint::Vector2` --- CHANGELOG.md | 1 + Cargo.toml | 1 + README.md | 1 + src/dpi.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 192ce58bf43..32da86f71ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - Implement `Default`, `Hash`, and `Eq` for `LogicalPosition`, `PhysicalPosition`, `LogicalSize`, and `PhysicalSize`. - On macOS, initialize the Menu Bar with minimal defaults. (Can be prevented using `enable_default_menu_creation`) - On macOS, change the default behavior for first click when the window was unfocused. Now the window becomes focused and then emits a `MouseInput` event on a "first mouse click". +- Implement mint (math interoperability standard types) conversions (under feature flag `mint`). # 0.24.0 (2020-12-09) diff --git a/Cargo.toml b/Cargo.toml index 1af7d4d23e3..45916253b25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ log = "0.4" serde = { version = "1", optional = true, features = ["serde_derive"] } raw-window-handle = "0.3" bitflags = "1" +mint = { version = "0.5.6", optional = true } [dev-dependencies] image = "0.23.12" diff --git a/README.md b/README.md index 8ba6518d4a8..6dd8c8d59c4 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Winit provides the following features, which can be enabled in your `Cargo.toml` * `serde`: Enables serialization/deserialization of certain types with [Serde](https://crates.io/crates/serde). * `x11` (enabled by default): On Unix platform, compiles with the X11 backend * `wayland` (enabled by default): On Unix platform, compiles with the Wayland backend +* `mint`: Enables mint (math interoperability standard types) conversions. ### Platform-specific usage diff --git a/src/dpi.rs b/src/dpi.rs index 35458452f4f..ac08952ffac 100644 --- a/src/dpi.rs +++ b/src/dpi.rs @@ -228,6 +228,23 @@ impl Into<[X; 2]> for LogicalPosition

{ } } +#[cfg(feature = "mint")] +impl From> for LogicalPosition

{ + fn from(mint: mint::Point2

) -> Self { + Self::new(mint.x, mint.y) + } +} + +#[cfg(feature = "mint")] +impl From> for mint::Point2

{ + fn from(winit: LogicalPosition

) -> Self { + mint::Point2 { + x: winit.x, + y: winit.y, + } + } +} + /// A position represented in physical pixels. #[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -293,6 +310,23 @@ impl Into<[X; 2]> for PhysicalPosition

{ } } +#[cfg(feature = "mint")] +impl From> for PhysicalPosition

{ + fn from(mint: mint::Point2

) -> Self { + Self::new(mint.x, mint.y) + } +} + +#[cfg(feature = "mint")] +impl From> for mint::Point2

{ + fn from(winit: PhysicalPosition

) -> Self { + mint::Point2 { + x: winit.x, + y: winit.y, + } + } +} + /// A size represented in logical pixels. #[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -358,6 +392,23 @@ impl Into<[X; 2]> for LogicalSize

{ } } +#[cfg(feature = "mint")] +impl From> for LogicalSize

{ + fn from(mint: mint::Vector2

) -> Self { + Self::new(mint.x, mint.y) + } +} + +#[cfg(feature = "mint")] +impl From> for mint::Vector2

{ + fn from(winit: LogicalSize

) -> Self { + mint::Vector2 { + x: winit.width, + y: winit.height, + } + } +} + /// A size represented in physical pixels. #[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -420,6 +471,23 @@ impl Into<[X; 2]> for PhysicalSize

{ } } +#[cfg(feature = "mint")] +impl From> for PhysicalSize

{ + fn from(mint: mint::Vector2

) -> Self { + Self::new(mint.x, mint.y) + } +} + +#[cfg(feature = "mint")] +impl From> for mint::Vector2

{ + fn from(winit: PhysicalSize

) -> Self { + mint::Vector2 { + x: winit.width, + y: winit.height, + } + } +} + /// A size that's either physical or logical. #[derive(Debug, Copy, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] From 1ef0b490d793f5a9ed8e5143de575a22cef65adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Art=C3=BAr=20Kov=C3=A1cs?= Date: Sun, 9 May 2021 19:29:19 +0200 Subject: [PATCH 26/26] Fix the `drag_window` example --- examples/drag_window.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/drag_window.rs b/examples/drag_window.rs index a408c7c7223..58920a09281 100644 --- a/examples/drag_window.rs +++ b/examples/drag_window.rs @@ -1,9 +1,8 @@ use simple_logger::SimpleLogger; use winit::{ - 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}, }; @@ -43,10 +42,10 @@ 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: Key::Character("x"), .. }, ..