diff --git a/.changes/feat-device-events.md b/.changes/feat-device-events.md new file mode 100644 index 000000000000..13852dfe39f3 --- /dev/null +++ b/.changes/feat-device-events.md @@ -0,0 +1,9 @@ +--- +"tauri-runtime": "minor:feat" +"tauri-runtime-wry": "minor:feat" +"tauri": "minor:feat" +--- + +Adds a callback method to App builder that is called with device events and Apphandle. +Update Runtime Trait to add a device event callback method +Update wry and mock runtime with this method diff --git a/Cargo.lock b/Cargo.lock index 7009dcf42aa3..6bee6073435e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4173,6 +4173,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keyboard-types" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6e0f18953c66af118a70064505bd3780a226d65b06553b7293fb8933067967" +dependencies = [ + "bitflags 2.7.0", + "serde", +] + [[package]] name = "konst" version = "0.3.16" @@ -4606,7 +4616,7 @@ dependencies = [ "crossbeam-channel", "dpi", "gtk", - "keyboard-types", + "keyboard-types 0.7.0", "libxdo", "objc2 0.6.0", "objc2-app-kit", @@ -8759,6 +8769,7 @@ dependencies = [ "gtk", "http 1.2.0", "jni", + "keyboard-types 0.8.0", "objc2 0.6.0", "objc2-ui-kit", "raw-window-handle", diff --git a/crates/tauri-runtime-wry/src/lib.rs b/crates/tauri-runtime-wry/src/lib.rs index ac5544a0690a..7c40353830f4 100644 --- a/crates/tauri-runtime-wry/src/lib.rs +++ b/crates/tauri-runtime-wry/src/lib.rs @@ -17,6 +17,7 @@ use http::Request; use raw_window_handle::{DisplayHandle, HasDisplayHandle, HasWindowHandle}; use tauri_runtime::{ + device_events::DeviceEventFilter, dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}, monitor::Monitor, webview::{DetachedWebview, DownloadEvent, PendingWebview, WebviewIpcHandler}, @@ -24,9 +25,9 @@ use tauri_runtime::{ CursorIcon, DetachedWindow, DetachedWindowWebview, DragDropEvent, PendingWindow, RawWindow, WebviewEvent, WindowBuilder, WindowBuilderBase, WindowEvent, WindowId, WindowSizeConstraints, }, - Cookie, DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Icon, - ProgressBarState, ProgressBarStatus, Result, RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs, - UserAttentionType, UserEvent, WebviewDispatch, WebviewEventId, WindowDispatch, WindowEventId, + Cookie, Error, EventLoopProxy, ExitRequestedEventAction, Icon, ProgressBarState, + ProgressBarStatus, Result, RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, + UserEvent, WebviewDispatch, WebviewEventId, WindowDispatch, WindowEventId, }; #[cfg(target_vendor = "apple")] @@ -37,17 +38,6 @@ use tao::platform::macos::{EventLoopWindowTargetExtMacOS, WindowBuilderExtMacOS} use tao::platform::unix::{WindowBuilderExtUnix, WindowExtUnix}; #[cfg(windows)] use tao::platform::windows::{WindowBuilderExtWindows, WindowExtWindows}; -#[cfg(windows)] -use webview2_com::FocusChangedEventHandler; -#[cfg(windows)] -use windows::Win32::Foundation::HWND; -#[cfg(target_os = "ios")] -use wry::WebViewBuilderExtIos; -#[cfg(windows)] -use wry::WebViewBuilderExtWindows; -#[cfg(target_vendor = "apple")] -use wry::{WebViewBuilderExtDarwin, WebViewExtDarwin}; - use tao::{ dpi::{ LogicalPosition as TaoLogicalPosition, LogicalSize as TaoLogicalSize, @@ -66,6 +56,7 @@ use tao::{ UserAttentionType as TaoUserAttentionType, }, }; + #[cfg(desktop)] use tauri_utils::config::PreventOverflowConfig; #[cfg(target_os = "macos")] @@ -75,10 +66,20 @@ use tauri_utils::{ Theme, }; use url::Url; +#[cfg(windows)] +use webview2_com::FocusChangedEventHandler; +#[cfg(windows)] +use windows::Win32::Foundation::HWND; +#[cfg(target_os = "ios")] +use wry::WebViewBuilderExtIos; +#[cfg(windows)] +use wry::WebViewBuilderExtWindows; use wry::{ DragDropEvent as WryDragDropEvent, ProxyConfig, ProxyEndpoint, WebContext as WryWebContext, WebView, WebViewBuilder, }; +#[cfg(target_vendor = "apple")] +use wry::{WebViewBuilderExtDarwin, WebViewExtDarwin}; pub use tao; pub use tao::window::{Window, WindowBuilder as TaoWindowBuilder, WindowId as TaoWindowId}; @@ -130,6 +131,7 @@ use std::{ pub type WebviewId = u32; type IpcHandler = dyn Fn(Request) + 'static; +mod map_device_event; mod monitor; #[cfg(any( windows, @@ -236,6 +238,22 @@ pub(crate) fn send_user_message( .map_err(|_| Error::FailedToSendMessage) } } +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +/// A unique identifier for a device. +/// There is no meaning to the value of this identifier, it is just a unique identifier. +pub struct DeviceId(tao::event::DeviceId); + +impl tauri_runtime::device_events::DeviceId for DeviceId { + unsafe fn dummy() -> Self { + DeviceId(unsafe { tao::event::DeviceId::dummy() }) + } +} + +type DeviceEventCallback = Arc< + Mutex< + Option>, + >, +>; #[derive(Clone)] pub struct Context { @@ -244,6 +262,7 @@ pub struct Context { pub proxy: TaoEventLoopProxy>, main_thread: DispatcherMainThreadContext, plugins: Arc + Send>>>>, + device_event_callback: DeviceEventCallback, next_window_id: Arc, next_webview_id: Arc, next_window_event_id: Arc, @@ -2707,6 +2726,7 @@ impl Wry { #[cfg(feature = "tracing")] active_tracing_spans: Default::default(), }, + device_event_callback: Default::default(), plugins: Default::default(), next_window_id: Default::default(), next_webview_id: Default::default(), @@ -2947,12 +2967,26 @@ impl Runtime for Wry { self.event_loop.hide_application(); } - fn set_device_event_filter(&mut self, filter: DeviceEventFilter) { + fn set_device_event_filter(&self, filter: DeviceEventFilter) { self .event_loop .set_device_event_filter(DeviceEventFilterWrapper::from(filter).0); } + type DeviceId = DeviceId; + + fn set_device_event_callback(&self, callback: F) + where + F: FnMut(Self::DeviceId, tauri_runtime::device_events::DeviceEvent) + Send + 'static, + { + self + .context + .device_event_callback + .lock() + .unwrap() + .replace(Box::new(callback)); + } + #[cfg(desktop)] fn run_iteration) + 'static>(&mut self, mut callback: F) { use tao::platform::run_return::EventLoopExtRunReturn; @@ -2960,6 +2994,7 @@ impl Runtime for Wry { let window_id_map = self.context.window_id_map.clone(); let web_context = &self.context.main_thread.web_context; let plugins = self.context.plugins.clone(); + let device_event_callback = self.context.device_event_callback.clone(); #[cfg(feature = "tracing")] let active_tracing_spans = self.context.main_thread.active_tracing_spans.clone(); @@ -2998,6 +3033,7 @@ impl Runtime for Wry { event, event_loop, control_flow, + device_event_callback.clone(), EventLoopIterationContext { callback: &mut callback, windows: windows.clone(), @@ -3043,35 +3079,41 @@ where let window_id_map = runtime.context.window_id_map.clone(); let web_context = runtime.context.main_thread.web_context.clone(); let plugins = runtime.context.plugins.clone(); + let device_event_callback = runtime.context.device_event_callback.clone(); #[cfg(feature = "tracing")] let active_tracing_spans = runtime.context.main_thread.active_tracing_spans.clone(); let proxy = runtime.event_loop.create_proxy(); move |event, event_loop, control_flow| { - for p in plugins.lock().unwrap().iter_mut() { - let prevent_default = p.on_event( - &event, - event_loop, - &proxy, - control_flow, - EventLoopIterationContext { - callback: &mut callback, - window_id_map: window_id_map.clone(), - windows: windows.clone(), - #[cfg(feature = "tracing")] - active_tracing_spans: active_tracing_spans.clone(), - }, - &web_context, - ); - if prevent_default { - return; + // if device event skip the plugins to optimize performance + // if device filter is set to always and this is not here there will be a performance hit + if !matches!(event, Event::DeviceEvent { .. }) { + for p in plugins.lock().unwrap().iter_mut() { + let prevent_default = p.on_event( + &event, + event_loop, + &proxy, + control_flow, + EventLoopIterationContext { + callback: &mut callback, + window_id_map: window_id_map.clone(), + windows: windows.clone(), + #[cfg(feature = "tracing")] + active_tracing_spans: active_tracing_spans.clone(), + }, + &web_context, + ); + if prevent_default { + return; + } } } handle_event_loop( event, event_loop, control_flow, + device_event_callback.clone(), EventLoopIterationContext { callback: &mut callback, window_id_map: window_id_map.clone(), @@ -3879,6 +3921,7 @@ fn handle_event_loop( event: Event<'_, Message>, event_loop: &EventLoopWindowTarget>, control_flow: &mut ControlFlow, + device_event_callback: DeviceEventCallback, context: EventLoopIterationContext<'_, T>, ) { let EventLoopIterationContext { @@ -3893,6 +3936,15 @@ fn handle_event_loop( } match event { + Event::DeviceEvent { + device_id, event, .. + } => { + if let Some(device_event_function) = device_event_callback.lock().unwrap().as_mut() { + if let Some(mapped_event) = map_device_event::map_device_event(event) { + device_event_function(DeviceId(device_id), mapped_event); + } + } + } Event::NewEvents(StartCause::Init) => { callback(RunEvent::Ready); } diff --git a/crates/tauri-runtime-wry/src/map_device_event.rs b/crates/tauri-runtime-wry/src/map_device_event.rs new file mode 100644 index 000000000000..6a5c21fd76ab --- /dev/null +++ b/crates/tauri-runtime-wry/src/map_device_event.rs @@ -0,0 +1,253 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use tauri_runtime::device_events::{Code, DeviceEvent, MouseScrollDelta}; + +pub fn map_device_event(event: tao::event::DeviceEvent) -> Option { + match event { + tao::event::DeviceEvent::Added => Some(DeviceEvent::Added), + tao::event::DeviceEvent::Removed => Some(DeviceEvent::Removed), + tao::event::DeviceEvent::MouseMotion { delta, .. } => Some(DeviceEvent::MouseMotion { delta }), + tao::event::DeviceEvent::MouseWheel { delta, .. } => match delta { + tao::event::MouseScrollDelta::LineDelta(x, y) => Some(DeviceEvent::MouseWheel { + delta: MouseScrollDelta::LineDelta(x, y), + }), + tao::event::MouseScrollDelta::PixelDelta(pos) => Some(DeviceEvent::MouseWheel { + delta: MouseScrollDelta::PixelDelta(pos), + }), + _ => None, + }, + tao::event::DeviceEvent::Motion { axis, value, .. } => { + Some(DeviceEvent::Motion { axis, value }) + } + tao::event::DeviceEvent::Button { button, state, .. } => Some(DeviceEvent::Button { + button, + state: map_element_state(state)?, + }), + tao::event::DeviceEvent::Key(raw_key_event) => { + let physical_key = raw_key_event.physical_key; + let state = map_element_state(raw_key_event.state)?; + Some(DeviceEvent::Key { + pysical_key: map_physical_key(physical_key)?, + state, + }) + } + tao::event::DeviceEvent::Text { codepoint, .. } => Some(DeviceEvent::Text { codepoint }), + _ => None, + } +} + +fn map_element_state( + state: tao::event::ElementState, +) -> Option { + match state { + tao::event::ElementState::Pressed => Some(tauri_runtime::device_events::KeyState::Down), + tao::event::ElementState::Released => Some(tauri_runtime::device_events::KeyState::Up), + _ => None, + } +} + +fn map_physical_key(event: tao::keyboard::KeyCode) -> Option { + match event { + tao::keyboard::KeyCode::Unidentified(_) => None, + tao::keyboard::KeyCode::Backquote => Some(Code::Backquote), + tao::keyboard::KeyCode::Backslash => Some(Code::Backslash), + tao::keyboard::KeyCode::BracketLeft => Some(Code::BracketLeft), + tao::keyboard::KeyCode::BracketRight => Some(Code::BracketRight), + tao::keyboard::KeyCode::Comma => Some(Code::Comma), + tao::keyboard::KeyCode::Digit0 => Some(Code::Digit0), + tao::keyboard::KeyCode::Digit1 => Some(Code::Digit1), + tao::keyboard::KeyCode::Digit2 => Some(Code::Digit2), + tao::keyboard::KeyCode::Digit3 => Some(Code::Digit3), + tao::keyboard::KeyCode::Digit4 => Some(Code::Digit4), + tao::keyboard::KeyCode::Digit5 => Some(Code::Digit5), + tao::keyboard::KeyCode::Digit6 => Some(Code::Digit6), + tao::keyboard::KeyCode::Digit7 => Some(Code::Digit7), + tao::keyboard::KeyCode::Digit8 => Some(Code::Digit8), + tao::keyboard::KeyCode::Digit9 => Some(Code::Digit9), + tao::keyboard::KeyCode::Equal => Some(Code::Equal), + tao::keyboard::KeyCode::IntlBackslash => Some(Code::IntlBackslash), + tao::keyboard::KeyCode::IntlRo => Some(Code::IntlRo), + tao::keyboard::KeyCode::IntlYen => Some(Code::IntlYen), + tao::keyboard::KeyCode::KeyA => Some(Code::KeyA), + tao::keyboard::KeyCode::KeyB => Some(Code::KeyB), + tao::keyboard::KeyCode::KeyC => Some(Code::KeyC), + tao::keyboard::KeyCode::KeyD => Some(Code::KeyD), + tao::keyboard::KeyCode::KeyE => Some(Code::KeyE), + tao::keyboard::KeyCode::KeyF => Some(Code::KeyF), + tao::keyboard::KeyCode::KeyG => Some(Code::KeyG), + tao::keyboard::KeyCode::KeyH => Some(Code::KeyH), + tao::keyboard::KeyCode::KeyI => Some(Code::KeyI), + tao::keyboard::KeyCode::KeyJ => Some(Code::KeyJ), + tao::keyboard::KeyCode::KeyK => Some(Code::KeyK), + tao::keyboard::KeyCode::KeyL => Some(Code::KeyL), + tao::keyboard::KeyCode::KeyM => Some(Code::KeyM), + tao::keyboard::KeyCode::KeyN => Some(Code::KeyN), + tao::keyboard::KeyCode::KeyO => Some(Code::KeyO), + tao::keyboard::KeyCode::KeyP => Some(Code::KeyP), + tao::keyboard::KeyCode::KeyQ => Some(Code::KeyQ), + tao::keyboard::KeyCode::KeyR => Some(Code::KeyR), + tao::keyboard::KeyCode::KeyS => Some(Code::KeyS), + tao::keyboard::KeyCode::KeyT => Some(Code::KeyT), + tao::keyboard::KeyCode::KeyU => Some(Code::KeyU), + tao::keyboard::KeyCode::KeyV => Some(Code::KeyV), + tao::keyboard::KeyCode::KeyW => Some(Code::KeyW), + tao::keyboard::KeyCode::KeyX => Some(Code::KeyX), + tao::keyboard::KeyCode::KeyY => Some(Code::KeyY), + tao::keyboard::KeyCode::KeyZ => Some(Code::KeyZ), + tao::keyboard::KeyCode::Minus => Some(Code::Minus), + // could be wrong convertion?? + tao::keyboard::KeyCode::Plus => Some(Code::Equal), + tao::keyboard::KeyCode::Period => Some(Code::Period), + tao::keyboard::KeyCode::Quote => Some(Code::Quote), + tao::keyboard::KeyCode::Semicolon => Some(Code::Semicolon), + tao::keyboard::KeyCode::Slash => Some(Code::Slash), + tao::keyboard::KeyCode::AltLeft => Some(Code::AltLeft), + tao::keyboard::KeyCode::AltRight => Some(Code::AltRight), + tao::keyboard::KeyCode::Backspace => Some(Code::Backspace), + tao::keyboard::KeyCode::CapsLock => Some(Code::CapsLock), + tao::keyboard::KeyCode::ContextMenu => Some(Code::ContextMenu), + tao::keyboard::KeyCode::ControlLeft => Some(Code::ControlLeft), + tao::keyboard::KeyCode::ControlRight => Some(Code::ControlRight), + tao::keyboard::KeyCode::Enter => Some(Code::Enter), + tao::keyboard::KeyCode::SuperLeft => Some(Code::MetaLeft), + tao::keyboard::KeyCode::SuperRight => Some(Code::MetaRight), + tao::keyboard::KeyCode::ShiftLeft => Some(Code::ShiftLeft), + tao::keyboard::KeyCode::ShiftRight => Some(Code::ShiftRight), + tao::keyboard::KeyCode::Space => Some(Code::Space), + tao::keyboard::KeyCode::Tab => Some(Code::Tab), + tao::keyboard::KeyCode::Convert => Some(Code::Convert), + tao::keyboard::KeyCode::KanaMode => Some(Code::KanaMode), + tao::keyboard::KeyCode::Lang1 => Some(Code::Lang1), + tao::keyboard::KeyCode::Lang2 => Some(Code::Lang2), + tao::keyboard::KeyCode::Lang3 => Some(Code::Lang3), + tao::keyboard::KeyCode::Lang4 => Some(Code::Lang4), + tao::keyboard::KeyCode::Lang5 => Some(Code::Lang5), + tao::keyboard::KeyCode::NonConvert => Some(Code::NonConvert), + tao::keyboard::KeyCode::Delete => Some(Code::Delete), + tao::keyboard::KeyCode::End => Some(Code::End), + tao::keyboard::KeyCode::Help => Some(Code::Help), + tao::keyboard::KeyCode::Home => Some(Code::Home), + tao::keyboard::KeyCode::Insert => Some(Code::Insert), + tao::keyboard::KeyCode::PageDown => Some(Code::PageDown), + tao::keyboard::KeyCode::PageUp => Some(Code::PageUp), + tao::keyboard::KeyCode::ArrowDown => Some(Code::ArrowDown), + tao::keyboard::KeyCode::ArrowLeft => Some(Code::ArrowLeft), + tao::keyboard::KeyCode::ArrowRight => Some(Code::ArrowRight), + tao::keyboard::KeyCode::ArrowUp => Some(Code::ArrowUp), + tao::keyboard::KeyCode::NumLock => Some(Code::NumLock), + tao::keyboard::KeyCode::Numpad0 => Some(Code::Numpad0), + tao::keyboard::KeyCode::Numpad1 => Some(Code::Numpad1), + tao::keyboard::KeyCode::Numpad2 => Some(Code::Numpad2), + tao::keyboard::KeyCode::Numpad3 => Some(Code::Numpad3), + tao::keyboard::KeyCode::Numpad4 => Some(Code::Numpad4), + tao::keyboard::KeyCode::Numpad5 => Some(Code::Numpad5), + tao::keyboard::KeyCode::Numpad6 => Some(Code::Numpad6), + tao::keyboard::KeyCode::Numpad7 => Some(Code::Numpad7), + tao::keyboard::KeyCode::Numpad8 => Some(Code::Numpad8), + tao::keyboard::KeyCode::Numpad9 => Some(Code::Numpad9), + tao::keyboard::KeyCode::NumpadAdd => Some(Code::NumpadAdd), + tao::keyboard::KeyCode::NumpadBackspace => Some(Code::NumpadBackspace), + tao::keyboard::KeyCode::NumpadClear => Some(Code::NumpadClear), + tao::keyboard::KeyCode::NumpadClearEntry => Some(Code::NumpadClearEntry), + tao::keyboard::KeyCode::NumpadComma => Some(Code::NumpadComma), + tao::keyboard::KeyCode::NumpadDecimal => Some(Code::NumpadDecimal), + tao::keyboard::KeyCode::NumpadDivide => Some(Code::NumpadDivide), + tao::keyboard::KeyCode::NumpadEnter => Some(Code::NumpadEnter), + tao::keyboard::KeyCode::NumpadEqual => Some(Code::NumpadEqual), + tao::keyboard::KeyCode::NumpadHash => Some(Code::NumpadHash), + tao::keyboard::KeyCode::NumpadMemoryAdd => Some(Code::NumpadMemoryAdd), + tao::keyboard::KeyCode::NumpadMemoryClear => Some(Code::NumpadMemoryClear), + tao::keyboard::KeyCode::NumpadMemoryRecall => Some(Code::NumpadMemoryRecall), + tao::keyboard::KeyCode::NumpadMemoryStore => Some(Code::NumpadMemoryStore), + tao::keyboard::KeyCode::NumpadMemorySubtract => Some(Code::NumpadMemorySubtract), + tao::keyboard::KeyCode::NumpadMultiply => Some(Code::NumpadMultiply), + tao::keyboard::KeyCode::NumpadParenLeft => Some(Code::NumpadParenLeft), + tao::keyboard::KeyCode::NumpadParenRight => Some(Code::NumpadParenRight), + tao::keyboard::KeyCode::NumpadStar => Some(Code::NumpadStar), + tao::keyboard::KeyCode::NumpadSubtract => Some(Code::NumpadSubtract), + tao::keyboard::KeyCode::Escape => Some(Code::Escape), + tao::keyboard::KeyCode::Fn => Some(Code::Fn), + tao::keyboard::KeyCode::FnLock => Some(Code::FnLock), + tao::keyboard::KeyCode::PrintScreen => Some(Code::PrintScreen), + tao::keyboard::KeyCode::ScrollLock => Some(Code::ScrollLock), + tao::keyboard::KeyCode::Pause => Some(Code::Pause), + tao::keyboard::KeyCode::BrowserBack => Some(Code::BrowserBack), + tao::keyboard::KeyCode::BrowserFavorites => Some(Code::BrowserFavorites), + tao::keyboard::KeyCode::BrowserForward => Some(Code::BrowserForward), + tao::keyboard::KeyCode::BrowserHome => Some(Code::BrowserHome), + tao::keyboard::KeyCode::BrowserRefresh => Some(Code::BrowserRefresh), + tao::keyboard::KeyCode::BrowserSearch => Some(Code::BrowserSearch), + tao::keyboard::KeyCode::BrowserStop => Some(Code::BrowserStop), + tao::keyboard::KeyCode::Eject => Some(Code::Eject), + tao::keyboard::KeyCode::LaunchApp1 => Some(Code::LaunchApp1), + tao::keyboard::KeyCode::LaunchApp2 => Some(Code::LaunchApp2), + tao::keyboard::KeyCode::LaunchMail => Some(Code::LaunchMail), + tao::keyboard::KeyCode::MediaPlayPause => Some(Code::MediaPlayPause), + tao::keyboard::KeyCode::MediaSelect => Some(Code::MediaSelect), + tao::keyboard::KeyCode::MediaStop => Some(Code::MediaStop), + tao::keyboard::KeyCode::MediaTrackNext => Some(Code::MediaTrackNext), + tao::keyboard::KeyCode::MediaTrackPrevious => Some(Code::MediaTrackPrevious), + tao::keyboard::KeyCode::Power => Some(Code::Power), + tao::keyboard::KeyCode::Sleep => Some(Code::Sleep), + tao::keyboard::KeyCode::AudioVolumeDown => Some(Code::AudioVolumeDown), + tao::keyboard::KeyCode::AudioVolumeMute => Some(Code::AudioVolumeMute), + tao::keyboard::KeyCode::AudioVolumeUp => Some(Code::AudioVolumeUp), + tao::keyboard::KeyCode::WakeUp => Some(Code::WakeUp), + // maybe needs to be changed but [`Code::Hyper`] is deprecated + tao::keyboard::KeyCode::Hyper => Some(Code::MetaLeft), + // maybe needs to be changed but [`Code::Super`] is deprecated + tao::keyboard::KeyCode::Turbo => Some(Code::MetaLeft), + tao::keyboard::KeyCode::Abort => Some(Code::Abort), + tao::keyboard::KeyCode::Resume => Some(Code::Resume), + tao::keyboard::KeyCode::Suspend => Some(Code::Suspend), + tao::keyboard::KeyCode::Again => Some(Code::Again), + tao::keyboard::KeyCode::Copy => Some(Code::Copy), + tao::keyboard::KeyCode::Cut => Some(Code::Cut), + tao::keyboard::KeyCode::Find => Some(Code::Find), + tao::keyboard::KeyCode::Open => Some(Code::Open), + tao::keyboard::KeyCode::Paste => Some(Code::Paste), + tao::keyboard::KeyCode::Props => Some(Code::Props), + tao::keyboard::KeyCode::Select => Some(Code::Select), + tao::keyboard::KeyCode::Undo => Some(Code::Undo), + tao::keyboard::KeyCode::Hiragana => Some(Code::Hiragana), + tao::keyboard::KeyCode::Katakana => Some(Code::Katakana), + tao::keyboard::KeyCode::F1 => Some(Code::F1), + tao::keyboard::KeyCode::F2 => Some(Code::F2), + tao::keyboard::KeyCode::F3 => Some(Code::F3), + tao::keyboard::KeyCode::F4 => Some(Code::F4), + tao::keyboard::KeyCode::F5 => Some(Code::F5), + tao::keyboard::KeyCode::F6 => Some(Code::F6), + tao::keyboard::KeyCode::F7 => Some(Code::F7), + tao::keyboard::KeyCode::F8 => Some(Code::F8), + tao::keyboard::KeyCode::F9 => Some(Code::F9), + tao::keyboard::KeyCode::F10 => Some(Code::F10), + tao::keyboard::KeyCode::F11 => Some(Code::F11), + tao::keyboard::KeyCode::F12 => Some(Code::F12), + tao::keyboard::KeyCode::F13 => Some(Code::F13), + tao::keyboard::KeyCode::F14 => Some(Code::F14), + tao::keyboard::KeyCode::F15 => Some(Code::F15), + tao::keyboard::KeyCode::F16 => Some(Code::F16), + tao::keyboard::KeyCode::F17 => Some(Code::F17), + tao::keyboard::KeyCode::F18 => Some(Code::F18), + tao::keyboard::KeyCode::F19 => Some(Code::F19), + tao::keyboard::KeyCode::F20 => Some(Code::F20), + tao::keyboard::KeyCode::F21 => Some(Code::F21), + tao::keyboard::KeyCode::F22 => Some(Code::F22), + tao::keyboard::KeyCode::F23 => Some(Code::F23), + tao::keyboard::KeyCode::F24 => Some(Code::F24), + tao::keyboard::KeyCode::F25 => Some(Code::F25), + tao::keyboard::KeyCode::F26 => Some(Code::F26), + tao::keyboard::KeyCode::F27 => Some(Code::F27), + tao::keyboard::KeyCode::F28 => Some(Code::F28), + tao::keyboard::KeyCode::F29 => Some(Code::F29), + tao::keyboard::KeyCode::F30 => Some(Code::F30), + tao::keyboard::KeyCode::F31 => Some(Code::F31), + tao::keyboard::KeyCode::F32 => Some(Code::F32), + tao::keyboard::KeyCode::F33 => Some(Code::F33), + tao::keyboard::KeyCode::F34 => Some(Code::F34), + tao::keyboard::KeyCode::F35 => Some(Code::F35), + _ => None, + } +} diff --git a/crates/tauri-runtime/Cargo.toml b/crates/tauri-runtime/Cargo.toml index 721af20e5b55..5ecce1e242fe 100644 --- a/crates/tauri-runtime/Cargo.toml +++ b/crates/tauri-runtime/Cargo.toml @@ -34,6 +34,7 @@ http = "1" raw-window-handle = "0.6" url = { version = "2" } dpi = { version = "0.1", features = ["serde"] } +keyboard-types = { version = "0.8.0", features = ["serde"] } # WARNING: cookie::Cookie is re-exported so bumping this is a breaking change, documented to be done as a minor bump cookie = "0.18" diff --git a/crates/tauri-runtime/src/device_events.rs b/crates/tauri-runtime/src/device_events.rs new file mode 100644 index 000000000000..38c02b68f2c6 --- /dev/null +++ b/crates/tauri-runtime/src/device_events.rs @@ -0,0 +1,104 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use dpi::PhysicalPosition; +pub use keyboard_types::{Code, KeyState}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(tag = "type")] +pub enum DeviceEventFilter { + /// Always filter out device events. + Always, + /// Filter out device events while the window is not focused. + Unfocused, + /// Report all device events regardless of window focus. + Never, +} + +impl Default for DeviceEventFilter { + fn default() -> Self { + Self::Unfocused + } +} + +pub trait DeviceId: Copy + Clone + PartialEq + Eq + PartialOrd + Ord + std::hash::Hash { + /// # Safety + /// Returns a dummy `DeviceId`, useful for unit testing. The only guarantee made about the return + /// value of this function is that it will always be equal to itself and to future values returned + /// by this function. No other guarantees are made. This may be equal to a real `DeviceId`. + /// + /// **Passing this id to any real device will result in undefined behavior.** + unsafe fn dummy() -> Self; +} + +#[derive(Clone, Debug, PartialEq)] +pub enum DeviceEvent { + Added, + Removed, + + /// Change in physical position of a pointing device. + /// + /// This represents raw, unfiltered physical motion. Not to be confused with `WindowEvent::CursorMoved`. + MouseMotion { + /// (x, y) change in position in unspecified units. + /// + /// Different devices may use different units. + delta: (f64, f64), + }, + + /// Physical scroll event + MouseWheel { + delta: MouseScrollDelta, + }, + + /// Motion on some analog axis. This event will be reported for all arbitrary input devices + /// that tao supports on this platform, including mouse devices. If the device is a mouse + /// device then this will be reported alongside the MouseMotion event. + Motion { + axis: AxisId, + value: f64, + }, + + Button { + button: ButtonId, + state: KeyState, + }, + + Key { + pysical_key: Code, + state: KeyState, + }, + + Text { + codepoint: char, + }, +} + +/// Identifier for a specific analog axis on some device. +pub type AxisId = u32; + +/// Identifier for a specific button on some device. +/// +/// For a mouse, this is the button number (0 for left, 1 for right, etc.). +pub type ButtonId = u32; + +/// Describes a difference in the mouse scroll wheel state. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum MouseScrollDelta { + /// Amount in lines or rows to scroll in the horizontal + /// and vertical directions. + /// + /// Positive values indicate movement forward + /// (away from the user) or rightwards. + LineDelta(f32, f32), + /// Amount in pixels to scroll in the horizontal and + /// vertical direction. + /// + /// Scroll events are expressed as a PixelDelta if + /// supported by the device (eg. a touchpad) and + /// platform. + PixelDelta(PhysicalPosition), +} diff --git a/crates/tauri-runtime/src/lib.rs b/crates/tauri-runtime/src/lib.rs index 201ec65b077d..0baaee4a2f8f 100644 --- a/crates/tauri-runtime/src/lib.rs +++ b/crates/tauri-runtime/src/lib.rs @@ -13,6 +13,7 @@ )] #![cfg_attr(docsrs, feature(doc_cfg))] +use device_events::DeviceEventFilter; use raw_window_handle::DisplayHandle; use serde::Deserialize; use std::{borrow::Cow, fmt::Debug, sync::mpsc::Sender}; @@ -21,6 +22,7 @@ use tauri_utils::Theme; use url::Url; use webview::{DetachedWebview, PendingWebview}; +pub mod device_events; /// UI scaling utilities. pub mod dpi; /// Types useful for interacting with a user's monitors. @@ -90,23 +92,6 @@ pub enum UserAttentionType { Informational, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] -#[serde(tag = "type")] -pub enum DeviceEventFilter { - /// Always filter out device events. - Always, - /// Filter out device events while the window is not focused. - Unfocused, - /// Report all device events regardless of window focus. - Never, -} - -impl Default for DeviceEventFilter { - fn default() -> Self { - Self::Unfocused - } -} - /// Defines the orientation that a window resize will be performed. #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] pub enum ResizeDirection { @@ -434,6 +419,8 @@ pub trait Runtime: Debug + Sized + 'static { #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] fn hide(&self); + type DeviceId: device_events::DeviceId; + /// Change the device event filter mode. /// /// Since the DeviceEvent capture can lead to high CPU usage for unfocused windows, [`tao`] @@ -442,10 +429,24 @@ pub trait Runtime: Debug + Sized + 'static { /// /// ## Platform-specific /// - /// - ** Linux / macOS / iOS / Android**: Unsupported. + /// - ** Wayland / macOS / iOS / Android**: Unsupported. /// /// [`tao`]: https://crates.io/crates/tao - fn set_device_event_filter(&mut self, filter: DeviceEventFilter); + fn set_device_event_filter(&self, filter: DeviceEventFilter); + + /// Set a callback to be called when a device event is received. + /// + /// By default, the device events are filtered out when the window is not focused. + /// To change this behavior, use [`set_device_event_filter`](Runtime::set_device_event_filter). + /// + /// ## Platform-specific + /// + /// - ** Wayland / macOS / iOS / Android**: Unsupported. + /// + /// [`tao`]: https://crates.io/crates/tao + fn set_device_event_callback(&self, callback: F) + where + F: FnMut(Self::DeviceId, device_events::DeviceEvent) + Send + 'static; /// Runs an iteration of the runtime event loop and returns control flow to the caller. #[cfg(desktop)] diff --git a/crates/tauri/src/app.rs b/crates/tauri/src/app.rs index 9f3e97ab61e6..ebb28a7ff57c 100644 --- a/crates/tauri/src/app.rs +++ b/crates/tauri/src/app.rs @@ -32,6 +32,7 @@ use tauri_macros::default_runtime; #[cfg(desktop)] use tauri_runtime::EventLoopProxy; use tauri_runtime::{ + device_events::DeviceEvent, dpi::{PhysicalPosition, PhysicalSize}, window::DragDropEvent, RuntimeInitArgs, @@ -1405,6 +1406,10 @@ pub struct Builder { /// The device event filter. device_event_filter: DeviceEventFilter, + /// device event callback + device_event_callback: + Option, R::DeviceId, DeviceEvent) + Send + 'static>>, + pub(crate) invoke_key: String, } @@ -1470,6 +1475,7 @@ impl Builder { window_event_listeners: Vec::new(), webview_event_listeners: Vec::new(), device_event_filter: Default::default(), + device_event_callback: None, invoke_key, } } @@ -2043,9 +2049,9 @@ tauri::Builder::default() /// /// ## Platform-specific /// - /// - ** Linux / macOS / iOS / Android**: Unsupported. + /// - ** Wayland / macOS / iOS / Android**: Unsupported. /// - /// # Examples + /// # Example /// ```,no_run /// tauri::Builder::default() /// .device_event_filter(tauri::DeviceEventFilter::Always); @@ -2057,6 +2063,35 @@ tauri::Builder::default() self } + /// Registers a device event callback. + /// + /// if [`DeviceEventFilter`] is set to [`DeviceEventFilter::Always`], this callback will be called for all device events. + /// This leads to high CPU usage for unfocused windows, so it is recommended to use [`DeviceEventFilter::FocusedOnly`] unless nessary and keep processing to a minimum. + /// + /// ## Platform-specific + /// + /// - ** Wayland / macOS / iOS / Android**: Unsupported. + /// + /// # Example + /// ``` + /// tauri::Builder::default().on_device_event(|_app, _device_id, event| match event { + /// tauri::DeviceEvent::MouseMotion { delta } => { + /// println!("Mouse moved: {:?}", delta); + /// } + /// tauri::DeviceEvent::MouseWheel { delta } => { + /// println!("Mouse wheel: {:?}", delta); + /// } + /// _ => {} + /// }); + /// ``` + pub fn on_device_event(mut self, callback: F) -> Self + where + F: FnMut(&AppHandle, R::DeviceId, DeviceEvent) + Send + 'static, + { + self.device_event_callback = Some(Box::new(callback)); + self + } + /// Builds the application. #[allow(clippy::type_complexity, unused_mut)] #[cfg_attr( @@ -2166,18 +2201,27 @@ tauri::Builder::default() let runtime_handle = runtime.handle(); + let app_handle = AppHandle { + runtime_handle, + manager: manager.clone(), + event_loop: Arc::new(Mutex::new(EventLoop { + main_thread_id: std::thread::current().id(), + })), + }; + + if let Some(mut device_event_callback) = self.device_event_callback { + let app_handle_clone = app_handle.clone(); + runtime.set_device_event_callback(move |id, event| { + device_event_callback(&app_handle_clone, id, event); + }); + } + #[allow(unused_mut)] let mut app = App { runtime: Some(runtime), setup: Some(self.setup), - manager: manager.clone(), - handle: AppHandle { - runtime_handle, - manager, - event_loop: Arc::new(Mutex::new(EventLoop { - main_thread_id: std::thread::current().id(), - })), - }, + manager, + handle: app_handle, ran_setup: false, }; diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs index 7809dfbfcd0e..9c430e75fb91 100644 --- a/crates/tauri/src/lib.rs +++ b/crates/tauri/src/lib.rs @@ -218,12 +218,15 @@ pub use { }, self::manager::Asset, self::runtime::{ + device_events::{ + AxisId, ButtonId, Code, DeviceEvent, DeviceEventFilter, DeviceId, KeyState, MouseScrollDelta, + }, dpi::{ LogicalPosition, LogicalRect, LogicalSize, PhysicalPosition, PhysicalRect, PhysicalSize, Pixel, Position, Rect, Size, }, window::{CursorIcon, DragDropEvent, WindowSizeConstraints}, - DeviceEventFilter, UserAttentionType, + UserAttentionType, }, self::state::{State, StateManager}, self::utils::{ diff --git a/crates/tauri/src/test/mock_runtime.rs b/crates/tauri/src/test/mock_runtime.rs index 1c1b9ca43767..01918de4cdfa 100644 --- a/crates/tauri/src/test/mock_runtime.rs +++ b/crates/tauri/src/test/mock_runtime.rs @@ -6,6 +6,7 @@ #![allow(missing_docs)] use tauri_runtime::{ + device_events::DeviceEventFilter, dpi::{PhysicalPosition, PhysicalSize, Position, Size}, monitor::Monitor, webview::{DetachedWebview, PendingWebview}, @@ -13,9 +14,9 @@ use tauri_runtime::{ CursorIcon, DetachedWindow, DetachedWindowWebview, PendingWindow, RawWindow, WindowBuilder, WindowBuilderBase, WindowEvent, WindowId, }, - DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Icon, ProgressBarState, - Result, RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, UserEvent, - WebviewDispatch, WindowDispatch, WindowEventId, + Error, EventLoopProxy, ExitRequestedEventAction, Icon, ProgressBarState, Result, RunEvent, + Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, UserEvent, WebviewDispatch, + WindowDispatch, WindowEventId, }; #[cfg(target_os = "macos")] @@ -1111,6 +1112,14 @@ impl MockRuntime { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(u32); +impl tauri_runtime::device_events::DeviceId for DeviceId { + unsafe fn dummy() -> Self { + Self(0) + } +} + impl Runtime for MockRuntime { type WindowDispatcher = MockWindowDispatcher; type WebviewDispatcher = MockWebviewDispatcher; @@ -1235,7 +1244,15 @@ impl Runtime for MockRuntime { #[cfg_attr(docsrs, doc(cfg(target_os = "macos")))] fn hide(&self) {} - fn set_device_event_filter(&mut self, filter: DeviceEventFilter) {} + fn set_device_event_filter(&self, filter: DeviceEventFilter) {} + + type DeviceId = DeviceId; + + fn set_device_event_callback(&self, callback: F) + where + F: FnMut(Self::DeviceId, tauri_runtime::device_events::DeviceEvent) + Send + 'static, + { + } #[cfg(any( target_os = "macos",