diff --git a/docs/guide/src/en/getting_started/desktop.md b/docs/guide/src/en/getting_started/desktop.md index c86d7760e..311ad2c9f 100644 --- a/docs/guide/src/en/getting_started/desktop.md +++ b/docs/guide/src/en/getting_started/desktop.md @@ -14,7 +14,7 @@ Examples: The desktop is a powerful target for Dioxus but is currently limited in capability when compared to the Web platform. Currently, desktop apps are rendered with the platform's WebView library, but your Rust code is running natively on a native thread. This means that browser APIs are *not* available, so rendering WebGL, Canvas, etc is not as easy as the Web. However, native system APIs *are* accessible, so streaming, WebSockets, filesystem, etc are all viable APIs. In the future, we plan to move to a custom web renderer-based DOM renderer with WGPU integrations. -Dioxus Desktop is built off [Tauri](https://tauri.app/). Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri – mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. +Dioxus Desktop is built off [Tauri](https://tauri.app/). Right now there aren't any Dioxus abstractions over the menubar, handling, etc, so you'll want to leverage Tauri – mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. # Getting started diff --git a/docs/reference/src/platforms/desktop.md b/docs/reference/src/platforms/desktop.md index a6c85c21e..dcdcca778 100644 --- a/docs/reference/src/platforms/desktop.md +++ b/docs/reference/src/platforms/desktop.md @@ -2,7 +2,7 @@ One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. -Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. +Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over the menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. ## Getting Set up diff --git a/examples/shortcut.rs b/examples/shortcut.rs new file mode 100644 index 000000000..d98a69ad6 --- /dev/null +++ b/examples/shortcut.rs @@ -0,0 +1,17 @@ +use dioxus::prelude::*; +use dioxus_desktop::tao::keyboard::ModifiersState; +use dioxus_desktop::use_global_shortcut; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let toggled = use_state(cx, || false); + use_global_shortcut(cx, KeyCode::S, ModifiersState::CONTROL, { + to_owned![toggled]; + move || toggled.modify(|t| !*t) + }); + + cx.render(rsx!("toggle: {toggled.get()}")) +} diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index edddd7887..7db554517 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -5,6 +5,11 @@ use std::rc::Weak; use crate::create_new_window; use crate::eval::EvalResult; use crate::events::IpcMessage; +use crate::shortcut::IntoKeyCode; +use crate::shortcut::IntoModifersState; +use crate::shortcut::ShortcutId; +use crate::shortcut::ShortcutRegistry; +use crate::shortcut::ShortcutRegistryError; use crate::Config; use crate::WebviewHandler; use dioxus_core::ScopeState; @@ -63,6 +68,8 @@ pub struct DesktopContext { pub(crate) event_handlers: WindowEventHandlers, + pub(crate) shortcut_manager: ShortcutRegistry, + #[cfg(target_os = "ios")] pub(crate) views: Rc>>, } @@ -83,6 +90,7 @@ impl DesktopContext { event_loop: EventLoopWindowTarget, webviews: WebviewQueue, event_handlers: WindowEventHandlers, + shortcut_manager: ShortcutRegistry, ) -> Self { Self { webview, @@ -91,6 +99,7 @@ impl DesktopContext { eval: tokio::sync::broadcast::channel(8).0, pending_windows: webviews, event_handlers, + shortcut_manager, #[cfg(target_os = "ios")] views: Default::default(), } @@ -111,6 +120,7 @@ impl DesktopContext { dom, &self.pending_windows, &self.event_handlers, + self.shortcut_manager.clone(), ); let id = window.webview.window().id(); @@ -240,6 +250,27 @@ impl DesktopContext { self.event_handlers.remove(id) } + /// Create a global shortcut + /// + /// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information. + pub fn create_shortcut( + &self, + key: impl IntoKeyCode, + modifiers: impl IntoModifersState, + callback: impl FnMut() + 'static, + ) -> Result { + self.shortcut_manager.add_shortcut( + modifiers.into_modifiers_state(), + key.into_key_code(), + Box::new(callback), + ) + } + + /// Remove a global shortcut + pub fn remove_shortcut(&self, id: ShortcutId) { + self.shortcut_manager.remove_shortcut(id) + } + /// Push an objc view to the window #[cfg(target_os = "ios")] pub fn push_view(&self, view: objc_id::ShareId) { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 16f562464..75c67dd22 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -9,6 +9,7 @@ mod escape; mod eval; mod events; mod protocol; +mod shortcut; mod waker; mod webview; @@ -21,6 +22,8 @@ use dioxus_core::*; use dioxus_html::HtmlEvent; pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; +use shortcut::ShortcutRegistry; +pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError}; use std::collections::HashMap; use std::rc::Rc; use std::task::Waker; @@ -139,6 +142,8 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) let queue = WebviewQueue::default(); + let shortcut_manager = ShortcutRegistry::new(&event_loop); + // By default, we'll create a new window when the app starts queue.borrow_mut().push(create_new_window( cfg, @@ -147,6 +152,7 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) VirtualDom::new_with_props(root, props), &queue, &event_handlers, + shortcut_manager.clone(), )); event_loop.run(move |window_event, event_loop, control_flow| { @@ -260,6 +266,7 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) _ => {} }, + Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id), _ => {} } }) @@ -272,6 +279,7 @@ fn create_new_window( dom: VirtualDom, queue: &WebviewQueue, event_handlers: &WindowEventHandlers, + shortcut_manager: ShortcutRegistry, ) -> WebviewHandler { let webview = webview::build(&mut cfg, event_loop, proxy.clone()); @@ -281,6 +289,7 @@ fn create_new_window( event_loop.clone(), queue.clone(), event_handlers.clone(), + shortcut_manager, )); let id = webview.window().id(); diff --git a/packages/desktop/src/readme.md b/packages/desktop/src/readme.md index 5c22f015b..cd4dbe41a 100644 --- a/packages/desktop/src/readme.md +++ b/packages/desktop/src/readme.md @@ -6,7 +6,7 @@ Render the Dioxus VirtualDom using the platform's native WebView implementation. One of Dioxus' flagship features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. -Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao) directly. An upcoming release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. +Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over the menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao) directly. An upcoming release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. ## Getting Set up diff --git a/packages/desktop/src/shortcut.rs b/packages/desktop/src/shortcut.rs new file mode 100644 index 000000000..cf70d307a --- /dev/null +++ b/packages/desktop/src/shortcut.rs @@ -0,0 +1,320 @@ +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +use dioxus_core::ScopeState; +use dioxus_html::input_data::keyboard_types::Modifiers; +use slab::Slab; +use wry::application::{ + accelerator::{Accelerator, AcceleratorId}, + event_loop::EventLoopWindowTarget, + global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError}, + keyboard::{KeyCode, ModifiersState}, +}; + +use crate::{use_window, DesktopContext}; + +#[derive(Clone)] +pub(crate) struct ShortcutRegistry { + manager: Rc>, + shortcuts: ShortcutMap, +} + +type ShortcutMap = Rc>>; + +struct Shortcut { + shortcut: GlobalShortcut, + callbacks: Slab>, +} + +impl Shortcut { + fn insert(&mut self, callback: Box) -> usize { + self.callbacks.insert(callback) + } + + fn remove(&mut self, id: usize) { + let _ = self.callbacks.remove(id); + } + + fn is_empty(&self) -> bool { + self.callbacks.is_empty() + } +} + +impl ShortcutRegistry { + pub fn new(target: &EventLoopWindowTarget) -> Self { + let myself = Self { + manager: Rc::new(RefCell::new(ShortcutManager::new(target))), + shortcuts: Rc::new(RefCell::new(HashMap::new())), + }; + // prevent CTRL+R from reloading the page which breaks apps + let _ = myself.add_shortcut( + Some(ModifiersState::CONTROL), + KeyCode::KeyR, + Box::new(|| {}), + ); + myself + } + + pub(crate) fn call_handlers(&self, id: AcceleratorId) { + if let Some(Shortcut { callbacks, .. }) = self.shortcuts.borrow_mut().get_mut(&id) { + for (_, callback) in callbacks.iter_mut() { + (callback)(); + } + } + } + + pub(crate) fn add_shortcut( + &self, + modifiers: impl Into>, + key: KeyCode, + callback: Box, + ) -> Result { + let accelerator = Accelerator::new(modifiers, key); + let accelerator_id = accelerator.clone().id(); + let mut shortcuts = self.shortcuts.borrow_mut(); + Ok( + if let Some(callbacks) = shortcuts.get_mut(&accelerator_id) { + let id = callbacks.insert(callback); + ShortcutId { + id: accelerator_id, + number: id, + } + } else { + match self.manager.borrow_mut().register(accelerator) { + Ok(global_shortcut) => { + let mut slab = Slab::new(); + let id = slab.insert(callback); + let shortcut = Shortcut { + shortcut: global_shortcut, + callbacks: slab, + }; + shortcuts.insert(accelerator_id, shortcut); + ShortcutId { + id: accelerator_id, + number: id, + } + } + Err(ShortcutManagerError::InvalidAccelerator(shortcut)) => { + return Err(ShortcutRegistryError::InvalidShortcut(shortcut)) + } + Err(err) => return Err(ShortcutRegistryError::Other(Box::new(err))), + } + }, + ) + } + + pub(crate) fn remove_shortcut(&self, id: ShortcutId) { + let mut shortcuts = self.shortcuts.borrow_mut(); + if let Some(callbacks) = shortcuts.get_mut(&id.id) { + callbacks.remove(id.number); + if callbacks.is_empty() { + if let Some(shortcut) = shortcuts.remove(&id.id) { + let _ = self.manager.borrow_mut().unregister(shortcut.shortcut); + } + } + } + } +} + +#[non_exhaustive] +#[derive(Debug)] +/// An error that can occur when registering a shortcut. +pub enum ShortcutRegistryError { + /// The shortcut is invalid. + InvalidShortcut(String), + /// An unknown error occurred. + Other(Box), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// An global id for a shortcut. +pub struct ShortcutId { + id: AcceleratorId, + number: usize, +} + +/// A global shortcut. This will be automatically removed when it is dropped. +pub struct ShortcutHandle { + desktop: DesktopContext, + /// The id of the shortcut + pub shortcut_id: ShortcutId, +} + +/// Get a closure that executes any JavaScript in the WebView context. +pub fn use_global_shortcut( + cx: &ScopeState, + key: impl IntoKeyCode, + modifiers: impl IntoModifersState, + handler: impl FnMut() + 'static, +) -> &Result { + let desktop = use_window(cx); + cx.use_hook(move || { + let desktop = desktop.clone(); + + let id = desktop.create_shortcut( + key.into_key_code(), + modifiers.into_modifiers_state(), + handler, + ); + + Ok(ShortcutHandle { + desktop, + shortcut_id: id?, + }) + }) +} + +impl ShortcutHandle { + /// Remove the shortcut. + pub fn remove(&self) { + self.desktop.remove_shortcut(self.shortcut_id); + } +} + +impl Drop for ShortcutHandle { + fn drop(&mut self) { + self.remove() + } +} + +pub trait IntoModifersState { + fn into_modifiers_state(self) -> ModifiersState; +} + +impl IntoModifersState for ModifiersState { + fn into_modifiers_state(self) -> ModifiersState { + self + } +} + +impl IntoModifersState for Modifiers { + fn into_modifiers_state(self) -> ModifiersState { + let mut state = ModifiersState::empty(); + if self.contains(Modifiers::SHIFT) { + state |= ModifiersState::SHIFT + } + if self.contains(Modifiers::CONTROL) { + state |= ModifiersState::CONTROL + } + if self.contains(Modifiers::ALT) { + state |= ModifiersState::ALT + } + if self.contains(Modifiers::META) || self.contains(Modifiers::SUPER) { + state |= ModifiersState::SUPER + } + state + } +} + +pub trait IntoKeyCode { + fn into_key_code(self) -> KeyCode; +} + +impl IntoKeyCode for KeyCode { + fn into_key_code(self) -> KeyCode { + self + } +} + +impl IntoKeyCode for dioxus_html::KeyCode { + fn into_key_code(self) -> KeyCode { + match self { + dioxus_html::KeyCode::Backspace => KeyCode::Backspace, + dioxus_html::KeyCode::Tab => KeyCode::Tab, + dioxus_html::KeyCode::Clear => KeyCode::NumpadClear, + dioxus_html::KeyCode::Enter => KeyCode::Enter, + dioxus_html::KeyCode::Shift => KeyCode::ShiftLeft, + dioxus_html::KeyCode::Ctrl => KeyCode::ControlLeft, + dioxus_html::KeyCode::Alt => KeyCode::AltLeft, + dioxus_html::KeyCode::Pause => KeyCode::Pause, + dioxus_html::KeyCode::CapsLock => KeyCode::CapsLock, + dioxus_html::KeyCode::Escape => KeyCode::Escape, + dioxus_html::KeyCode::Space => KeyCode::Space, + dioxus_html::KeyCode::PageUp => KeyCode::PageUp, + dioxus_html::KeyCode::PageDown => KeyCode::PageDown, + dioxus_html::KeyCode::End => KeyCode::End, + dioxus_html::KeyCode::Home => KeyCode::Home, + dioxus_html::KeyCode::LeftArrow => KeyCode::ArrowLeft, + dioxus_html::KeyCode::UpArrow => KeyCode::ArrowUp, + dioxus_html::KeyCode::RightArrow => KeyCode::ArrowRight, + dioxus_html::KeyCode::DownArrow => KeyCode::ArrowDown, + dioxus_html::KeyCode::Insert => KeyCode::Insert, + dioxus_html::KeyCode::Delete => KeyCode::Delete, + dioxus_html::KeyCode::Num0 => KeyCode::Numpad0, + dioxus_html::KeyCode::Num1 => KeyCode::Numpad1, + dioxus_html::KeyCode::Num2 => KeyCode::Numpad2, + dioxus_html::KeyCode::Num3 => KeyCode::Numpad3, + dioxus_html::KeyCode::Num4 => KeyCode::Numpad4, + dioxus_html::KeyCode::Num5 => KeyCode::Numpad5, + dioxus_html::KeyCode::Num6 => KeyCode::Numpad6, + dioxus_html::KeyCode::Num7 => KeyCode::Numpad7, + dioxus_html::KeyCode::Num8 => KeyCode::Numpad8, + dioxus_html::KeyCode::Num9 => KeyCode::Numpad9, + dioxus_html::KeyCode::A => KeyCode::KeyA, + dioxus_html::KeyCode::B => KeyCode::KeyB, + dioxus_html::KeyCode::C => KeyCode::KeyC, + dioxus_html::KeyCode::D => KeyCode::KeyD, + dioxus_html::KeyCode::E => KeyCode::KeyE, + dioxus_html::KeyCode::F => KeyCode::KeyF, + dioxus_html::KeyCode::G => KeyCode::KeyG, + dioxus_html::KeyCode::H => KeyCode::KeyH, + dioxus_html::KeyCode::I => KeyCode::KeyI, + dioxus_html::KeyCode::J => KeyCode::KeyJ, + dioxus_html::KeyCode::K => KeyCode::KeyK, + dioxus_html::KeyCode::L => KeyCode::KeyL, + dioxus_html::KeyCode::M => KeyCode::KeyM, + dioxus_html::KeyCode::N => KeyCode::KeyN, + dioxus_html::KeyCode::O => KeyCode::KeyO, + dioxus_html::KeyCode::P => KeyCode::KeyP, + dioxus_html::KeyCode::Q => KeyCode::KeyQ, + dioxus_html::KeyCode::R => KeyCode::KeyR, + dioxus_html::KeyCode::S => KeyCode::KeyS, + dioxus_html::KeyCode::T => KeyCode::KeyT, + dioxus_html::KeyCode::U => KeyCode::KeyU, + dioxus_html::KeyCode::V => KeyCode::KeyV, + dioxus_html::KeyCode::W => KeyCode::KeyW, + dioxus_html::KeyCode::X => KeyCode::KeyX, + dioxus_html::KeyCode::Y => KeyCode::KeyY, + dioxus_html::KeyCode::Z => KeyCode::KeyZ, + dioxus_html::KeyCode::Numpad0 => KeyCode::Numpad0, + dioxus_html::KeyCode::Numpad1 => KeyCode::Numpad1, + dioxus_html::KeyCode::Numpad2 => KeyCode::Numpad2, + dioxus_html::KeyCode::Numpad3 => KeyCode::Numpad3, + dioxus_html::KeyCode::Numpad4 => KeyCode::Numpad4, + dioxus_html::KeyCode::Numpad5 => KeyCode::Numpad5, + dioxus_html::KeyCode::Numpad6 => KeyCode::Numpad6, + dioxus_html::KeyCode::Numpad7 => KeyCode::Numpad7, + dioxus_html::KeyCode::Numpad8 => KeyCode::Numpad8, + dioxus_html::KeyCode::Numpad9 => KeyCode::Numpad9, + dioxus_html::KeyCode::Multiply => KeyCode::NumpadMultiply, + dioxus_html::KeyCode::Add => KeyCode::NumpadAdd, + dioxus_html::KeyCode::Subtract => KeyCode::NumpadSubtract, + dioxus_html::KeyCode::DecimalPoint => KeyCode::NumpadDecimal, + dioxus_html::KeyCode::Divide => KeyCode::NumpadDivide, + dioxus_html::KeyCode::F1 => KeyCode::F1, + dioxus_html::KeyCode::F2 => KeyCode::F2, + dioxus_html::KeyCode::F3 => KeyCode::F3, + dioxus_html::KeyCode::F4 => KeyCode::F4, + dioxus_html::KeyCode::F5 => KeyCode::F5, + dioxus_html::KeyCode::F6 => KeyCode::F6, + dioxus_html::KeyCode::F7 => KeyCode::F7, + dioxus_html::KeyCode::F8 => KeyCode::F8, + dioxus_html::KeyCode::F9 => KeyCode::F9, + dioxus_html::KeyCode::F10 => KeyCode::F10, + dioxus_html::KeyCode::F11 => KeyCode::F11, + dioxus_html::KeyCode::F12 => KeyCode::F12, + dioxus_html::KeyCode::NumLock => KeyCode::NumLock, + dioxus_html::KeyCode::ScrollLock => KeyCode::ScrollLock, + dioxus_html::KeyCode::Semicolon => KeyCode::Semicolon, + dioxus_html::KeyCode::EqualSign => KeyCode::Equal, + dioxus_html::KeyCode::Comma => KeyCode::Comma, + dioxus_html::KeyCode::Period => KeyCode::Period, + dioxus_html::KeyCode::ForwardSlash => KeyCode::Slash, + dioxus_html::KeyCode::GraveAccent => KeyCode::Backquote, + dioxus_html::KeyCode::OpenBracket => KeyCode::BracketLeft, + dioxus_html::KeyCode::BackSlash => KeyCode::Backslash, + dioxus_html::KeyCode::CloseBraket => KeyCode::BracketRight, + dioxus_html::KeyCode::SingleQuote => KeyCode::Quote, + key => panic!("Failed to convert {:?} to tao::keyboard::KeyCode, try using tao::keyboard::KeyCode directly", key), + } + } +}