From 292f757d5d86109a31043450811eb9f99c793395 Mon Sep 17 00:00:00 2001 From: Klemen Date: Thu, 31 Oct 2024 20:46:17 +0100 Subject: [PATCH] SystemTray (#3123) * Added tray icon --- Cargo.toml | 1 + packages/desktop/Cargo.toml | 2 + packages/desktop/src/app.rs | 48 ++++++++++++++++++++++ packages/desktop/src/hooks.rs | 34 ++++++++++++++++ packages/desktop/src/ipc.rs | 6 +++ packages/desktop/src/launch.rs | 6 +++ packages/desktop/src/lib.rs | 4 ++ packages/desktop/src/trayicon.rs | 68 ++++++++++++++++++++++++++++++++ 8 files changed, 169 insertions(+) create mode 100644 packages/desktop/src/trayicon.rs diff --git a/Cargo.toml b/Cargo.toml index 2520d9d3f..819330082 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -235,6 +235,7 @@ cocoa = "0.26" core-foundation = "0.10.0" objc = { version = "0.2.7", features = ["exception"] } objc_id = "0.1.1" +tray-icon = "0.19" # our release profile should be fast to compile and fast to run # when we ship our CI builds, we turn on LTO which improves perf leftover by turning on incremental diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 200edfe23..b39fd6278 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -73,6 +73,8 @@ global-hotkey = "0.5.0" rfd = { version = "0.14", default-features = false, features = ["xdg-portal", "tokio"] } muda = "0.11.3" +[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux"))'.dependencies] +tray-icon = { workspace = true } [target.'cfg(target_os = "ios")'.dependencies] objc = "0.2.7" diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 389bcc483..557d05bc4 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -85,6 +85,10 @@ impl App { #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] app.set_menubar_receiver(); + // Wire up the tray icon receiver - this way any component can key into the menubar actions + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + app.set_tray_icon_receiver(); + // Allow hotreloading to work - but only in debug mode #[cfg(all(feature = "devtools", debug_assertions))] app.connect_hotreload(); @@ -134,6 +138,29 @@ impl App { _ => (), } } + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + pub fn handle_tray_menu_event(&mut self, event: tray_icon::menu::MenuEvent) { + _ = event; + } + + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + pub fn handle_tray_icon_event(&mut self, event: tray_icon::TrayIconEvent) { + if let tray_icon::TrayIconEvent::Click { + id: _, + position: _, + rect: _, + button, + button_state: _, + } = event + { + if button == tray_icon::MouseButton::Left { + for webview in self.webviews.values() { + webview.desktop_context.window.set_visible(true); + webview.desktop_context.window.set_focus(); + } + } + } + } #[cfg(all(feature = "devtools", debug_assertions))] pub fn connect_hotreload(&self) { @@ -360,6 +387,27 @@ impl App { })); } + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + fn set_tray_icon_receiver(&self) { + let receiver = self.shared.proxy.clone(); + + // The event loop becomes the menu receiver + // This means we don't need to poll the receiver on every tick - we just get the events as they come in + // This is a bit more efficient than the previous implementation, but if someone else sets a handler, the + // receiver will become inert. + tray_icon::TrayIconEvent::set_event_handler(Some(move |t| { + // todo: should we unset the event handler when the app shuts down? + _ = receiver.send_event(UserWindowEvent::TrayIconEvent(t)); + })); + + // for whatever reason they had to make it separate + let receiver = self.shared.proxy.clone(); + tray_icon::menu::MenuEvent::set_event_handler(Some(move |t| { + // todo: should we unset the event handler when the app shuts down? + _ = receiver.send_event(UserWindowEvent::TrayMenuEvent(t)); + })); + } + /// Do our best to preserve state about the window when the event loop is destroyed /// /// This will attempt to save the window position, size, and monitor into the environment before diff --git a/packages/desktop/src/hooks.rs b/packages/desktop/src/hooks.rs index 237696d8d..c7992fc16 100644 --- a/packages/desktop/src/hooks.rs +++ b/packages/desktop/src/hooks.rs @@ -54,6 +54,40 @@ pub fn use_muda_event_handler( }) } +/// Register an event handler that runs when a tray icon menu event is processed. +#[cfg_attr( + docsrs, + doc(cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))) +)] +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +pub fn use_tray_menu_event_handler( + mut handler: impl FnMut(&tray_icon::menu::MenuEvent) + 'static, +) -> WryEventHandler { + use_wry_event_handler(move |event, _| { + if let Event::UserEvent(UserWindowEvent::TrayMenuEvent(event)) = event { + handler(event); + } + }) +} + +/// Register an event handler that runs when a tray icon event is processed. +/// This is only for tray icon and not it's menus. +/// If you want to register tray icon menus handler use `use_tray_menu_event_handler` instead. +#[cfg_attr( + docsrs, + doc(cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))) +)] +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +pub fn use_tray_icon_event_handler( + mut handler: impl FnMut(&tray_icon::TrayIconEvent) + 'static, +) -> WryEventHandler { + use_wry_event_handler(move |event, _| { + if let Event::UserEvent(UserWindowEvent::TrayIconEvent(event)) = event { + handler(event); + } + }) +} + /// Provide a callback to handle asset loading yourself. /// /// The callback takes a path as requested by the web view, and it should return `Some(response)` diff --git a/packages/desktop/src/ipc.rs b/packages/desktop/src/ipc.rs index c89a502bd..9dfbf104b 100644 --- a/packages/desktop/src/ipc.rs +++ b/packages/desktop/src/ipc.rs @@ -11,6 +11,12 @@ pub enum UserWindowEvent { #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] MudaMenuEvent(muda::MenuEvent), + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + TrayIconEvent(tray_icon::TrayIconEvent), + + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + TrayMenuEvent(tray_icon::menu::MenuEvent), + /// Poll the virtualdom Poll(WindowId), diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs index 7cb659446..04d5a12db 100644 --- a/packages/desktop/src/launch.rs +++ b/packages/desktop/src/launch.rs @@ -42,6 +42,12 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Conf #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] UserWindowEvent::MudaMenuEvent(evnt) => app.handle_menu_event(evnt), + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + UserWindowEvent::TrayMenuEvent(evnt) => app.handle_tray_menu_event(evnt), + + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + UserWindowEvent::TrayIconEvent(evnt) => app.handle_tray_icon_event(evnt), + #[cfg(all(feature = "devtools", debug_assertions))] UserWindowEvent::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg), diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 8e7a4303d..d71c3934e 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -41,6 +41,10 @@ pub use wry; #[cfg(not(any(target_os = "ios", target_os = "android")))] pub use muda; +// Tray icon +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +pub mod trayicon; + // Public exports pub use assets::AssetRequest; pub use config::{Config, WindowCloseBehaviour}; diff --git a/packages/desktop/src/trayicon.rs b/packages/desktop/src/trayicon.rs new file mode 100644 index 000000000..8e16d424f --- /dev/null +++ b/packages/desktop/src/trayicon.rs @@ -0,0 +1,68 @@ +//! tray icon + +use dioxus_core::{ + prelude::{provide_context, try_consume_context}, + use_hook, +}; + +#[cfg(not(any(target_os = "ios", target_os = "android")))] +pub use tray_icon::*; + +/// tray icon menu type trait +#[cfg(not(any(target_os = "ios", target_os = "android")))] +pub type DioxusTrayMenu = tray_icon::menu::Menu; +#[cfg(any(target_os = "ios", target_os = "android"))] +pub type DioxusTrayMenu = (); + +/// tray icon icon type trait +#[cfg(not(any(target_os = "ios", target_os = "android")))] +pub type DioxusTrayIcon = tray_icon::Icon; +#[cfg(any(target_os = "ios", target_os = "android"))] +pub type DioxusTrayIcon = (); + +/// tray icon type trait +#[cfg(not(any(target_os = "ios", target_os = "android")))] +pub type DioxusTray = tray_icon::TrayIcon; +#[cfg(any(target_os = "ios", target_os = "android"))] +pub type DioxusTray = (); + +/// initializes a tray icon +#[allow(unused)] +pub fn init_tray_icon(menu: DioxusTrayMenu, icon: Option) -> DioxusTray { + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + { + let builder = tray_icon::TrayIconBuilder::new() + .with_menu(Box::new(menu)) + .with_menu_on_left_click(false) + .with_icon(match icon { + Some(value) => value, + None => tray_icon::Icon::from_rgba( + include_bytes!("./assets/default_icon.bin").to_vec(), + 460, + 460, + ) + .expect("image parse failed"), + }); + + provide_context(builder.build().expect("tray icon builder failed")) + } +} + +/// Returns a default tray icon menu +pub fn default_tray_icon() -> DioxusTrayMenu { + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + { + use tray_icon::menu::{Menu, PredefinedMenuItem}; + let tray_menu = Menu::new(); + tray_menu + .append_items(&[&PredefinedMenuItem::quit(None)]) + .unwrap(); + tray_menu + } +} + +/// Provides a hook to the tray icon +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +pub fn use_tray_icon() -> Option { + use_hook(try_consume_context) +}