pub use crate::{
runtime::{
menu::{
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::{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<MenuHash, MenuId>, menu: &SystemTrayMenu) {
for item in &menu.items {
match item {
SystemTrayMenuEntry::CustomItem(c) => {
map.insert(c.id, c.id_str.clone());
}
SystemTrayMenuEntry::Submenu(s) => get_menu_ids(map, &s.inner),
_ => {}
}
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct SystemTray {
pub id: String,
pub icon: Option<tauri_runtime::Icon>,
pub menu: Option<SystemTrayMenu>,
#[cfg(target_os = "macos")]
pub icon_as_template: bool,
#[cfg(target_os = "macos")]
pub menu_on_left_click: bool,
on_event: Option<Arc<TrayEventHandler>>,
#[cfg(target_os = "macos")]
menu_on_left_click_set: bool,
#[cfg(target_os = "macos")]
icon_as_template_set: bool,
#[cfg(target_os = "macos")]
title: Option<String>,
tooltip: Option<String>,
}
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,
#[cfg(target_os = "macos")]
title: None,
tooltip: None,
}
}
}
impl SystemTray {
pub fn new() -> Self {
Default::default()
}
pub(crate) fn menu(&self) -> Option<&SystemTrayMenu> {
self.menu.as_ref()
}
#[must_use]
pub fn with_id<I: Into<String>>(mut self, id: I) -> Self {
self.id = id.into();
self
}
#[must_use]
pub fn with_icon<I: TryInto<tauri_runtime::Icon>>(mut self, icon: I) -> Self
where
I::Error: std::error::Error,
{
match icon.try_into() {
Ok(icon) => {
self.icon.replace(icon);
}
Err(e) => {
debug_eprintln!("Failed to load tray icon: {}", e);
}
}
self
}
#[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
}
#[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
}
#[cfg(target_os = "macos")]
#[must_use]
pub fn with_title(mut self, title: &str) -> Self {
self.title = Some(title.to_owned());
self
}
#[must_use]
pub fn with_tooltip(mut self, tooltip: &str) -> Self {
self.tooltip = Some(tooltip.to_owned());
self
}
#[must_use]
pub fn on_event<F: Fn(SystemTrayEvent) + Send + Sync + 'static>(mut self, f: F) -> Self {
self.on_event.replace(Arc::new(f));
self
}
#[must_use]
pub fn with_menu(mut self, menu: SystemTrayMenu) -> Self {
self.menu.replace(menu);
self
}
pub fn build<R: Runtime, M: Manager<R>>(
mut self,
manager: &M,
) -> crate::Result<SystemTrayHandle<R>> {
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 self.title.is_none() {
self.title = manager
.config()
.tauri
.system_tray
.as_ref()
.and_then(|t| t.title.clone())
}
}
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")]
{
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);
if let Some(title) = self.title {
runtime_tray = runtime_tray.with_title(&title);
}
}
if let Some(tooltip) = self.tooltip {
runtime_tray = runtime_tray.with_tooltip(&tooltip);
}
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
}
#[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
#[non_exhaustive]
pub enum SystemTrayEvent {
#[non_exhaustive]
MenuItemClick {
tray_id: String,
id: MenuId,
},
#[non_exhaustive]
LeftClick {
tray_id: String,
position: PhysicalPosition<f64>,
size: PhysicalSize<f64>,
},
#[non_exhaustive]
RightClick {
tray_id: String,
position: PhysicalPosition<f64>,
size: PhysicalSize<f64>,
},
#[non_exhaustive]
DoubleClick {
tray_id: String,
position: PhysicalPosition<f64>,
size: PhysicalSize<f64>,
},
}
impl SystemTrayEvent {
pub(crate) fn from_runtime_event(
event: &RuntimeSystemTrayEvent,
tray_id: String,
menu_ids: &Arc<Mutex<HashMap<u16, String>>>,
) -> 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,
},
}
}
}
#[default_runtime(crate::Wry, wry)]
#[derive(Debug)]
pub struct SystemTrayHandle<R: Runtime> {
pub(crate) id: TrayId,
pub(crate) ids: Arc<Mutex<HashMap<MenuHash, MenuId>>>,
pub(crate) inner: R::TrayHandler,
}
impl<R: Runtime> Clone for SystemTrayHandle<R> {
fn clone(&self) -> Self {
Self {
id: self.id,
ids: self.ids.clone(),
inner: self.inner.clone(),
}
}
}
#[default_runtime(crate::Wry, wry)]
#[derive(Debug)]
pub struct SystemTrayMenuItemHandle<R: Runtime> {
id: MenuHash,
tray_handler: R::TrayHandler,
}
impl<R: Runtime> Clone for SystemTrayMenuItemHandle<R> {
fn clone(&self) -> Self {
Self {
id: self.id,
tray_handler: self.tray_handler.clone(),
}
}
}
impl<R: Runtime> SystemTrayHandle<R> {
pub fn get_item(&self, id: MenuIdRef<'_>) -> SystemTrayMenuItemHandle<R> {
let ids = self.ids.lock().unwrap();
let iter = ids.iter();
for (raw, item_id) in iter {
if item_id == id {
return SystemTrayMenuItemHandle {
id: *raw,
tray_handler: self.inner.clone(),
};
}
}
panic!("item id not found")
}
pub fn try_get_item(&self, id: MenuIdRef<'_>) -> Option<SystemTrayMenuItemHandle<R>> {
self
.ids
.lock()
.unwrap()
.iter()
.find(|i| i.1 == id)
.map(|i| SystemTrayMenuItemHandle {
id: *i.0,
tray_handler: self.inner.clone(),
})
}
pub fn set_icon(&self, icon: Icon) -> crate::Result<()> {
self.inner.set_icon(icon.try_into()?).map_err(Into::into)
}
pub fn set_menu(&self, menu: SystemTrayMenu) -> crate::Result<()> {
let mut ids = HashMap::new();
get_menu_ids(&mut ids, &menu);
self.inner.set_menu(menu)?;
*self.ids.lock().unwrap() = ids;
Ok(())
}
#[cfg(target_os = "macos")]
pub fn set_icon_as_template(&self, is_template: bool) -> crate::Result<()> {
self
.inner
.set_icon_as_template(is_template)
.map_err(Into::into)
}
#[cfg(target_os = "macos")]
pub fn set_title(&self, title: &str) -> crate::Result<()> {
self.inner.set_title(title).map_err(Into::into)
}
pub fn set_tooltip(&self, tooltip: &str) -> crate::Result<()> {
self.inner.set_tooltip(tooltip).map_err(Into::into)
}
pub fn destroy(&self) -> crate::Result<()> {
self.inner.destroy().map_err(Into::into)
}
}
impl<R: Runtime> SystemTrayMenuItemHandle<R> {
pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
self
.tray_handler
.update_item(self.id, MenuUpdate::SetEnabled(enabled))
.map_err(Into::into)
}
pub fn set_title<S: Into<String>>(&self, title: S) -> crate::Result<()> {
self
.tray_handler
.update_item(self.id, MenuUpdate::SetTitle(title.into()))
.map_err(Into::into)
}
pub fn set_selected(&self, selected: bool) -> crate::Result<()> {
self
.tray_handler
.update_item(self.id, MenuUpdate::SetSelected(selected))
.map_err(Into::into)
}
#[cfg(target_os = "macos")]
#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
pub fn set_native_image(&self, image: crate::NativeImage) -> crate::Result<()> {
self
.tray_handler
.update_item(self.id, MenuUpdate::SetNativeImage(image))
.map_err(Into::into)
}
}