diff --git a/.changes/runtime-tray.md b/.changes/runtime-tray.md new file mode 100644 index 000000000..69eeb6c31 --- /dev/null +++ b/.changes/runtime-tray.md @@ -0,0 +1,7 @@ +--- +"tauri": minor +"tauri-runtime": minor +"tauri-runtime-wry": minor +--- + +Added APIs to create a system tray at runtime. diff --git a/.changes/tray-destroy.md b/.changes/tray-destroy.md new file mode 100644 index 000000000..f4f8e5893 --- /dev/null +++ b/.changes/tray-destroy.md @@ -0,0 +1,5 @@ +--- +"tauri": minor +--- + +Added the `SystemTrayHandle::destroy` method. diff --git a/Cargo.toml b/Cargo.toml index 7368cf18b..4d739f148 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ "core/tauri-utils", "core/tauri-build", "core/tauri-codegen", - + # integration tests "core/tests/restart", "core/tests/app-updater" diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs index b6478fdf8..e269c1177 100644 --- a/core/tauri-runtime-wry/src/lib.rs +++ b/core/tauri-runtime-wry/src/lib.rs @@ -30,16 +30,11 @@ use webview2_com::FocusChangedEventHandler; use windows::Win32::{Foundation::HWND, System::WinRT::EventRegistrationToken}; #[cfg(target_os = "macos")] use wry::application::platform::macos::WindowBuilderExtMacOS; -#[cfg(all(feature = "system-tray", target_os = "macos"))] -use wry::application::platform::macos::{SystemTrayBuilderExtMacOS, SystemTrayExtMacOS}; #[cfg(target_os = "linux")] use wry::application::platform::unix::{WindowBuilderExtUnix, WindowExtUnix}; #[cfg(windows)] use wry::application::platform::windows::{WindowBuilderExtWindows, WindowExtWindows}; -#[cfg(all(desktop, feature = "system-tray"))] -use wry::application::system_tray::{SystemTray as WrySystemTray, SystemTrayBuilder}; - use tauri_utils::{config::WindowConfig, debug_eprintln, Theme}; use uuid::Uuid; use wry::{ @@ -91,7 +86,6 @@ use std::{ HashMap, HashSet, }, fmt, - marker::PhantomData, ops::Deref, path::PathBuf, sync::{ @@ -104,6 +98,8 @@ use std::{ pub type WebviewId = u64; type IpcHandler = dyn Fn(&Window, String) + 'static; type FileDropHandler = dyn Fn(&Window, WryFileDropEvent) -> bool + 'static; +#[cfg(all(desktop, feature = "system-tray"))] +pub use tauri_runtime::TrayId; #[cfg(desktop)] mod webview; @@ -173,7 +169,6 @@ fn send_user_message(context: &Context, message: Message) -> &context.main_thread.window_target, message, UserMessageContext { - marker: &PhantomData, webview_id_map: context.webview_id_map.clone(), #[cfg(all(desktop, feature = "global-shortcut"))] global_shortcut_manager: context.main_thread.global_shortcut_manager.clone(), @@ -181,7 +176,7 @@ fn send_user_message(context: &Context, message: Message) -> clipboard_manager: context.main_thread.clipboard_manager.clone(), windows: context.main_thread.windows.clone(), #[cfg(all(desktop, feature = "system-tray"))] - tray_context: &context.main_thread.tray_context, + system_tray_manager: context.main_thread.system_tray_manager.clone(), }, &context.main_thread.web_context, ); @@ -256,7 +251,7 @@ pub struct DispatcherMainThreadContext { pub clipboard_manager: Arc>, pub windows: Arc>>, #[cfg(all(desktop, feature = "system-tray"))] - pub tray_context: TrayContext, + system_tray_manager: SystemTrayManager, } // SAFETY: we ensure this type is only used on the main thread. @@ -1086,7 +1081,8 @@ pub enum TrayMessage { UpdateIcon(Icon), #[cfg(target_os = "macos")] UpdateIconAsTemplate(bool), - Close, + Create(SystemTray, Sender>), + Destroy, } pub type CreateWebviewClosure = Box< @@ -1098,7 +1094,7 @@ pub enum Message { Window(WebviewId, WindowMessage), Webview(WebviewId, WebviewMessage), #[cfg(all(desktop, feature = "system-tray"))] - Tray(TrayMessage), + Tray(TrayId, TrayMessage), CreateWebview(WebviewId, CreateWebviewClosure), CreateWindow( WebviewId, @@ -1117,7 +1113,7 @@ impl Clone for Message { match self { Self::Webview(i, m) => Self::Webview(*i, m.clone()), #[cfg(all(desktop, feature = "system-tray"))] - Self::Tray(m) => Self::Tray(m.clone()), + Self::Tray(i, m) => Self::Tray(*i, m.clone()), #[cfg(all(desktop, feature = "global-shortcut"))] Self::GlobalShortcut(m) => Self::GlobalShortcut(m.clone()), #[cfg(feature = "clipboard")] @@ -1525,23 +1521,6 @@ impl Dispatch for WryDispatcher { } } -#[cfg(all(desktop, feature = "system-tray"))] -#[derive(Clone, Default)] -pub struct TrayContext { - tray: Arc>>>>, - listeners: SystemTrayEventListeners, - items: SystemTrayItems, -} - -#[cfg(all(desktop, feature = "system-tray"))] -impl fmt::Debug for TrayContext { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("TrayContext") - .field("items", &self.items) - .finish() - } -} - #[derive(Clone)] enum WindowHandle { Webview(Arc), @@ -1650,7 +1629,10 @@ impl fmt::Debug for Wry { .field("web_context", &self.context.main_thread.web_context); #[cfg(all(desktop, feature = "system-tray"))] - d.field("tray_context", &self.context.main_thread.tray_context); + d.field( + "system_tray_manager", + &self.context.main_thread.system_tray_manager, + ); #[cfg(all(desktop, feature = "global-shortcut"))] #[cfg(feature = "global-shortcut")] @@ -1741,9 +1723,22 @@ impl RuntimeHandle for WryHandle { send_user_message(&self.context, Message::Task(Box::new(f))) } - #[cfg(all(windows, feature = "system-tray"))] - fn remove_system_tray(&self) -> Result<()> { - send_user_message(&self.context, Message::Tray(TrayMessage::Close)) + #[cfg(all(desktop, feature = "system-tray"))] + fn system_tray( + &self, + system_tray: SystemTray, + ) -> Result<>::TrayHandler> { + let id = system_tray.id; + let (tx, rx) = channel(); + send_user_message( + &self.context, + Message::Tray(id, TrayMessage::Create(system_tray, tx)), + )?; + rx.recv().unwrap()?; + Ok(SystemTrayHandle { + id, + proxy: self.context.proxy.clone(), + }) } fn raw_display_handle(&self) -> RawDisplayHandle { @@ -1766,7 +1761,7 @@ impl Wry { let webview_id_map = WebviewIdStore::default(); #[cfg(all(desktop, feature = "system-tray"))] - let tray_context = TrayContext::default(); + let system_tray_manager = Default::default(); let context = Context { webview_id_map, @@ -1781,7 +1776,7 @@ impl Wry { clipboard_manager, windows, #[cfg(all(desktop, feature = "system-tray"))] - tray_context, + system_tray_manager, }, }; @@ -1906,50 +1901,45 @@ impl Runtime for Wry { } #[cfg(all(desktop, feature = "system-tray"))] - fn system_tray(&self, system_tray: SystemTray) -> Result { - let icon = TrayIcon::try_from(system_tray.icon.expect("tray icon not set"))?; - - let mut items = HashMap::new(); - - #[allow(unused_mut)] - let mut tray_builder = SystemTrayBuilder::new( - icon.0, - system_tray - .menu - .map(|menu| to_wry_context_menu(&mut items, menu)), - ); - - #[cfg(target_os = "macos")] - { - tray_builder = tray_builder - .with_icon_as_template(system_tray.icon_as_template) - .with_menu_on_left_click(system_tray.menu_on_left_click); + fn system_tray(&self, mut system_tray: SystemTray) -> Result { + let id = system_tray.id; + let mut listeners = Vec::new(); + if let Some(l) = system_tray.on_event.take() { + listeners.push(Arc::new(l)); } - - let tray = tray_builder - .build(&self.event_loop) - .map_err(|e| Error::SystemTray(Box::new(e)))?; - - *self.context.main_thread.tray_context.items.lock().unwrap() = items; - *self.context.main_thread.tray_context.tray.lock().unwrap() = Some(Arc::new(Mutex::new(tray))); + let (tray, items) = create_tray(WryTrayId(id), system_tray, &self.event_loop)?; + self + .context + .main_thread + .system_tray_manager + .trays + .lock() + .unwrap() + .insert( + id, + TrayContext { + tray: Arc::new(Mutex::new(Some(tray))), + listeners: Arc::new(Mutex::new(listeners)), + items: Arc::new(Mutex::new(items)), + }, + ); Ok(SystemTrayHandle { + id, proxy: self.event_loop.create_proxy(), }) } #[cfg(all(desktop, feature = "system-tray"))] - fn on_system_tray_event(&mut self, f: F) -> Uuid { - let id = Uuid::new_v4(); + fn on_system_tray_event(&mut self, f: F) { self .context .main_thread - .tray_context - .listeners + .system_tray_manager + .global_listeners .lock() .unwrap() - .insert(id, Arc::new(Box::new(f))); - id + .push(Arc::new(Box::new(f))); } #[cfg(target_os = "macos")] @@ -1972,7 +1962,7 @@ impl Runtime for Wry { let web_context = &self.context.main_thread.web_context; let plugins = &mut self.plugins; #[cfg(all(desktop, feature = "system-tray"))] - let tray_context = self.context.main_thread.tray_context.clone(); + let system_tray_manager = self.context.main_thread.system_tray_manager.clone(); #[cfg(all(desktop, feature = "global-shortcut"))] let global_shortcut_manager = self.context.main_thread.global_shortcut_manager.clone(); @@ -2010,7 +2000,7 @@ impl Runtime for Wry { #[cfg(feature = "clipboard")] clipboard_manager: clipboard_manager.clone(), #[cfg(all(desktop, feature = "system-tray"))] - tray_context: &tray_context, + system_tray_manager: system_tray_manager.clone(), }, web_context, ); @@ -2034,7 +2024,7 @@ impl Runtime for Wry { #[cfg(feature = "clipboard")] clipboard_manager: clipboard_manager.clone(), #[cfg(all(desktop, feature = "system-tray"))] - tray_context: &tray_context, + system_tray_manager: system_tray_manager.clone(), }, web_context, ); @@ -2050,7 +2040,7 @@ impl Runtime for Wry { let mut plugins = self.plugins; #[cfg(all(desktop, feature = "system-tray"))] - let tray_context = self.context.main_thread.tray_context; + let system_tray_manager = self.context.main_thread.system_tray_manager; #[cfg(all(desktop, feature = "global-shortcut"))] let global_shortcut_manager = self.context.main_thread.global_shortcut_manager.clone(); @@ -2080,7 +2070,7 @@ impl Runtime for Wry { #[cfg(feature = "clipboard")] clipboard_manager: clipboard_manager.clone(), #[cfg(all(desktop, feature = "system-tray"))] - tray_context: &tray_context, + system_tray_manager: system_tray_manager.clone(), }, &web_context, ); @@ -2103,7 +2093,7 @@ impl Runtime for Wry { #[cfg(feature = "clipboard")] clipboard_manager: clipboard_manager.clone(), #[cfg(all(desktop, feature = "system-tray"))] - tray_context: &tray_context, + system_tray_manager: system_tray_manager.clone(), }, &web_context, ); @@ -2122,12 +2112,10 @@ pub struct EventLoopIterationContext<'a, T: UserEvent> { #[cfg(feature = "clipboard")] pub clipboard_manager: Arc>, #[cfg(all(desktop, feature = "system-tray"))] - pub tray_context: &'a TrayContext, + pub system_tray_manager: SystemTrayManager, } -struct UserMessageContext<'a> { - #[allow(dead_code)] - marker: &'a PhantomData<()>, +struct UserMessageContext { webview_id_map: WebviewIdStore, #[cfg(all(desktop, feature = "global-shortcut"))] global_shortcut_manager: Arc>, @@ -2135,17 +2123,16 @@ struct UserMessageContext<'a> { clipboard_manager: Arc>, windows: Arc>>, #[cfg(all(desktop, feature = "system-tray"))] - tray_context: &'a TrayContext, + system_tray_manager: SystemTrayManager, } fn handle_user_message( event_loop: &EventLoopWindowTarget>, message: Message, - context: UserMessageContext<'_>, + context: UserMessageContext, web_context: &WebContextStore, ) -> RunIteration { let UserMessageContext { - marker: _, webview_id_map, #[cfg(all(desktop, feature = "global-shortcut"))] global_shortcut_manager, @@ -2153,7 +2140,7 @@ fn handle_user_message( clipboard_manager, windows, #[cfg(all(desktop, feature = "system-tray"))] - tray_context, + system_tray_manager, } = context; match message { Message::Task(task) => task(), @@ -2455,49 +2442,74 @@ fn handle_user_message( } #[cfg(all(desktop, feature = "system-tray"))] - Message::Tray(tray_message) => match tray_message { - TrayMessage::UpdateItem(menu_id, update) => { - let mut tray = tray_context.items.as_ref().lock().unwrap(); - let item = tray.get_mut(&menu_id).expect("menu item not found"); - match update { - MenuUpdate::SetEnabled(enabled) => item.set_enabled(enabled), - MenuUpdate::SetTitle(title) => item.set_title(&title), - MenuUpdate::SetSelected(selected) => item.set_selected(selected), + Message::Tray(tray_id, tray_message) => { + let mut trays = system_tray_manager.trays.lock().unwrap(); + + if let TrayMessage::Create(tray, tx) = tray_message { + match create_tray(WryTrayId(tray_id), tray, event_loop) { + Ok((tray, items)) => { + trays.insert( + tray_id, + TrayContext { + tray: Arc::new(Mutex::new(Some(tray))), + listeners: Default::default(), + items: Arc::new(Mutex::new(items)), + }, + ); + + tx.send(Ok(())).unwrap(); + } + + Err(e) => { + tx.send(Err(e)).unwrap(); + } + } + } else if let Some(tray_context) = trays.get(&tray_id) { + match tray_message { + TrayMessage::UpdateItem(menu_id, update) => { + let mut tray = tray_context.items.as_ref().lock().unwrap(); + let item = tray.get_mut(&menu_id).expect("menu item not found"); + match update { + MenuUpdate::SetEnabled(enabled) => item.set_enabled(enabled), + MenuUpdate::SetTitle(title) => item.set_title(&title), + MenuUpdate::SetSelected(selected) => item.set_selected(selected), + #[cfg(target_os = "macos")] + MenuUpdate::SetNativeImage(image) => { + item.set_native_image(NativeImageWrapper::from(image).0) + } + } + } + TrayMessage::UpdateMenu(menu) => { + if let Some(tray) = &mut *tray_context.tray.lock().unwrap() { + let mut items = HashMap::new(); + tray.set_menu(&to_wry_context_menu(&mut items, menu)); + *tray_context.items.lock().unwrap() = items; + } + } + TrayMessage::UpdateIcon(icon) => { + if let Some(tray) = &mut *tray_context.tray.lock().unwrap() { + if let Ok(icon) = TrayIcon::try_from(icon) { + tray.set_icon(icon.0); + } + } + } #[cfg(target_os = "macos")] - MenuUpdate::SetNativeImage(image) => { - item.set_native_image(NativeImageWrapper::from(image).0) + TrayMessage::UpdateIconAsTemplate(is_template) => { + if let Some(tray) = &mut *tray_context.tray.lock().unwrap() { + tray.set_icon_as_template(is_template); + } + } + TrayMessage::Create(_tray, _tx) => { + // already handled + } + TrayMessage::Destroy => { + *tray_context.tray.lock().unwrap() = None; + tray_context.listeners.lock().unwrap().clear(); + tray_context.items.lock().unwrap().clear(); } } } - TrayMessage::UpdateMenu(menu) => { - if let Some(tray) = &*tray_context.tray.lock().unwrap() { - let mut items = HashMap::new(); - tray - .lock() - .unwrap() - .set_menu(&to_wry_context_menu(&mut items, menu)); - *tray_context.items.lock().unwrap() = items; - } - } - TrayMessage::UpdateIcon(icon) => { - if let Some(tray) = &*tray_context.tray.lock().unwrap() { - if let Ok(icon) = TrayIcon::try_from(icon) { - tray.lock().unwrap().set_icon(icon.0); - } - } - } - #[cfg(target_os = "macos")] - TrayMessage::UpdateIconAsTemplate(is_template) => { - if let Some(tray) = &*tray_context.tray.lock().unwrap() { - tray.lock().unwrap().set_icon_as_template(is_template); - } - } - TrayMessage::Close => { - *tray_context.tray.lock().unwrap() = None; - tray_context.listeners.lock().unwrap().clear(); - tray_context.items.lock().unwrap().clear(); - } - }, + } #[cfg(all(desktop, feature = "global-shortcut"))] Message::GlobalShortcut(message) => { handle_global_shortcut_message(message, &global_shortcut_manager) @@ -2531,7 +2543,7 @@ fn handle_event_loop( #[cfg(feature = "clipboard")] clipboard_manager, #[cfg(all(desktop, feature = "system-tray"))] - tray_context, + system_tray_manager, } = context; if *control_flow != ControlFlow::Exit { *control_flow = ControlFlow::Wait; @@ -2612,13 +2624,40 @@ fn handle_event_loop( .. } => { let event = SystemTrayEvent::MenuItemClick(menu_id.0); - let listeners = tray_context.listeners.lock().unwrap().clone(); - for handler in listeners.values() { - handler(&event); + + let trays = system_tray_manager.trays.lock().unwrap(); + let trays_iter = trays.iter(); + + let (mut listeners, mut tray_id) = (None, 0); + for (id, tray_context) in trays_iter { + let has_menu = { + let items = tray_context.items.lock().unwrap(); + items.contains_key(&menu_id.0) + }; + if has_menu { + listeners.replace(tray_context.listeners.clone()); + tray_id = *id; + break; + } + } + drop(trays); + if let Some(listeners) = listeners { + let listeners = listeners.lock().unwrap(); + let handlers = listeners.iter(); + for handler in handlers { + handler(&event); + } + + let global_listeners = system_tray_manager.global_listeners.lock().unwrap(); + let global_listeners_iter = global_listeners.iter(); + for global_listener in global_listeners_iter { + global_listener(tray_id, &event); + } } } #[cfg(all(desktop, feature = "system-tray"))] Event::TrayEvent { + id, bounds, event, position: _cursor_position, @@ -2634,10 +2673,13 @@ fn handle_event_loop( // default to left click _ => SystemTrayEvent::LeftClick { position, size }, }; - let listeners = tray_context.listeners.lock().unwrap(); - let handlers = listeners.values(); - for handler in handlers { - handler(&event); + let trays = system_tray_manager.trays.lock().unwrap(); + if let Some(tray_context) = trays.get(&id.0) { + let listeners = tray_context.listeners.lock().unwrap(); + let iter = listeners.iter(); + for handler in iter { + handler(&event); + } } } Event::WindowEvent { @@ -2715,7 +2757,6 @@ fn handle_event_loop( event_loop, message, UserMessageContext { - marker: &PhantomData, webview_id_map, #[cfg(all(desktop, feature = "global-shortcut"))] global_shortcut_manager, @@ -2723,7 +2764,7 @@ fn handle_event_loop( clipboard_manager, windows, #[cfg(all(desktop, feature = "system-tray"))] - tray_context, + system_tray_manager, }, web_context, ); diff --git a/core/tauri-runtime-wry/src/system_tray.rs b/core/tauri-runtime-wry/src/system_tray.rs index 1a5f11c1d..cb3f8863b 100644 --- a/core/tauri-runtime-wry/src/system_tray.rs +++ b/core/tauri-runtime-wry/src/system_tray.rs @@ -9,6 +9,7 @@ pub use tauri_runtime::{ }, Icon, SystemTrayEvent, }; +use wry::application::event_loop::EventLoopWindowTarget; pub use wry::application::{ event::TrayEvent, event_loop::EventLoopProxy, @@ -16,26 +17,62 @@ pub use wry::application::{ ContextMenu as WryContextMenu, CustomMenuItem as WryCustomMenuItem, MenuItem as WryMenuItem, }, system_tray::Icon as WryTrayIcon, + TrayId as WryTrayId, }; #[cfg(target_os = "macos")] -pub use wry::application::platform::macos::CustomMenuItemExtMacOS; +pub use wry::application::platform::macos::{ + CustomMenuItemExtMacOS, SystemTrayBuilderExtMacOS, SystemTrayExtMacOS, +}; -use crate::{Error, Message, Result, TrayMessage}; +use wry::application::system_tray::{SystemTray as WrySystemTray, SystemTrayBuilder}; -use tauri_runtime::{menu::MenuHash, UserEvent}; +use crate::{Error, Message, Result, TrayId, TrayMessage}; -use uuid::Uuid; +use tauri_runtime::{menu::MenuHash, SystemTray, UserEvent}; use std::{ collections::HashMap, + fmt, sync::{Arc, Mutex}, }; +pub type GlobalSystemTrayEventHandler = Box; +pub type GlobalSystemTrayEventListeners = Arc>>>; + pub type SystemTrayEventHandler = Box; -pub type SystemTrayEventListeners = Arc>>>; +pub type SystemTrayEventListeners = Arc>>>; pub type SystemTrayItems = Arc>>; +#[derive(Clone, Default)] +pub struct TrayContext { + pub tray: Arc>>, + pub listeners: SystemTrayEventListeners, + pub items: SystemTrayItems, +} + +impl fmt::Debug for TrayContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TrayContext") + .field("items", &self.items) + .finish() + } +} + +#[derive(Clone, Default)] +pub struct SystemTrayManager { + pub trays: Arc>>, + pub global_listeners: GlobalSystemTrayEventListeners, +} + +impl fmt::Debug for SystemTrayManager { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SystemTrayManager") + .field("trays", &self.trays) + .finish() + } +} + /// Wrapper around a [`wry::application::system_tray::Icon`] that can be created from an [`WindowIcon`]. pub struct TrayIcon(pub(crate) WryTrayIcon); @@ -48,8 +85,39 @@ impl TryFrom for TrayIcon { } } +pub fn create_tray( + id: WryTrayId, + system_tray: SystemTray, + event_loop: &EventLoopWindowTarget, +) -> crate::Result<(WrySystemTray, HashMap)> { + let icon = TrayIcon::try_from(system_tray.icon.expect("tray icon not set"))?; + + let mut items = HashMap::new(); + + #[allow(unused_mut)] + let mut builder = SystemTrayBuilder::new( + icon.0, + system_tray + .menu + .map(|menu| to_wry_context_menu(&mut items, menu)), + ) + .with_id(id); + + #[cfg(target_os = "macos")] + { + builder = builder.with_icon_as_template(system_tray.icon_as_template) + } + + let tray = builder + .build(event_loop) + .map_err(|e| Error::SystemTray(Box::new(e)))?; + + Ok((tray, items)) +} + #[derive(Debug, Clone)] pub struct SystemTrayHandle { + pub(crate) id: TrayId, pub(crate) proxy: EventLoopProxy>, } @@ -57,28 +125,39 @@ impl TrayHandle for SystemTrayHandle { fn set_icon(&self, icon: Icon) -> Result<()> { self .proxy - .send_event(Message::Tray(TrayMessage::UpdateIcon(icon))) + .send_event(Message::Tray(self.id, TrayMessage::UpdateIcon(icon))) .map_err(|_| Error::FailedToSendMessage) } + fn set_menu(&self, menu: SystemTrayMenu) -> Result<()> { self .proxy - .send_event(Message::Tray(TrayMessage::UpdateMenu(menu))) + .send_event(Message::Tray(self.id, TrayMessage::UpdateMenu(menu))) .map_err(|_| Error::FailedToSendMessage) } + fn update_item(&self, id: u16, update: MenuUpdate) -> Result<()> { self .proxy - .send_event(Message::Tray(TrayMessage::UpdateItem(id, update))) + .send_event(Message::Tray(self.id, TrayMessage::UpdateItem(id, update))) .map_err(|_| Error::FailedToSendMessage) } + #[cfg(target_os = "macos")] fn set_icon_as_template(&self, is_template: bool) -> tauri_runtime::Result<()> { self .proxy - .send_event(Message::Tray(TrayMessage::UpdateIconAsTemplate( - is_template, - ))) + .send_event(Message::Tray( + self.id, + TrayMessage::UpdateIconAsTemplate(is_template), + )) + .map_err(|_| Error::FailedToSendMessage) + } + + fn destroy(&self) -> Result<()> { + self + .proxy + .send_event(Message::Tray(self.id, TrayMessage::Destroy)) .map_err(|_| Error::FailedToSendMessage) } } diff --git a/core/tauri-runtime/Cargo.toml b/core/tauri-runtime/Cargo.toml index 9a3dd7dfe..bcd81972e 100644 --- a/core/tauri-runtime/Cargo.toml +++ b/core/tauri-runtime/Cargo.toml @@ -32,6 +32,7 @@ http = "0.2.4" http-range = "0.1.4" infer = "0.7" raw-window-handle = "0.5" +rand = "0.8" [target."cfg(windows)".dependencies] webview2-com = "0.16.0" diff --git a/core/tauri-runtime/src/lib.rs b/core/tauri-runtime/src/lib.rs index 40dba68be..096defcf5 100644 --- a/core/tauri-runtime/src/lib.rs +++ b/core/tauri-runtime/src/lib.rs @@ -34,16 +34,71 @@ use crate::http::{ InvalidUri, }; +#[cfg(all(desktop, feature = "system-tray"))] +use std::fmt; + +pub type TrayId = u16; +pub type TrayEventHandler = dyn Fn(&SystemTrayEvent) + Send + 'static; + #[cfg(all(desktop, feature = "system-tray"))] #[non_exhaustive] -#[derive(Debug, Default)] pub struct SystemTray { + pub id: TrayId, pub icon: Option, pub menu: Option, #[cfg(target_os = "macos")] pub icon_as_template: bool, #[cfg(target_os = "macos")] pub menu_on_left_click: bool, + pub on_event: Option>, +} + +#[cfg(all(desktop, feature = "system-tray"))] +impl fmt::Debug for SystemTray { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("SystemTray"); + d.field("id", &self.id) + .field("icon", &self.icon) + .field("menu", &self.menu); + #[cfg(target_os = "macos")] + { + d.field("icon_as_template", &self.icon_as_template) + .field("menu_on_left_click", &self.menu_on_left_click); + } + d.finish() + } +} + +#[cfg(all(desktop, feature = "system-tray"))] +impl Clone for SystemTray { + fn clone(&self) -> Self { + Self { + id: self.id, + icon: self.icon.clone(), + menu: self.menu.clone(), + on_event: None, + #[cfg(target_os = "macos")] + icon_as_template: self.icon_as_template, + #[cfg(target_os = "macos")] + menu_on_left_click: self.menu_on_left_click, + } + } +} + +#[cfg(all(desktop, feature = "system-tray"))] +impl Default for SystemTray { + fn default() -> Self { + Self { + id: rand::random(), + icon: None, + menu: None, + #[cfg(target_os = "macos")] + icon_as_template: false, + #[cfg(target_os = "macos")] + menu_on_left_click: false, + on_event: None, + } + } } #[cfg(all(desktop, feature = "system-tray"))] @@ -57,6 +112,13 @@ impl SystemTray { self.menu.as_ref() } + /// Sets the tray id. + #[must_use] + pub fn with_id(mut self, id: TrayId) -> Self { + self.id = id; + self + } + /// Sets the tray icon. #[must_use] pub fn with_icon(mut self, icon: Icon) -> Self { @@ -86,6 +148,12 @@ impl SystemTray { self.menu.replace(menu); self } + + #[must_use] + pub fn on_event(mut self, f: F) -> Self { + self.on_event.replace(Box::new(f)); + self + } } /// Type of user attention requested on a window. @@ -261,9 +329,13 @@ pub trait RuntimeHandle: Debug + Clone + Send + Sync + Sized + 'st /// Run a task on the main thread. fn run_on_main_thread(&self, f: F) -> Result<()>; - #[cfg(all(windows, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(all(windows, feature = "system-tray"))))] - fn remove_system_tray(&self) -> Result<()>; + /// Adds an icon to the system tray with the specified menu items. + #[cfg(all(desktop, feature = "system-tray"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "system-tray"))))] + fn system_tray( + &self, + system_tray: SystemTray, + ) -> Result<>::TrayHandler>; fn raw_display_handle(&self) -> RawDisplayHandle; } @@ -348,7 +420,7 @@ pub trait Runtime: Debug + Sized + 'static { /// Registers a system tray event handler. #[cfg(all(desktop, feature = "system-tray"))] #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] - fn on_system_tray_event(&mut self, f: F) -> Uuid; + fn on_system_tray_event(&mut self, f: F); /// Sets the activation policy for the application. It is set to `NSApplicationActivationPolicyRegular` by default. #[cfg(target_os = "macos")] diff --git a/core/tauri-runtime/src/menu.rs b/core/tauri-runtime/src/menu.rs index 181b012e1..0fa219836 100644 --- a/core/tauri-runtime/src/menu.rs +++ b/core/tauri-runtime/src/menu.rs @@ -152,6 +152,7 @@ pub trait TrayHandle: fmt::Debug + Clone + Send + Sync { fn update_item(&self, id: u16, update: MenuUpdate) -> crate::Result<()>; #[cfg(target_os = "macos")] fn set_icon_as_template(&self, is_template: bool) -> crate::Result<()>; + fn destroy(&self) -> crate::Result<()>; } /// A window menu. diff --git a/core/tauri-utils/src/config.rs b/core/tauri-utils/src/config.rs index 08c8fb03e..7810d376b 100644 --- a/core/tauri-utils/src/config.rs +++ b/core/tauri-utils/src/config.rs @@ -2329,9 +2329,7 @@ impl Default for UpdaterConfig { #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct SystemTrayConfig { - /// Path to the icon to use on the system tray. - /// - /// It is forced to be a `.png` file on Linux and macOS, and a `.ico` file on Windows. + /// Path to the default icon to use on the system tray. #[serde(alias = "icon-path")] pub icon_path: PathBuf, /// A Boolean value that determines whether the image represents a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) image on macOS. diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs index 84d840eb6..7084b0924 100644 --- a/core/tauri/src/app.rs +++ b/core/tauri/src/app.rs @@ -47,8 +47,6 @@ use std::{ use crate::runtime::menu::{Menu, MenuId, MenuIdRef}; use crate::runtime::RuntimeHandle; -#[cfg(all(desktop, feature = "system-tray"))] -use crate::runtime::SystemTrayEvent as RuntimeSystemTrayEvent; #[cfg(updater)] use crate::updater; @@ -326,8 +324,6 @@ pub struct AppHandle { global_shortcut_manager: R::GlobalShortcutManager, #[cfg(feature = "clipboard")] clipboard_manager: R::ClipboardManager, - #[cfg(all(desktop, feature = "system-tray"))] - tray_handle: Option>, /// The updater configuration. #[cfg(updater)] pub(crate) updater_settings: UpdaterSettings, @@ -379,8 +375,6 @@ impl Clone for AppHandle { global_shortcut_manager: self.global_shortcut_manager.clone(), #[cfg(feature = "clipboard")] clipboard_manager: self.clipboard_manager.clone(), - #[cfg(all(desktop, feature = "system-tray"))] - tray_handle: self.tray_handle.clone(), #[cfg(updater)] updater_settings: self.updater_settings.clone(), } @@ -403,13 +397,6 @@ impl AppHandle { .map_err(Into::into) } - /// Removes the system tray. - #[cfg(all(windows, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(all(windows, feature = "system-tray"))))] - fn remove_system_tray(&self) -> crate::Result<()> { - self.runtime_handle.remove_system_tray().map_err(Into::into) - } - /// Adds a Tauri application plugin. /// This function can be used to register a plugin that is loaded dynamically e.g. after login. /// For plugins that are created when the app is started, prefer [`Builder::plugin`]. @@ -513,7 +500,9 @@ impl AppHandle { } #[cfg(all(windows, feature = "system-tray"))] { - let _ = self.remove_system_tray(); + for tray in self.manager().trays().values() { + let _ = tray.destroy(); + } } } } @@ -545,8 +534,6 @@ pub struct App { global_shortcut_manager: R::GlobalShortcutManager, #[cfg(feature = "clipboard")] clipboard_manager: R::ClipboardManager, - #[cfg(all(desktop, feature = "system-tray"))] - tray_handle: Option>, handle: AppHandle, } @@ -607,14 +594,72 @@ macro_rules! shared_app_impl { updater::builder(self.app_handle()) } + /// Gets a handle to the first system tray. + /// + /// Prefer [`Self::tray_handle_by_id`] when multiple system trays are created. + /// + /// # Examples + /// ``` + /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let app_handle = app.handle(); + /// SystemTray::new() + /// .with_menu( + /// SystemTrayMenu::new() + /// .add_item(CustomMenuItem::new("quit", "Quit")) + /// .add_item(CustomMenuItem::new("open", "Open")) + /// ) + /// .on_event(move |event| { + /// let tray_handle = app_handle.tray_handle(); + /// }) + /// .build(app)?; + /// Ok(()) + /// }); + /// ``` #[cfg(all(desktop, feature = "system-tray"))] #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] - /// Gets a handle handle to the system tray. pub fn tray_handle(&self) -> tray::SystemTrayHandle { self - .tray_handle - .clone() - .expect("tray not configured; use the `Builder#system_tray` API first.") + .manager() + .trays() + .values() + .next() + .cloned() + .expect("tray not configured; use the `Builder#system_tray`, `App#system_tray` or `AppHandle#system_tray` APIs first.") + } + + + /// Gets a handle to a system tray by its id. + /// + /// ``` + /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let app_handle = app.handle(); + /// let tray_id = "my-tray"; + /// SystemTray::new() + /// .with_id(tray_id) + /// .with_menu( + /// SystemTrayMenu::new() + /// .add_item(CustomMenuItem::new("quit", "Quit")) + /// .add_item(CustomMenuItem::new("open", "Open")) + /// ) + /// .on_event(move |event| { + /// let tray_handle = app_handle.tray_handle_by_id(tray_id).unwrap(); + /// }) + /// .build(app)?; + /// Ok(()) + /// }); + /// ``` + #[cfg(all(desktop, feature = "system-tray"))] + #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] + pub fn tray_handle_by_id(&self, id: &str) -> Option> { + self + .manager() + .get_tray(id) } /// The path resolver for the application. @@ -672,7 +717,7 @@ impl App { /// Sets the activation policy for the application. It is set to `NSApplicationActivationPolicyRegular` by default. /// /// # Examples - /// ```rust,no_run + /// ```,no_run /// let mut app = tauri::Builder::default() /// // on an actual app, remove the string argument /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) @@ -695,7 +740,7 @@ impl App { /// /// # Examples /// - /// ```rust,no_run + /// ``` /// tauri::Builder::default() /// .setup(|app| { /// let matches = app.get_cli_matches()?; @@ -714,7 +759,7 @@ impl App { /// Runs the application. /// /// # Examples - /// ```rust,no_run + /// ```,no_run /// let app = tauri::Builder::default() /// // on an actual app, remove the string argument /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) @@ -752,7 +797,7 @@ impl App { /// Additionally, the cleanup calls [AppHandle#remove_system_tray](`AppHandle#method.remove_system_tray`) (Windows only). /// /// # Examples - /// ```rust,no_run + /// ```no_run /// let mut app = tauri::Builder::default() /// // on an actual app, remove the string argument /// .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) @@ -831,7 +876,7 @@ impl App { /// Builds a Tauri application. /// /// # Examples -/// ```rust,no_run +/// ```,no_run /// tauri::Builder::default() /// // on an actual app, remove the string argument /// .run(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) @@ -941,7 +986,7 @@ impl Builder { /// Defines the JS message handler callback. /// /// # Examples - /// ```rust,no_run + /// ``` /// #[tauri::command] /// fn command_1() -> String { /// return "hello world".to_string(); @@ -980,7 +1025,7 @@ impl Builder { /// Defines the setup hook. /// /// # Examples - /// ```rust,no_run + /// ``` /// use tauri::Manager; /// tauri::Builder::default() /// .setup(|app| { @@ -1076,7 +1121,7 @@ impl Builder { /// /// Since the managed state is global and must be [`Send`] + [`Sync`], mutations can only happen through interior mutability: /// - /// ```rust,no_run + /// ```,no_run /// use std::{collections::HashMap, sync::Mutex}; /// use tauri::State; /// // here we use Mutex to achieve interior mutability @@ -1111,7 +1156,7 @@ impl Builder { /// /// # Examples /// - /// ```rust,no_run + /// ```,no_run /// use tauri::State; /// /// struct MyInt(isize); @@ -1149,7 +1194,21 @@ impl Builder { self } - /// Adds the icon configured on `tauri.conf.json` to the system tray with the specified menu items. + /// Sets the given system tray to be built before the app runs. + /// + /// Prefer the [`SystemTray#method.build`] method to create the tray at runtime instead. + /// + /// # Examples + /// ``` + /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu}; + /// + /// tauri::Builder::default() + /// .system_tray(SystemTray::new().with_menu( + /// SystemTrayMenu::new() + /// .add_item(CustomMenuItem::new("quit", "Quit")) + /// .add_item(CustomMenuItem::new("open", "Open")) + /// )); + /// ``` #[cfg(all(desktop, feature = "system-tray"))] #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] #[must_use] @@ -1161,7 +1220,7 @@ impl Builder { /// Sets the menu to use on all windows. /// /// # Examples - /// ```rust,no_run + /// ``` /// use tauri::{MenuEntry, Submenu, MenuItem, Menu, CustomMenuItem}; /// /// tauri::Builder::default() @@ -1185,7 +1244,7 @@ impl Builder { /// Registers a menu event handler for all windows. /// /// # Examples - /// ```rust,no_run + /// ``` /// use tauri::{Menu, MenuEntry, Submenu, CustomMenuItem, api, Manager}; /// tauri::Builder::default() /// .menu(Menu::with_items([ @@ -1225,7 +1284,7 @@ impl Builder { /// Registers a window event handler for all windows. /// /// # Examples - /// ```rust,no_run + /// ``` /// tauri::Builder::default() /// .on_window_event(|event| match event.event() { /// tauri::WindowEvent::Focused(focused) => { @@ -1248,13 +1307,15 @@ impl Builder { /// Registers a system tray event handler. /// + /// Prefer the [`SystemTray#method.on_event`] method when creating a tray at runtime instead. + /// /// # Examples - /// ```rust,no_run - /// use tauri::Manager; + /// ``` + /// use tauri::{Manager, SystemTrayEvent}; /// tauri::Builder::default() /// .on_system_tray_event(|app, event| match event { /// // show window with id "main" when the tray is left clicked - /// tauri::SystemTrayEvent::LeftClick { .. } => { + /// SystemTrayEvent::LeftClick { .. } => { /// let window = app.get_window("main").unwrap(); /// window.show().unwrap(); /// window.set_focus().unwrap(); @@ -1313,7 +1374,7 @@ impl Builder { /// /// - Use a macOS Universal binary target name: /// - /// ```no_run + /// ``` /// let mut builder = tauri::Builder::default(); /// #[cfg(target_os = "macos")] /// { @@ -1323,7 +1384,7 @@ impl Builder { /// /// - Append debug information to the target: /// - /// ```no_run + /// ``` /// let kind = if cfg!(debug_assertions) { "debug" } else { "release" }; /// tauri::Builder::default() /// .updater_target(format!("{}-{}", tauri::updater::target().unwrap(), kind)); @@ -1331,7 +1392,7 @@ impl Builder { /// /// - Use the platform's target triple: /// - /// ```no_run + /// ``` /// tauri::Builder::default() /// .updater_target(tauri::utils::platform::target_triple().unwrap()); /// ``` @@ -1349,18 +1410,6 @@ impl Builder { self.menu = Some(Menu::os_default(&context.package_info().name)); } - #[cfg(all(desktop, feature = "system-tray"))] - let system_tray_icon = context.system_tray_icon.clone(); - - #[cfg(all(feature = "system-tray", target_os = "macos"))] - let (system_tray_icon_as_template, system_tray_menu_on_left_click) = context - .config - .tauri - .system_tray - .as_ref() - .map(|t| (t.icon_as_template, t.menu_on_left_click)) - .unwrap_or_default(); - #[cfg(shell_scope)] let shell_scope = context.shell_scope.clone(); @@ -1418,8 +1467,6 @@ impl Builder { global_shortcut_manager: global_shortcut_manager.clone(), #[cfg(feature = "clipboard")] clipboard_manager: clipboard_manager.clone(), - #[cfg(all(desktop, feature = "system-tray"))] - tray_handle: None, handle: AppHandle { runtime_handle, manager, @@ -1427,8 +1474,6 @@ impl Builder { global_shortcut_manager, #[cfg(feature = "clipboard")] clipboard_manager, - #[cfg(all(desktop, feature = "system-tray"))] - tray_handle: None, #[cfg(updater)] updater_settings: self.updater_settings, }, @@ -1481,77 +1526,25 @@ impl Builder { } #[cfg(all(desktop, feature = "system-tray"))] - if let Some(system_tray) = self.system_tray { - let mut ids = HashMap::new(); - if let Some(menu) = system_tray.menu() { - tray::get_menu_ids(&mut ids, menu); + { + if let Some(tray) = self.system_tray { + tray.build(&app)?; } - let tray_icon = if let Some(icon) = system_tray.icon { - Some(icon) - } else if let Some(tray_icon) = system_tray_icon { - Some(tray_icon.try_into()?) - } else { - None - }; - let mut tray = tray::SystemTray::new() - .with_icon(tray_icon.expect("tray icon not found; please configure it on tauri.conf.json")); - if let Some(menu) = system_tray.menu { - tray = tray.with_menu(menu); - } - #[cfg(target_os = "macos")] - let tray = tray - .with_icon_as_template(system_tray_icon_as_template) - .with_menu_on_left_click(system_tray_menu_on_left_click); - let tray_handler = app - .runtime - .as_ref() - .unwrap() - .system_tray(tray.into()) - .expect("failed to run tray"); - - let tray_handle = tray::SystemTrayHandle { - ids: Arc::new(std::sync::Mutex::new(ids)), - inner: tray_handler, - }; - let ids = tray_handle.ids.clone(); - app.tray_handle.replace(tray_handle.clone()); - app.handle.tray_handle.replace(tray_handle); for listener in self.system_tray_event_listeners { let app_handle = app.handle(); - let ids = ids.clone(); let listener = Arc::new(std::sync::Mutex::new(listener)); app .runtime .as_mut() .unwrap() - .on_system_tray_event(move |event| { - let app_handle = app_handle.clone(); - let event = match event { - RuntimeSystemTrayEvent::MenuItemClick(id) => tray::SystemTrayEvent::MenuItemClick { - id: ids.lock().unwrap().get(id).unwrap().clone(), - }, - RuntimeSystemTrayEvent::LeftClick { position, size } => { - tray::SystemTrayEvent::LeftClick { - position: *position, - size: *size, - } - } - RuntimeSystemTrayEvent::RightClick { position, size } => { - tray::SystemTrayEvent::RightClick { - position: *position, - size: *size, - } - } - RuntimeSystemTrayEvent::DoubleClick { position, size } => { - tray::SystemTrayEvent::DoubleClick { - position: *position, - size: *size, - } - } - }; - let listener = listener.clone(); - listener.lock().unwrap()(&app_handle, event); + .on_system_tray_event(move |tray_id, event| { + if let Some((tray_id, tray)) = app_handle.manager().get_tray_by_runtime_id(tray_id) { + let app_handle = app_handle.clone(); + let event = tray::SystemTrayEvent::from_runtime_event(event, tray_id, &tray.ids); + let listener = listener.clone(); + listener.lock().unwrap()(&app_handle, event); + } }); } } diff --git a/core/tauri/src/app/tray.rs b/core/tauri/src/app/tray.rs index c460cf899..d175eb8d8 100644 --- a/core/tauri/src/app/tray.rs +++ b/core/tauri/src/app/tray.rs @@ -8,18 +8,26 @@ pub use crate::{ MenuHash, MenuId, MenuIdRef, MenuUpdate, SystemTrayMenu, SystemTrayMenuEntry, TrayHandle, }, window::dpi::{PhysicalPosition, PhysicalSize}, + RuntimeHandle, SystemTrayEvent as RuntimeSystemTrayEvent, }, Icon, Runtime, }; +use crate::{sealed::RuntimeOrDispatch, Manager}; +use rand::distributions::{Alphanumeric, DistString}; use tauri_macros::default_runtime; +use tauri_runtime::TrayId; use tauri_utils::debug_eprintln; use std::{ - collections::HashMap, + collections::{hash_map::DefaultHasher, HashMap}, + fmt, + hash::{Hash, Hasher}, sync::{Arc, Mutex}, }; +type TrayEventHandler = dyn Fn(SystemTrayEvent) + Send + Sync + 'static; + pub(crate) fn get_menu_ids(map: &mut HashMap, menu: &SystemTrayMenu) { for item in &menu.items { match item { @@ -33,9 +41,11 @@ pub(crate) fn get_menu_ids(map: &mut HashMap, menu: &SystemTra } /// Represents a System Tray instance. -#[derive(Debug, Default)] +#[derive(Clone)] #[non_exhaustive] pub struct SystemTray { + /// The tray identifier. Defaults to a random string. + pub id: String, /// The tray icon. pub icon: Option, /// The tray menu. @@ -46,10 +56,62 @@ pub struct SystemTray { /// Whether the menu should appear when the tray receives a left click. Defaults to `true` #[cfg(target_os = "macos")] pub menu_on_left_click: bool, + on_event: Option>, + // TODO: icon_as_template and menu_on_left_click should be an Option instead :( + #[cfg(target_os = "macos")] + menu_on_left_click_set: bool, + #[cfg(target_os = "macos")] + icon_as_template_set: bool, +} + +impl fmt::Debug for SystemTray { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("SystemTray"); + d.field("id", &self.id) + .field("icon", &self.icon) + .field("menu", &self.menu); + #[cfg(target_os = "macos")] + { + d.field("icon_as_template", &self.icon_as_template) + .field("menu_on_left_click", &self.menu_on_left_click); + } + d.finish() + } +} + +impl Default for SystemTray { + fn default() -> Self { + Self { + id: Alphanumeric.sample_string(&mut rand::thread_rng(), 16), + icon: None, + menu: None, + on_event: None, + #[cfg(target_os = "macos")] + icon_as_template: false, + #[cfg(target_os = "macos")] + menu_on_left_click: false, + #[cfg(target_os = "macos")] + icon_as_template_set: false, + #[cfg(target_os = "macos")] + menu_on_left_click_set: false, + } + } } impl SystemTray { /// Creates a new system tray that only renders an icon. + /// + /// # Examples + /// + /// ``` + /// use tauri::SystemTray; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let tray_handle = SystemTray::new().build(app)?; + /// Ok(()) + /// }); + /// ``` pub fn new() -> Self { Default::default() } @@ -58,7 +120,43 @@ impl SystemTray { self.menu.as_ref() } - /// Sets the tray icon. + /// Sets the tray identifier, used to retrieve its handle and to identify a tray event source. + /// + /// # Examples + /// + /// ``` + /// use tauri::SystemTray; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let tray_handle = SystemTray::new() + /// .with_id("tray-id") + /// .build(app)?; + /// Ok(()) + /// }); + /// ``` + #[must_use] + pub fn with_id>(mut self, id: I) -> Self { + self.id = id.into(); + self + } + + /// Sets the tray [`Icon`]. + /// + /// # Examples + /// + /// ``` + /// use tauri::{Icon, SystemTray}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let tray_handle = SystemTray::new() + /// // dummy and invalid Rgba icon; see the Icon documentation for more information + /// .with_icon(Icon::Rgba { rgba: Vec::new(), width: 0, height: 0 }) + /// .build(app)?; + /// Ok(()) + /// }); + /// ``` #[must_use] pub fn with_icon>(mut self, icon: I) -> Self where @@ -79,50 +177,230 @@ impl SystemTray { /// /// Images you mark as template images should consist of only black and clear colors. /// You can use the alpha channel in the image to adjust the opacity of black content. + /// + /// # Examples + /// + /// ``` + /// use tauri::SystemTray; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut tray_builder = SystemTray::new(); + /// #[cfg(target_os = "macos")] + /// { + /// tray_builder = tray_builder.with_icon_as_template(true); + /// } + /// let tray_handle = tray_builder.build(app)?; + /// Ok(()) + /// }); + /// ``` #[cfg(target_os = "macos")] #[must_use] pub fn with_icon_as_template(mut self, is_template: bool) -> Self { + self.icon_as_template_set = true; self.icon_as_template = is_template; self } /// Sets whether the menu should appear when the tray receives a left click. Defaults to `true`. + /// + /// # Examples + /// + /// ``` + /// use tauri::SystemTray; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let mut tray_builder = SystemTray::new(); + /// #[cfg(target_os = "macos")] + /// { + /// tray_builder = tray_builder.with_menu_on_left_click(false); + /// } + /// let tray_handle = tray_builder.build(app)?; + /// Ok(()) + /// }); + /// ``` #[cfg(target_os = "macos")] #[must_use] pub fn with_menu_on_left_click(mut self, menu_on_left_click: bool) -> Self { + self.menu_on_left_click_set = true; self.menu_on_left_click = menu_on_left_click; self } + /// Sets the event listener for this system tray. + /// + /// # Examples + /// + /// ``` + /// use tauri::{Icon, Manager, SystemTray, SystemTrayEvent}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let handle = app.handle(); + /// let id = "tray-id"; + /// SystemTray::new() + /// .with_id(id) + /// .on_event(move |event| { + /// let tray_handle = handle.tray_handle_by_id(id).unwrap(); + /// match event { + /// // show window with id "main" when the tray is left clicked + /// SystemTrayEvent::LeftClick { .. } => { + /// let window = handle.get_window("main").unwrap(); + /// window.show().unwrap(); + /// window.set_focus().unwrap(); + /// } + /// _ => {} + /// } + /// }) + /// .build(app)?; + /// Ok(()) + /// }); + /// ``` + #[must_use] + pub fn on_event(mut self, f: F) -> Self { + self.on_event.replace(Arc::new(f)); + self + } + /// Sets the menu to show when the system tray is right clicked. + /// + /// # Examples + /// + /// ``` + /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let tray_handle = SystemTray::new() + /// .with_menu( + /// SystemTrayMenu::new() + /// .add_item(CustomMenuItem::new("quit", "Quit")) + /// .add_item(CustomMenuItem::new("open", "Open")) + /// ) + /// .build(app)?; + /// Ok(()) + /// }); + /// ``` #[must_use] pub fn with_menu(mut self, menu: SystemTrayMenu) -> Self { self.menu.replace(menu); self } -} -impl From for tauri_runtime::SystemTray { - fn from(tray: SystemTray) -> Self { - let mut t = tauri_runtime::SystemTray::new(); - if let Some(i) = tray.icon { - t = t.with_icon(i); + /// Builds and shows the system tray. + /// + /// # Examples + /// + /// ``` + /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu}; + /// + /// tauri::Builder::default() + /// .setup(|app| { + /// let tray_handle = SystemTray::new() + /// .with_menu( + /// SystemTrayMenu::new() + /// .add_item(CustomMenuItem::new("quit", "Quit")) + /// .add_item(CustomMenuItem::new("open", "Open")) + /// ) + /// .build(app)?; + /// + /// tray_handle.get_item("quit").set_enabled(false); + /// Ok(()) + /// }); + /// ``` + pub fn build>( + mut self, + manager: &M, + ) -> crate::Result> { + let mut ids = HashMap::new(); + if let Some(menu) = self.menu() { + get_menu_ids(&mut ids, menu); + } + let ids = Arc::new(Mutex::new(ids)); + + if self.icon.is_none() { + if let Some(tray_icon) = &manager.manager().inner.tray_icon { + self = self.with_icon(tray_icon.clone()); + } + } + #[cfg(target_os = "macos")] + { + if !self.icon_as_template_set { + self.icon_as_template = manager + .config() + .tauri + .system_tray + .as_ref() + .map_or(false, |t| t.icon_as_template); + } + if !self.menu_on_left_click_set { + self.menu_on_left_click = manager + .config() + .tauri + .system_tray + .as_ref() + .map_or(false, |t| t.menu_on_left_click); + } } - if let Some(menu) = tray.menu { - t = t.with_menu(menu); + let tray_id = self.id.clone(); + + let mut runtime_tray = tauri_runtime::SystemTray::new(); + runtime_tray = runtime_tray.with_id(hash(&self.id)); + if let Some(i) = self.icon { + runtime_tray = runtime_tray.with_icon(i); + } + + if let Some(menu) = self.menu { + runtime_tray = runtime_tray.with_menu(menu); + } + + if let Some(on_event) = self.on_event { + let ids_ = ids.clone(); + let tray_id_ = tray_id.clone(); + runtime_tray = runtime_tray.on_event(move |event| { + on_event(SystemTrayEvent::from_runtime_event( + event, + tray_id_.clone(), + &ids_, + )) + }); } #[cfg(target_os = "macos")] { - t = t.with_icon_as_template(tray.icon_as_template); - t = t.with_menu_on_left_click(tray.menu_on_left_click); + runtime_tray = runtime_tray.with_icon_as_template(self.icon_as_template); + runtime_tray = runtime_tray.with_menu_on_left_click(self.menu_on_left_click); } - t + let id = runtime_tray.id; + let tray_handler = match manager.runtime() { + RuntimeOrDispatch::Runtime(r) => r.system_tray(runtime_tray), + RuntimeOrDispatch::RuntimeHandle(h) => h.system_tray(runtime_tray), + RuntimeOrDispatch::Dispatch(_) => manager + .app_handle() + .runtime_handle + .system_tray(runtime_tray), + }?; + + let tray_handle = SystemTrayHandle { + id, + ids, + inner: tray_handler, + }; + manager.manager().attach_tray(tray_id, tray_handle.clone()); + + Ok(tray_handle) } } +fn hash(id: &str) -> MenuHash { + let mut hasher = DefaultHasher::new(); + id.hash(&mut hasher); + hasher.finish() as MenuHash +} + /// System tray event. #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] #[non_exhaustive] @@ -130,6 +408,8 @@ pub enum SystemTrayEvent { /// Tray context menu item was clicked. #[non_exhaustive] MenuItemClick { + /// The tray id. + tray_id: String, /// The id of the menu item. id: MenuId, }, @@ -140,6 +420,8 @@ pub enum SystemTrayEvent { /// - **Linux:** Unsupported #[non_exhaustive] LeftClick { + /// The tray id. + tray_id: String, /// The position of the tray icon. position: PhysicalPosition, /// The size of the tray icon. @@ -153,6 +435,8 @@ pub enum SystemTrayEvent { /// - **macOS:** `Ctrl` + `Left click` fire this event. #[non_exhaustive] RightClick { + /// The tray id. + tray_id: String, /// The position of the tray icon. position: PhysicalPosition, /// The size of the tray icon. @@ -166,6 +450,8 @@ pub enum SystemTrayEvent { /// #[non_exhaustive] DoubleClick { + /// The tray id. + tray_id: String, /// The position of the tray icon. position: PhysicalPosition, /// The size of the tray icon. @@ -173,10 +459,41 @@ pub enum SystemTrayEvent { }, } +impl SystemTrayEvent { + pub(crate) fn from_runtime_event( + event: &RuntimeSystemTrayEvent, + tray_id: String, + menu_ids: &Arc>>, + ) -> Self { + match event { + RuntimeSystemTrayEvent::MenuItemClick(id) => Self::MenuItemClick { + tray_id, + id: menu_ids.lock().unwrap().get(id).unwrap().clone(), + }, + RuntimeSystemTrayEvent::LeftClick { position, size } => Self::LeftClick { + tray_id, + position: *position, + size: *size, + }, + RuntimeSystemTrayEvent::RightClick { position, size } => Self::RightClick { + tray_id, + position: *position, + size: *size, + }, + RuntimeSystemTrayEvent::DoubleClick { position, size } => Self::DoubleClick { + tray_id, + position: *position, + size: *size, + }, + } + } +} + /// A handle to a system tray. Allows updating the context menu items. #[default_runtime(crate::Wry, wry)] #[derive(Debug)] pub struct SystemTrayHandle { + pub(crate) id: TrayId, pub(crate) ids: Arc>>, pub(crate) inner: R::TrayHandler, } @@ -184,6 +501,7 @@ pub struct SystemTrayHandle { impl Clone for SystemTrayHandle { fn clone(&self) -> Self { Self { + id: self.id, ids: self.ids.clone(), inner: self.inner.clone(), } @@ -245,6 +563,11 @@ impl SystemTrayHandle { .set_icon_as_template(is_template) .map_err(Into::into) } + + /// Destroys this system tray. + pub fn destroy(&self) -> crate::Result<()> { + self.inner.destroy().map_err(Into::into) + } } impl SystemTrayMenuItemHandle { diff --git a/core/tauri/src/manager.rs b/core/tauri/src/manager.rs index 34846714b..673fb3c6b 100644 --- a/core/tauri/src/manager.rs +++ b/core/tauri/src/manager.rs @@ -196,6 +196,8 @@ fn replace_csp_nonce( #[default_runtime(crate::Wry, wry)] pub struct InnerWindowManager { windows: Mutex>>, + #[cfg(all(desktop, feature = "system-tray"))] + pub(crate) trays: Mutex>>, pub(crate) plugins: Mutex>, listeners: Listeners, pub(crate) state: Arc, @@ -210,6 +212,7 @@ pub struct InnerWindowManager { assets: Arc, pub(crate) default_window_icon: Option, pub(crate) app_icon: Option>, + pub(crate) tray_icon: Option, package_info: PackageInfo, /// The webview protocols protocols available to all windows. @@ -236,6 +239,7 @@ impl fmt::Debug for InnerWindowManager { .field("config", &self.config) .field("default_window_icon", &self.default_window_icon) .field("app_icon", &self.app_icon) + .field("tray_icon", &self.tray_icon) .field("package_info", &self.package_info) .field("menu", &self.menu) .field("pattern", &self.pattern) @@ -300,6 +304,8 @@ impl WindowManager { Self { inner: Arc::new(InnerWindowManager { windows: Mutex::default(), + #[cfg(all(desktop, feature = "system-tray"))] + trays: Default::default(), plugins: Mutex::new(plugins), listeners: Listeners::default(), state: Arc::new(state), @@ -309,6 +315,7 @@ impl WindowManager { assets: context.assets, default_window_icon: context.default_window_icon, app_icon: context.app_icon, + tray_icon: context.system_tray_icon, package_info: context.package_info, pattern: context.pattern, uri_scheme_protocols, @@ -1289,6 +1296,33 @@ impl WindowManager { } } +/// Tray APIs +#[cfg(all(desktop, feature = "system-tray"))] +impl WindowManager { + pub fn get_tray(&self, id: &str) -> Option> { + self.inner.trays.lock().unwrap().get(id).cloned() + } + + pub fn trays(&self) -> HashMap> { + self.inner.trays.lock().unwrap().clone() + } + + pub fn attach_tray(&self, id: String, tray: crate::SystemTrayHandle) { + self.inner.trays.lock().unwrap().insert(id, tray); + } + + pub fn get_tray_by_runtime_id(&self, id: u16) -> Option<(String, crate::SystemTrayHandle)> { + let trays = self.inner.trays.lock().unwrap(); + let iter = trays.iter(); + for (tray_id, tray) in iter { + if tray.id == id { + return Some((tray_id.clone(), tray.clone())); + } + } + None + } +} + fn on_window_event( window: &Window, manager: &WindowManager, diff --git a/core/tauri/src/test/mock_runtime.rs b/core/tauri/src/test/mock_runtime.rs index 26ccbbf81..de43dcd7d 100644 --- a/core/tauri/src/test/mock_runtime.rs +++ b/core/tauri/src/test/mock_runtime.rs @@ -18,7 +18,7 @@ use tauri_runtime::{ #[cfg(all(desktop, feature = "system-tray"))] use tauri_runtime::{ menu::{SystemTrayMenu, TrayHandle}, - SystemTray, SystemTrayEvent, + SystemTray, SystemTrayEvent, TrayId, }; use tauri_utils::{config::WindowConfig, Theme}; use uuid::Uuid; @@ -80,10 +80,13 @@ impl RuntimeHandle for MockRuntimeHandle { unimplemented!() } - #[cfg(all(windows, feature = "system-tray"))] - #[cfg_attr(doc_cfg, doc(cfg(all(windows, feature = "system-tray"))))] - fn remove_system_tray(&self) -> Result<()> { - Ok(()) + #[cfg(all(desktop, feature = "system-tray"))] + #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "system-tray"))))] + fn system_tray( + &self, + system_tray: SystemTray, + ) -> Result<>::TrayHandler> { + unimplemented!() } fn raw_display_handle(&self) -> raw_window_handle::RawDisplayHandle { @@ -531,6 +534,10 @@ impl TrayHandle for MockTrayHandler { fn set_icon_as_template(&self, is_template: bool) -> Result<()> { Ok(()) } + + fn destroy(&self) -> Result<()> { + Ok(()) + } } #[derive(Debug, Clone)] @@ -636,9 +643,7 @@ impl Runtime for MockRuntime { #[cfg(all(desktop, feature = "system-tray"))] #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))] - fn on_system_tray_event(&mut self, f: F) -> Uuid { - Uuid::new_v4() - } + fn on_system_tray_event(&mut self, f: F) {} #[cfg(target_os = "macos")] #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))] diff --git a/examples/api/src-tauri/Cargo.lock b/examples/api/src-tauri/Cargo.lock index dee9803f6..71bf38485 100644 --- a/examples/api/src-tauri/Cargo.lock +++ b/examples/api/src-tauri/Cargo.lock @@ -3037,9 +3037,9 @@ dependencies = [ [[package]] name = "tao" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6901eece433f5ce79a60c2660e204d8b2ff4f76668db9aedb8ae6b2c5a57ae43" +checksum = "8ad691ca9fca6c2c76c09ffcddf6ae6593fba65d95477cf31780910ed272f5b8" dependencies = [ "bitflags", "cairo-rs", @@ -3217,6 +3217,7 @@ dependencies = [ "http", "http-range", "infer 0.7.0", + "rand 0.8.5", "raw-window-handle", "serde", "serde_json", diff --git a/examples/api/src-tauri/src/main.rs b/examples/api/src-tauri/src/main.rs index 44f2e8ef2..f14cb16a8 100644 --- a/examples/api/src-tauri/src/main.rs +++ b/examples/api/src-tauri/src/main.rs @@ -31,7 +31,10 @@ struct Reply { fn main() { #[allow(unused_mut)] let mut builder = tauri::Builder::default() - .setup(|app| { + .setup(move |app| { + #[cfg(desktop)] + create_tray(app)?; + #[allow(unused_mut)] let mut window_builder = WindowBuilder::new(app, "main", WindowUrl::default()) .title("Tauri API Validation") @@ -100,99 +103,6 @@ fn main() { builder = builder.menu(tauri::Menu::os_default("Tauri API Validation")); } - #[cfg(desktop)] - { - let tray_menu1 = SystemTrayMenu::new() - .add_item(CustomMenuItem::new("toggle", "Toggle")) - .add_item(CustomMenuItem::new("new", "New window")) - .add_item(CustomMenuItem::new("icon_1", "Tray Icon 1")) - .add_item(CustomMenuItem::new("icon_2", "Tray Icon 2")) - .add_item(CustomMenuItem::new("switch_menu", "Switch Menu")) - .add_item(CustomMenuItem::new("exit_app", "Quit")); - let tray_menu2 = SystemTrayMenu::new() - .add_item(CustomMenuItem::new("toggle", "Toggle")) - .add_item(CustomMenuItem::new("new", "New window")) - .add_item(CustomMenuItem::new("switch_menu", "Switch Menu")) - .add_item(CustomMenuItem::new("exit_app", "Quit")); - let is_menu1 = AtomicBool::new(true); - - builder = builder - .system_tray(SystemTray::new().with_menu(tray_menu1.clone())) - .on_system_tray_event(move |app, event| match event { - SystemTrayEvent::LeftClick { - position: _, - size: _, - .. - } => { - let window = app.get_window("main").unwrap(); - window.show().unwrap(); - window.set_focus().unwrap(); - } - SystemTrayEvent::MenuItemClick { id, .. } => { - let item_handle = app.tray_handle().get_item(&id); - match id.as_str() { - "exit_app" => { - // exit the app - app.exit(0); - } - "toggle" => { - let window = app.get_window("main").unwrap(); - let new_title = if window.is_visible().unwrap() { - window.hide().unwrap(); - "Show" - } else { - window.show().unwrap(); - "Hide" - }; - item_handle.set_title(new_title).unwrap(); - } - "new" => { - WindowBuilder::new(app, "new", WindowUrl::App("index.html".into())) - .title("Tauri") - .build() - .unwrap(); - } - "icon_1" => { - #[cfg(target_os = "macos")] - app.tray_handle().set_icon_as_template(true).unwrap(); - - app - .tray_handle() - .set_icon(tauri::Icon::Raw( - include_bytes!("../../../.icons/tray_icon_with_transparency.png").to_vec(), - )) - .unwrap(); - } - "icon_2" => { - #[cfg(target_os = "macos")] - app.tray_handle().set_icon_as_template(true).unwrap(); - - app - .tray_handle() - .set_icon(tauri::Icon::Raw( - include_bytes!("../../../.icons/icon.ico").to_vec(), - )) - .unwrap(); - } - "switch_menu" => { - let flag = is_menu1.load(Ordering::Relaxed); - app - .tray_handle() - .set_menu(if flag { - tray_menu2.clone() - } else { - tray_menu1.clone() - }) - .unwrap(); - is_menu1.store(!flag, Ordering::Relaxed); - } - _ => {} - } - } - _ => {} - }); - } - #[allow(unused_mut)] let mut app = builder .invoke_handler(tauri::generate_handler![ @@ -259,3 +169,106 @@ fn main() { _ => {} }) } + +#[cfg(desktop)] +fn create_tray(app: &tauri::App) -> tauri::Result<()> { + let tray_menu1 = SystemTrayMenu::new() + .add_item(CustomMenuItem::new("toggle", "Toggle")) + .add_item(CustomMenuItem::new("new", "New window")) + .add_item(CustomMenuItem::new("icon_1", "Tray Icon 1")) + .add_item(CustomMenuItem::new("icon_2", "Tray Icon 2")) + .add_item(CustomMenuItem::new("switch_menu", "Switch Menu")) + .add_item(CustomMenuItem::new("exit_app", "Quit")) + .add_item(CustomMenuItem::new("destroy", "Destroy")); + let tray_menu2 = SystemTrayMenu::new() + .add_item(CustomMenuItem::new("toggle", "Toggle")) + .add_item(CustomMenuItem::new("new", "New window")) + .add_item(CustomMenuItem::new("switch_menu", "Switch Menu")) + .add_item(CustomMenuItem::new("exit_app", "Quit")) + .add_item(CustomMenuItem::new("destroy", "Destroy")); + let is_menu1 = AtomicBool::new(true); + + let handle = app.handle(); + let tray_id = "my-tray".to_string(); + SystemTray::new() + .with_id(&tray_id) + .with_menu(tray_menu1.clone()) + .on_event(move |event| { + let tray_handle = handle.tray_handle_by_id(&tray_id).unwrap(); + match event { + SystemTrayEvent::LeftClick { + position: _, + size: _, + .. + } => { + let window = handle.get_window("main").unwrap(); + window.show().unwrap(); + window.set_focus().unwrap(); + } + SystemTrayEvent::MenuItemClick { id, .. } => { + let item_handle = tray_handle.get_item(&id); + match id.as_str() { + "exit_app" => { + // exit the app + handle.exit(0); + } + "destroy" => { + tray_handle.destroy().unwrap(); + } + "toggle" => { + let window = handle.get_window("main").unwrap(); + let new_title = if window.is_visible().unwrap() { + window.hide().unwrap(); + "Show" + } else { + window.show().unwrap(); + "Hide" + }; + item_handle.set_title(new_title).unwrap(); + } + "new" => { + WindowBuilder::new(&handle, "new", WindowUrl::App("index.html".into())) + .title("Tauri") + .build() + .unwrap(); + } + "icon_1" => { + #[cfg(target_os = "macos")] + tray_handle.set_icon_as_template(true).unwrap(); + + tray_handle + .set_icon(tauri::Icon::Raw( + include_bytes!("../../../.icons/tray_icon_with_transparency.png").to_vec(), + )) + .unwrap(); + } + "icon_2" => { + #[cfg(target_os = "macos")] + tray_handle.set_icon_as_template(true).unwrap(); + + tray_handle + .set_icon(tauri::Icon::Raw( + include_bytes!("../../../.icons/icon.ico").to_vec(), + )) + .unwrap(); + } + "switch_menu" => { + let flag = is_menu1.load(Ordering::Relaxed); + tray_handle + .set_menu(if flag { + tray_menu2.clone() + } else { + tray_menu1.clone() + }) + .unwrap(); + is_menu1.store(!flag, Ordering::Relaxed); + } + _ => {} + } + } + _ => {} + } + }) + .build(app) + .map(|_| ()) +}