Merge pull request #856 from Demonthos/support-global-hotkeys

Create global shortcut handler for Dioxus desktop
This commit is contained in:
Jon Kelley 2023-03-02 09:15:52 -08:00 committed by GitHub
commit 66b62fc8ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 380 additions and 3 deletions

View file

@ -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

View file

@ -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

17
examples/shortcut.rs Normal file
View file

@ -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()}"))
}

View file

@ -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<RefCell<Vec<*mut objc::runtime::Object>>>,
}
@ -83,6 +90,7 @@ impl DesktopContext {
event_loop: EventLoopWindowTarget<UserWindowEvent>,
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<ShortcutId, ShortcutRegistryError> {
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<objc::runtime::Object>) {

View file

@ -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<P: 'static>(root: Component<P>, 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<P: 'static>(root: Component<P>, 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<P: 'static>(root: Component<P>, 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();

View file

@ -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

View file

@ -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<RefCell<ShortcutManager>>,
shortcuts: ShortcutMap,
}
type ShortcutMap = Rc<RefCell<HashMap<AcceleratorId, Shortcut>>>;
struct Shortcut {
shortcut: GlobalShortcut,
callbacks: Slab<Box<dyn FnMut()>>,
}
impl Shortcut {
fn insert(&mut self, callback: Box<dyn FnMut()>) -> 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<T>(target: &EventLoopWindowTarget<T>) -> 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<Option<ModifiersState>>,
key: KeyCode,
callback: Box<dyn FnMut()>,
) -> Result<ShortcutId, ShortcutRegistryError> {
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<dyn std::error::Error>),
}
#[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<ShortcutHandle, ShortcutRegistryError> {
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),
}
}
}