Ver Fonte

feat(core): create system tray at runtime, closes #2278 (#4862)

Lucas Fernandes Nogueira há 3 anos atrás
pai
commit
4d063ae9ee

+ 7 - 0
.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.

+ 5 - 0
.changes/tray-destroy.md

@@ -0,0 +1,5 @@
+---
+"tauri": minor
+---
+
+Added the `SystemTrayHandle::destroy` method.

+ 1 - 1
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"

+ 169 - 128
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<T: UserEvent>(context: &Context<T>, message: Message<T>) ->
       &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<T: UserEvent>(context: &Context<T>, message: Message<T>) ->
         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<T: UserEvent> {
   pub clipboard_manager: Arc<Mutex<Clipboard>>,
   pub windows: Arc<Mutex<HashMap<WebviewId, WindowWrapper>>>,
   #[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<Result<()>>),
+  Destroy,
 }
 
 pub type CreateWebviewClosure<T> = Box<
@@ -1098,7 +1094,7 @@ pub enum Message<T: 'static> {
   Window(WebviewId, WindowMessage),
   Webview(WebviewId, WebviewMessage),
   #[cfg(all(desktop, feature = "system-tray"))]
-  Tray(TrayMessage),
+  Tray(TrayId, TrayMessage),
   CreateWebview(WebviewId, CreateWebviewClosure<T>),
   CreateWindow(
     WebviewId,
@@ -1117,7 +1113,7 @@ impl<T: UserEvent> Clone for Message<T> {
     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<T: UserEvent> Dispatch<T> for WryDispatcher<T> {
   }
 }
 
-#[cfg(all(desktop, feature = "system-tray"))]
-#[derive(Clone, Default)]
-pub struct TrayContext {
-  tray: Arc<Mutex<Option<Arc<Mutex<WrySystemTray>>>>>,
-  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<WebView>),
@@ -1650,7 +1629,10 @@ impl<T: UserEvent> fmt::Debug for Wry<T> {
       .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<T: UserEvent> RuntimeHandle<T> for WryHandle<T> {
     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<<Self::Runtime as Runtime<T>>::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<T: UserEvent> Wry<T> {
     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<T: UserEvent> Wry<T> {
         clipboard_manager,
         windows,
         #[cfg(all(desktop, feature = "system-tray"))]
-        tray_context,
+        system_tray_manager,
       },
     };
 
@@ -1906,50 +1901,45 @@ impl<T: UserEvent> Runtime<T> for Wry<T> {
   }
 
   #[cfg(all(desktop, feature = "system-tray"))]
-  fn system_tray(&self, system_tray: SystemTray) -> Result<Self::TrayHandler> {
-    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<Self::TrayHandler> {
+    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<F: Fn(&SystemTrayEvent) + Send + 'static>(&mut self, f: F) -> Uuid {
-    let id = Uuid::new_v4();
+  fn on_system_tray_event<F: Fn(TrayId, &SystemTrayEvent) + Send + 'static>(&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<T: UserEvent> Runtime<T> for Wry<T> {
     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<T: UserEvent> Runtime<T> for Wry<T> {
               #[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<T: UserEvent> Runtime<T> for Wry<T> {
             #[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<T: UserEvent> Runtime<T> for Wry<T> {
     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<T: UserEvent> Runtime<T> for Wry<T> {
             #[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<T: UserEvent> Runtime<T> for Wry<T> {
           #[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<Mutex<Clipboard>>,
   #[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<Mutex<WryShortcutManager>>,
@@ -2135,17 +2123,16 @@ struct UserMessageContext<'a> {
   clipboard_manager: Arc<Mutex<Clipboard>>,
   windows: Arc<Mutex<HashMap<WebviewId, WindowWrapper>>>,
   #[cfg(all(desktop, feature = "system-tray"))]
-  tray_context: &'a TrayContext,
+  system_tray_manager: SystemTrayManager,
 }
 
 fn handle_user_message<T: UserEvent>(
   event_loop: &EventLoopWindowTarget<Message<T>>,
   message: Message<T>,
-  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<T: UserEvent>(
     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<T: UserEvent>(
     }
 
     #[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),
-          #[cfg(target_os = "macos")]
-          MenuUpdate::SetNativeImage(image) => {
-            item.set_native_image(NativeImageWrapper::from(image).0)
+    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();
           }
-        }
-      }
-      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);
+
+          Err(e) => {
+            tx.send(Err(e)).unwrap();
           }
         }
-      }
-      #[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);
+      } 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")]
+          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::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<T: UserEvent>(
     #[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<T: UserEvent>(
       ..
     } => {
       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<T: UserEvent>(
         // 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<T: UserEvent>(
           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<T: UserEvent>(
             clipboard_manager,
             windows,
             #[cfg(all(desktop, feature = "system-tray"))]
-            tray_context,
+            system_tray_manager,
           },
           web_context,
         );

+ 90 - 11
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<dyn Fn(TrayId, &SystemTrayEvent) + Send>;
+pub type GlobalSystemTrayEventListeners = Arc<Mutex<Vec<Arc<GlobalSystemTrayEventHandler>>>>;
+
 pub type SystemTrayEventHandler = Box<dyn Fn(&SystemTrayEvent) + Send>;
-pub type SystemTrayEventListeners = Arc<Mutex<HashMap<Uuid, Arc<SystemTrayEventHandler>>>>;
+pub type SystemTrayEventListeners = Arc<Mutex<Vec<Arc<SystemTrayEventHandler>>>>;
 pub type SystemTrayItems = Arc<Mutex<HashMap<u16, WryCustomMenuItem>>>;
 
+#[derive(Clone, Default)]
+pub struct TrayContext {
+  pub tray: Arc<Mutex<Option<WrySystemTray>>>,
+  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<Mutex<HashMap<TrayId, TrayContext>>>,
+  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<Icon> for TrayIcon {
   }
 }
 
+pub fn create_tray<T>(
+  id: WryTrayId,
+  system_tray: SystemTray,
+  event_loop: &EventLoopWindowTarget<T>,
+) -> crate::Result<(WrySystemTray, HashMap<u16, WryCustomMenuItem>)> {
+  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<T: UserEvent> {
+  pub(crate) id: TrayId,
   pub(crate) proxy: EventLoopProxy<super::Message<T>>,
 }
 
@@ -57,28 +125,39 @@ impl<T: UserEvent> TrayHandle for SystemTrayHandle<T> {
   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)
   }
 }

+ 1 - 0
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"

+ 77 - 5
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<Icon>,
   pub menu: Option<menu::SystemTrayMenu>,
   #[cfg(target_os = "macos")]
   pub icon_as_template: bool,
   #[cfg(target_os = "macos")]
   pub menu_on_left_click: bool,
+  pub on_event: Option<Box<TrayEventHandler>>,
+}
+
+#[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<F: Fn(&SystemTrayEvent) + Send + 'static>(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<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'st
   /// Run a task on the main thread.
   fn run_on_main_thread<F: FnOnce() + Send + 'static>(&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<<Self::Runtime as Runtime<T>>::TrayHandler>;
 
   fn raw_display_handle(&self) -> RawDisplayHandle;
 }
@@ -348,7 +420,7 @@ pub trait Runtime<T: UserEvent>: 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<F: Fn(&SystemTrayEvent) + Send + 'static>(&mut self, f: F) -> Uuid;
+  fn on_system_tray_event<F: Fn(TrayId, &SystemTrayEvent) + Send + 'static>(&mut self, f: F);
 
   /// Sets the activation policy for the application. It is set to `NSApplicationActivationPolicyRegular` by default.
   #[cfg(target_os = "macos")]

+ 1 - 0
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.

+ 1 - 3
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.

+ 111 - 118
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<R: Runtime> {
   global_shortcut_manager: R::GlobalShortcutManager,
   #[cfg(feature = "clipboard")]
   clipboard_manager: R::ClipboardManager,
-  #[cfg(all(desktop, feature = "system-tray"))]
-  tray_handle: Option<tray::SystemTrayHandle<R>>,
   /// The updater configuration.
   #[cfg(updater)]
   pub(crate) updater_settings: UpdaterSettings,
@@ -379,8 +375,6 @@ impl<R: Runtime> Clone for AppHandle<R> {
       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<R: Runtime> AppHandle<R> {
       .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<R: Runtime> AppHandle<R> {
     }
     #[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<R: Runtime> {
   global_shortcut_manager: R::GlobalShortcutManager,
   #[cfg(feature = "clipboard")]
   clipboard_manager: R::ClipboardManager,
-  #[cfg(all(desktop, feature = "system-tray"))]
-  tray_handle: Option<tray::SystemTrayHandle<R>>,
   handle: AppHandle<R>,
 }
 
@@ -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<R> {
         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<tray::SystemTrayHandle<R>> {
+        self
+          .manager()
+          .get_tray(id)
       }
 
       /// The path resolver for the application.
@@ -672,7 +717,7 @@ impl<R: Runtime> App<R> {
   /// 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<R: Runtime> App<R> {
   ///
   /// # Examples
   ///
-  /// ```rust,no_run
+  /// ```
   /// tauri::Builder::default()
   ///   .setup(|app| {
   ///     let matches = app.get_cli_matches()?;
@@ -714,7 +759,7 @@ impl<R: Runtime> App<R> {
   /// 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<R: Runtime> App<R> {
   /// 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<R: Runtime> App<R> {
 /// 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<R: Runtime> Builder<R> {
   /// 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<R: Runtime> Builder<R> {
   /// Defines the setup hook.
   ///
   /// # Examples
-  /// ```rust,no_run
+  /// ```
   /// use tauri::Manager;
   /// tauri::Builder::default()
   ///   .setup(|app| {
@@ -1076,7 +1121,7 @@ impl<R: Runtime> Builder<R> {
   ///
   /// 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<R: Runtime> Builder<R> {
   ///
   /// # Examples
   ///
-  /// ```rust,no_run
+  /// ```,no_run
   /// use tauri::State;
   ///
   /// struct MyInt(isize);
@@ -1149,7 +1194,21 @@ impl<R: Runtime> Builder<R> {
     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<R: Runtime> Builder<R> {
   /// 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<R: Runtime> Builder<R> {
   /// 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<R: Runtime> Builder<R> {
   /// 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<R: Runtime> Builder<R> {
 
   /// 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<R: Runtime> Builder<R> {
   ///
   /// - Use a macOS Universal binary target name:
   ///
-  /// ```no_run
+  /// ```
   /// let mut builder = tauri::Builder::default();
   /// #[cfg(target_os = "macos")]
   /// {
@@ -1323,7 +1384,7 @@ impl<R: Runtime> Builder<R> {
   ///
   /// - 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<R: Runtime> Builder<R> {
   ///
   /// - Use the platform's target triple:
   ///
-  /// ```no_run
+  /// ```
   /// tauri::Builder::default()
   ///   .updater_target(tauri::utils::platform::target_triple().unwrap());
   /// ```
@@ -1349,18 +1410,6 @@ impl<R: Runtime> Builder<R> {
       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<R: Runtime> Builder<R> {
       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<R: Runtime> Builder<R> {
         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<R: Runtime> Builder<R> {
     }
 
     #[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);
-      }
-      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);
+    {
+      if let Some(tray) = self.system_tray {
+        tray.build(&app)?;
       }
-      #[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);
+            }
           });
       }
     }

+ 337 - 14
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<MenuHash, MenuId>, menu: &SystemTrayMenu) {
   for item in &menu.items {
     match item {
@@ -33,9 +41,11 @@ pub(crate) fn get_menu_ids(map: &mut HashMap<MenuHash, MenuId>, 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<tauri_runtime::Icon>,
   /// 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<Arc<TrayEventHandler>>,
+  // 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<I: Into<String>>(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<I: TryInto<tauri_runtime::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<F: Fn(SystemTrayEvent) + Send + Sync + 'static>(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<SystemTray> 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<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);
+      }
+    }
+
+    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) = tray.menu {
-      t = t.with_menu(menu);
+    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<f64>,
     /// 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<f64>,
     /// 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<f64>,
     /// 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<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,
+      },
+    }
+  }
+}
+
 /// A handle to a system tray. Allows updating the context menu items.
 #[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,
 }
@@ -184,6 +501,7 @@ pub struct SystemTrayHandle<R: Runtime> {
 impl<R: Runtime> Clone for SystemTrayHandle<R> {
   fn clone(&self) -> Self {
     Self {
+      id: self.id,
       ids: self.ids.clone(),
       inner: self.inner.clone(),
     }
@@ -245,6 +563,11 @@ impl<R: Runtime> SystemTrayHandle<R> {
       .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<R: Runtime> SystemTrayMenuItemHandle<R> {

+ 34 - 0
core/tauri/src/manager.rs

@@ -196,6 +196,8 @@ fn replace_csp_nonce(
 #[default_runtime(crate::Wry, wry)]
 pub struct InnerWindowManager<R: Runtime> {
   windows: Mutex<HashMap<String, Window<R>>>,
+  #[cfg(all(desktop, feature = "system-tray"))]
+  pub(crate) trays: Mutex<HashMap<String, crate::SystemTrayHandle<R>>>,
   pub(crate) plugins: Mutex<PluginStore<R>>,
   listeners: Listeners,
   pub(crate) state: Arc<StateManager>,
@@ -210,6 +212,7 @@ pub struct InnerWindowManager<R: Runtime> {
   assets: Arc<dyn Assets>,
   pub(crate) default_window_icon: Option<Icon>,
   pub(crate) app_icon: Option<Vec<u8>>,
+  pub(crate) tray_icon: Option<Icon>,
 
   package_info: PackageInfo,
   /// The webview protocols protocols available to all windows.
@@ -236,6 +239,7 @@ impl<R: Runtime> fmt::Debug for InnerWindowManager<R> {
       .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<R: Runtime> WindowManager<R> {
     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<R: Runtime> WindowManager<R> {
         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<R: Runtime> WindowManager<R> {
   }
 }
 
+/// Tray APIs
+#[cfg(all(desktop, feature = "system-tray"))]
+impl<R: Runtime> WindowManager<R> {
+  pub fn get_tray(&self, id: &str) -> Option<crate::SystemTrayHandle<R>> {
+    self.inner.trays.lock().unwrap().get(id).cloned()
+  }
+
+  pub fn trays(&self) -> HashMap<String, crate::SystemTrayHandle<R>> {
+    self.inner.trays.lock().unwrap().clone()
+  }
+
+  pub fn attach_tray(&self, id: String, tray: crate::SystemTrayHandle<R>) {
+    self.inner.trays.lock().unwrap().insert(id, tray);
+  }
+
+  pub fn get_tray_by_runtime_id(&self, id: u16) -> Option<(String, crate::SystemTrayHandle<R>)> {
+    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<R: Runtime>(
   window: &Window<R>,
   manager: &WindowManager<R>,

+ 13 - 8
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<T: UserEvent> RuntimeHandle<T> 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<<Self::Runtime as Runtime<T>>::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<T: UserEvent> Runtime<T> for MockRuntime {
 
   #[cfg(all(desktop, feature = "system-tray"))]
   #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-  fn on_system_tray_event<F: Fn(&SystemTrayEvent) + Send + 'static>(&mut self, f: F) -> Uuid {
-    Uuid::new_v4()
-  }
+  fn on_system_tray_event<F: Fn(TrayId, &SystemTrayEvent) + Send + 'static>(&mut self, f: F) {}
 
   #[cfg(target_os = "macos")]
   #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]

+ 3 - 2
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",

+ 107 - 94
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(|_| ())
+}