Эх сурвалжийг харах

refactor(core): new system tray and window menu APIs, closes #1898 (#1944)

Lucas Fernandes Nogueira 4 жил өмнө
parent
commit
f7e9fe8f3f

+ 5 - 0
.changes/menu-refactor.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+**Breaking change**: The `menu` API was not designed to have all the new features: submenus, item updates, disabled state... so we broke it before going to stable.

+ 5 - 0
.changes/system-tray-refactor.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+**Breaking change**: The `system_tray` and `on_system_tray_event` APIs were not designed to have all the new features: submenus, item updates, click events, positioning... so we broke it before going to stable.

+ 1 - 1
Cargo.toml

@@ -23,7 +23,7 @@ members = [
 ]
 
 [patch.crates-io]
-tao = { git = "https://github.com/tauri-apps/tao", rev = "a3f533232df25dc30998809094ed5431b449489c" }
+tao = { git = "https://github.com/tauri-apps/tao", rev = "5be88eb9488e3ad27194b5eff2ea31a473128f9c" }
 
 # default to small, optimized workspace release binaries
 [profile.release]

+ 260 - 126
core/tauri-runtime-wry/src/lib.rs

@@ -19,13 +19,13 @@ use tauri_runtime::{
 #[cfg(feature = "menu")]
 use tauri_runtime::window::MenuEvent;
 #[cfg(feature = "system-tray")]
-use tauri_runtime::SystemTrayEvent;
+use tauri_runtime::{SystemTray, SystemTrayEvent};
 #[cfg(windows)]
 use winapi::shared::windef::HWND;
-#[cfg(feature = "system-tray")]
-use wry::application::platform::system_tray::SystemTrayBuilder;
 #[cfg(windows)]
 use wry::application::platform::windows::WindowBuilderExtWindows;
+#[cfg(feature = "system-tray")]
+use wry::application::system_tray::{SystemTray as WrySystemTray, SystemTrayBuilder};
 
 use tauri_utils::config::WindowConfig;
 use uuid::Uuid;
@@ -63,7 +63,7 @@ mod menu;
 use menu::*;
 
 type CreateWebviewHandler =
-  Box<dyn FnOnce(&EventLoopWindowTarget<Message>) -> Result<WebView> + Send>;
+  Box<dyn FnOnce(&EventLoopWindowTarget<Message>) -> Result<WebviewWrapper> + Send>;
 type MainThreadTask = Box<dyn FnOnce() + Send>;
 type WindowEventHandler = Box<dyn Fn(&WindowEvent) + Send>;
 type WindowEventListeners = Arc<Mutex<HashMap<Uuid, WindowEventHandler>>>;
@@ -241,7 +241,14 @@ impl From<Position> for PositionWrapper {
 }
 
 #[derive(Debug, Clone, Default)]
-pub struct WindowBuilderWrapper(WryWindowBuilder);
+pub struct WindowBuilderWrapper {
+  inner: WryWindowBuilder,
+  #[cfg(feature = "menu")]
+  menu_items: HashMap<u32, WryCustomMenuItem>,
+}
+
+// safe since `menu_items` are read only here
+unsafe impl Send for WindowBuilderWrapper {}
 
 impl WindowBuilderBase for WindowBuilderWrapper {}
 impl WindowBuilder for WindowBuilderWrapper {
@@ -280,108 +287,122 @@ impl WindowBuilder for WindowBuilderWrapper {
   }
 
   #[cfg(feature = "menu")]
-  fn menu<I: MenuId>(self, menu: Vec<Menu<I>>) -> Self {
-    Self(
-      self.0.with_menu(
-        menu
-          .into_iter()
-          .map(|m| MenuWrapper::from(m).0)
-          .collect::<Vec<WryMenu>>(),
-      ),
-    )
+  fn menu<I: MenuId>(mut self, menu: Menu<I>) -> Self {
+    let mut items = HashMap::new();
+    let window_menu = to_wry_menu(&mut items, menu);
+    self.menu_items = items;
+    self.inner = self.inner.with_menu(window_menu);
+    self
   }
 
-  fn position(self, x: f64, y: f64) -> Self {
-    Self(self.0.with_position(WryLogicalPosition::new(x, y)))
+  fn position(mut self, x: f64, y: f64) -> Self {
+    self.inner = self.inner.with_position(WryLogicalPosition::new(x, y));
+    self
   }
 
-  fn inner_size(self, width: f64, height: f64) -> Self {
-    Self(self.0.with_inner_size(WryLogicalSize::new(width, height)))
+  fn inner_size(mut self, width: f64, height: f64) -> Self {
+    self.inner = self
+      .inner
+      .with_inner_size(WryLogicalSize::new(width, height));
+    self
   }
 
-  fn min_inner_size(self, min_width: f64, min_height: f64) -> Self {
-    Self(
-      self
-        .0
-        .with_min_inner_size(WryLogicalSize::new(min_width, min_height)),
-    )
+  fn min_inner_size(mut self, min_width: f64, min_height: f64) -> Self {
+    self.inner = self
+      .inner
+      .with_min_inner_size(WryLogicalSize::new(min_width, min_height));
+    self
   }
 
-  fn max_inner_size(self, max_width: f64, max_height: f64) -> Self {
-    Self(
-      self
-        .0
-        .with_max_inner_size(WryLogicalSize::new(max_width, max_height)),
-    )
+  fn max_inner_size(mut self, max_width: f64, max_height: f64) -> Self {
+    self.inner = self
+      .inner
+      .with_max_inner_size(WryLogicalSize::new(max_width, max_height));
+    self
   }
 
-  fn resizable(self, resizable: bool) -> Self {
-    Self(self.0.with_resizable(resizable))
+  fn resizable(mut self, resizable: bool) -> Self {
+    self.inner = self.inner.with_resizable(resizable);
+    self
   }
 
-  fn title<S: Into<String>>(self, title: S) -> Self {
-    Self(self.0.with_title(title.into()))
+  fn title<S: Into<String>>(mut self, title: S) -> Self {
+    self.inner = self.inner.with_title(title.into());
+    self
   }
 
-  fn fullscreen(self, fullscreen: bool) -> Self {
-    if fullscreen {
-      Self(self.0.with_fullscreen(Some(Fullscreen::Borderless(None))))
+  fn fullscreen(mut self, fullscreen: bool) -> Self {
+    self.inner = if fullscreen {
+      self
+        .inner
+        .with_fullscreen(Some(Fullscreen::Borderless(None)))
     } else {
-      Self(self.0.with_fullscreen(None))
-    }
+      self.inner.with_fullscreen(None)
+    };
+    self
   }
 
-  fn focus(self) -> Self {
-    Self(self.0.with_focus())
+  fn focus(mut self) -> Self {
+    self.inner = self.inner.with_focus();
+    self
   }
 
-  fn maximized(self, maximized: bool) -> Self {
-    Self(self.0.with_maximized(maximized))
+  fn maximized(mut self, maximized: bool) -> Self {
+    self.inner = self.inner.with_maximized(maximized);
+    self
   }
 
-  fn visible(self, visible: bool) -> Self {
-    Self(self.0.with_visible(visible))
+  fn visible(mut self, visible: bool) -> Self {
+    self.inner = self.inner.with_visible(visible);
+    self
   }
 
-  fn transparent(self, transparent: bool) -> Self {
-    Self(self.0.with_transparent(transparent))
+  fn transparent(mut self, transparent: bool) -> Self {
+    self.inner = self.inner.with_transparent(transparent);
+    self
   }
 
-  fn decorations(self, decorations: bool) -> Self {
-    Self(self.0.with_decorations(decorations))
+  fn decorations(mut self, decorations: bool) -> Self {
+    self.inner = self.inner.with_decorations(decorations);
+    self
   }
 
-  fn always_on_top(self, always_on_top: bool) -> Self {
-    Self(self.0.with_always_on_top(always_on_top))
+  fn always_on_top(mut self, always_on_top: bool) -> Self {
+    self.inner = self.inner.with_always_on_top(always_on_top);
+    self
   }
 
   #[cfg(windows)]
-  fn parent_window(self, parent: HWND) -> Self {
-    Self(self.0.with_parent_window(parent))
+  fn parent_window(mut self, parent: HWND) -> Self {
+    self.inner = self.inner.with_parent_window(parent);
+    self
   }
 
   #[cfg(windows)]
-  fn owner_window(self, owner: HWND) -> Self {
-    Self(self.0.with_owner_window(owner))
+  fn owner_window(mut self, owner: HWND) -> Self {
+    self.inner = self.inner.with_owner_window(owner);
+    self
   }
 
-  fn icon(self, icon: Icon) -> Result<Self> {
-    Ok(Self(
-      self.0.with_window_icon(Some(WryIcon::try_from(icon)?.0)),
-    ))
+  fn icon(mut self, icon: Icon) -> Result<Self> {
+    self.inner = self
+      .inner
+      .with_window_icon(Some(WryIcon::try_from(icon)?.0));
+    Ok(self)
   }
 
-  fn skip_taskbar(self, skip: bool) -> Self {
-    Self(self.0.with_skip_taskbar(skip))
+  fn skip_taskbar(mut self, skip: bool) -> Self {
+    self.inner = self.inner.with_skip_taskbar(skip);
+    self
   }
 
   fn has_icon(&self) -> bool {
-    self.0.window.window_icon.is_some()
+    self.inner.window.window_icon.is_some()
   }
 
   #[cfg(feature = "menu")]
   fn has_menu(&self) -> bool {
-    self.0.window.window_menu.is_some()
+    self.inner.window.window_menu.is_some()
   }
 }
 
@@ -452,6 +473,8 @@ enum WindowMessage {
   SetIcon(WindowIcon),
   SetSkipTaskbar(bool),
   DragWindow,
+  #[cfg(feature = "menu")]
+  UpdateMenuItem(u32, menu::MenuUpdate),
 }
 
 #[derive(Debug, Clone)]
@@ -460,10 +483,21 @@ enum WebviewMessage {
   Print,
 }
 
+#[cfg(feature = "system-tray")]
 #[derive(Clone)]
-enum Message {
+pub(crate) enum TrayMessage {
+  UpdateItem(u32, menu::MenuUpdate),
+  UpdateIcon(Icon),
+  #[cfg(windows)]
+  Remove,
+}
+
+#[derive(Clone)]
+pub(crate) enum Message {
   Window(WindowId, WindowMessage),
   Webview(WindowId, WebviewMessage),
+  #[cfg(feature = "system-tray")]
+  Tray(TrayMessage),
   CreateWebview(Arc<Mutex<Option<CreateWebviewHandler>>>, Sender<WindowId>),
 }
 
@@ -842,19 +876,45 @@ impl Dispatch for WryDispatcher {
       ))
       .map_err(|_| Error::FailedToSendMessage)
   }
+
+  #[cfg(feature = "menu")]
+  fn update_menu_item(&self, id: u32, update: menu::MenuUpdate) -> Result<()> {
+    self
+      .context
+      .proxy
+      .send_event(Message::Window(
+        self.window_id,
+        WindowMessage::UpdateMenuItem(id, update),
+      ))
+      .map_err(|_| Error::FailedToSendMessage)
+  }
+}
+
+#[cfg(feature = "system-tray")]
+#[derive(Clone, Default)]
+struct TrayContext {
+  tray: Arc<Mutex<Option<Arc<Mutex<WrySystemTray>>>>>,
+  listeners: SystemTrayEventListeners,
+  items: SystemTrayItems,
+}
+
+struct WebviewWrapper {
+  inner: WebView,
+  #[cfg(feature = "menu")]
+  menu_items: HashMap<u32, WryCustomMenuItem>,
 }
 
 /// A Tauri [`Runtime`] wrapper around wry.
 pub struct Wry {
   event_loop: EventLoop<Message>,
-  webviews: Arc<Mutex<HashMap<WindowId, WebView>>>,
+  webviews: Arc<Mutex<HashMap<WindowId, WebviewWrapper>>>,
   task_tx: Sender<MainThreadTask>,
   window_event_listeners: WindowEventListeners,
   #[cfg(feature = "menu")]
   menu_event_listeners: MenuEventListeners,
-  #[cfg(feature = "system-tray")]
-  system_tray_event_listeners: SystemTrayEventListeners,
   task_rx: Arc<Receiver<MainThreadTask>>,
+  #[cfg(feature = "system-tray")]
+  tray_context: TrayContext,
 }
 
 /// A handle to the Wry runtime.
@@ -892,11 +952,22 @@ impl RuntimeHandle for WryHandle {
     };
     Ok(DetachedWindow { label, dispatcher })
   }
+
+  #[cfg(all(windows, feature = "system-tray"))]
+  fn remove_system_tray(&self) -> Result<()> {
+    self
+      .dispatcher_context
+      .proxy
+      .send_event(Message::Tray(TrayMessage::Remove))
+      .map_err(|_| Error::FailedToSendMessage)
+  }
 }
 
 impl Runtime for Wry {
   type Dispatcher = WryDispatcher;
   type Handle = WryHandle;
+  #[cfg(feature = "system-tray")]
+  type TrayHandler = SystemTrayHandle;
 
   fn new() -> Result<Self> {
     let event_loop = EventLoop::<Message>::with_user_event();
@@ -910,7 +981,7 @@ impl Runtime for Wry {
       #[cfg(feature = "menu")]
       menu_event_listeners: Default::default(),
       #[cfg(feature = "system-tray")]
-      system_tray_event_listeners: Default::default(),
+      tray_context: Default::default(),
     })
   }
 
@@ -945,7 +1016,7 @@ impl Runtime for Wry {
     )?;
 
     let dispatcher = WryDispatcher {
-      window_id: webview.window().id(),
+      window_id: webview.inner.window().id(),
       context: DispatcherContext {
         proxy,
         task_tx: self.task_tx.clone(),
@@ -959,56 +1030,43 @@ impl Runtime for Wry {
       .webviews
       .lock()
       .unwrap()
-      .insert(webview.window().id(), webview);
+      .insert(webview.inner.window().id(), webview);
 
     Ok(DetachedWindow { label, dispatcher })
   }
 
   #[cfg(feature = "system-tray")]
-  fn system_tray<I: MenuId>(
-    &self,
-    icon: Icon,
-    menu_items: Vec<SystemTrayMenuItem<I>>,
-  ) -> Result<()> {
-    // todo: fix this interface in Tao to an enum similar to Icon
+  fn system_tray<I: MenuId>(&self, system_tray: SystemTray<I>) -> Result<Self::TrayHandler> {
+    let icon = system_tray
+      .icon
+      .expect("tray icon not set")
+      .into_tray_icon();
 
-    // we expect the code that passes the Icon enum to have already checked the platform.
-    let icon = match icon {
-      #[cfg(target_os = "linux")]
-      Icon::File(path) => path,
-
-      #[cfg(not(target_os = "linux"))]
-      Icon::Raw(bytes) => bytes,
-
-      #[cfg(target_os = "linux")]
-      Icon::Raw(_) => {
-        panic!("linux requires the system menu icon to be a file path, not bytes.")
-      }
-
-      #[cfg(not(target_os = "linux"))]
-      Icon::File(_) => {
-        panic!("non-linux system menu icons must be bytes, not a file path",)
-      }
-      _ => unreachable!(),
-    };
+    let mut items = HashMap::new();
 
-    SystemTrayBuilder::new(
+    let tray = SystemTrayBuilder::new(
       icon,
-      menu_items
-        .into_iter()
-        .map(|m| MenuItemWrapper::from(m).0)
-        .collect(),
+      system_tray
+        .menu
+        .map(|menu| to_wry_context_menu(&mut items, menu)),
     )
     .build(&self.event_loop)
     .map_err(|e| Error::SystemTray(Box::new(e)))?;
-    Ok(())
+
+    *self.tray_context.items.lock().unwrap() = items;
+    *self.tray_context.tray.lock().unwrap() = Some(Arc::new(Mutex::new(tray)));
+
+    Ok(SystemTrayHandle {
+      proxy: self.event_loop.create_proxy(),
+    })
   }
 
   #[cfg(feature = "system-tray")]
   fn on_system_tray_event<F: Fn(&SystemTrayEvent) + Send + 'static>(&mut self, f: F) -> Uuid {
     let id = Uuid::new_v4();
     self
-      .system_tray_event_listeners
+      .tray_context
+      .listeners
       .lock()
       .unwrap()
       .insert(id, Box::new(f));
@@ -1024,7 +1082,7 @@ impl Runtime for Wry {
     #[cfg(feature = "menu")]
     let menu_event_listeners = self.menu_event_listeners.clone();
     #[cfg(feature = "system-tray")]
-    let system_tray_event_listeners = self.system_tray_event_listeners.clone();
+    let tray_context = self.tray_context.clone();
 
     let mut iteration = RunIteration::default();
 
@@ -1039,13 +1097,14 @@ impl Runtime for Wry {
           event_loop,
           control_flow,
           EventLoopIterationContext {
+            callback: None,
             webviews: webviews.lock().expect("poisoned webview collection"),
             task_rx: task_rx.clone(),
             window_event_listeners: window_event_listeners.clone(),
             #[cfg(feature = "menu")]
             menu_event_listeners: menu_event_listeners.clone(),
             #[cfg(feature = "system-tray")]
-            system_tray_event_listeners: system_tray_event_listeners.clone(),
+            tray_context: tray_context.clone(),
           },
         );
       });
@@ -1053,14 +1112,14 @@ impl Runtime for Wry {
     iteration
   }
 
-  fn run(self) {
+  fn run<F: Fn() + 'static>(self, callback: F) {
     let webviews = self.webviews.clone();
     let task_rx = self.task_rx;
     let window_event_listeners = self.window_event_listeners.clone();
     #[cfg(feature = "menu")]
     let menu_event_listeners = self.menu_event_listeners.clone();
     #[cfg(feature = "system-tray")]
-    let system_tray_event_listeners = self.system_tray_event_listeners;
+    let tray_context = self.tray_context;
 
     self.event_loop.run(move |event, event_loop, control_flow| {
       handle_event_loop(
@@ -1068,13 +1127,14 @@ impl Runtime for Wry {
         event_loop,
         control_flow,
         EventLoopIterationContext {
+          callback: Some(&callback),
           webviews: webviews.lock().expect("poisoned webview collection"),
           task_rx: task_rx.clone(),
           window_event_listeners: window_event_listeners.clone(),
           #[cfg(feature = "menu")]
           menu_event_listeners: menu_event_listeners.clone(),
           #[cfg(feature = "system-tray")]
-          system_tray_event_listeners: system_tray_event_listeners.clone(),
+          tray_context: tray_context.clone(),
         },
       );
     })
@@ -1082,13 +1142,14 @@ impl Runtime for Wry {
 }
 
 struct EventLoopIterationContext<'a> {
-  webviews: MutexGuard<'a, HashMap<WindowId, WebView>>,
+  callback: Option<&'a (dyn Fn() + 'static)>,
+  webviews: MutexGuard<'a, HashMap<WindowId, WebviewWrapper>>,
   task_rx: Arc<Receiver<MainThreadTask>>,
   window_event_listeners: WindowEventListeners,
   #[cfg(feature = "menu")]
   menu_event_listeners: MenuEventListeners,
   #[cfg(feature = "system-tray")]
-  system_tray_event_listeners: SystemTrayEventListeners,
+  tray_context: TrayContext,
 }
 
 fn handle_event_loop(
@@ -1098,18 +1159,19 @@ fn handle_event_loop(
   context: EventLoopIterationContext<'_>,
 ) -> RunIteration {
   let EventLoopIterationContext {
+    callback,
     mut webviews,
     task_rx,
     window_event_listeners,
     #[cfg(feature = "menu")]
     menu_event_listeners,
     #[cfg(feature = "system-tray")]
-    system_tray_event_listeners,
+    tray_context,
   } = context;
   *control_flow = ControlFlow::Wait;
 
   for (_, w) in webviews.iter() {
-    if let Err(e) = w.evaluate_script() {
+    if let Err(e) = w.inner.evaluate_script() {
       eprintln!("{}", e);
     }
   }
@@ -1122,7 +1184,7 @@ fn handle_event_loop(
     #[cfg(feature = "menu")]
     Event::MenuEvent {
       menu_id,
-      origin: MenuType::Menubar,
+      origin: MenuType::MenuBar,
     } => {
       let event = MenuEvent {
         menu_item_id: menu_id.0,
@@ -1134,12 +1196,29 @@ fn handle_event_loop(
     #[cfg(feature = "system-tray")]
     Event::MenuEvent {
       menu_id,
-      origin: MenuType::SystemTray,
+      origin: MenuType::ContextMenu,
     } => {
-      let event = SystemTrayEvent {
-        menu_item_id: menu_id.0,
+      let event = SystemTrayEvent::MenuItemClick(menu_id.0);
+      for handler in tray_context.listeners.lock().unwrap().values() {
+        handler(&event);
+      }
+    }
+    #[cfg(feature = "system-tray")]
+    Event::TrayEvent {
+      bounds,
+      event,
+      position: _cursor_position,
+    } => {
+      let (position, size) = (
+        PhysicalPositionWrapper(bounds.position).into(),
+        PhysicalSizeWrapper(bounds.size).into(),
+      );
+      let event = match event {
+        TrayEvent::LeftClick => SystemTrayEvent::LeftClick { position, size },
+        TrayEvent::RightClick => SystemTrayEvent::RightClick { position, size },
+        TrayEvent::DoubleClick => SystemTrayEvent::DoubleClick { position, size },
       };
-      for handler in system_tray_event_listeners.lock().unwrap().values() {
+      for handler in tray_context.listeners.lock().unwrap().values() {
         handler(&event);
       }
     }
@@ -1154,10 +1233,13 @@ fn handle_event_loop(
           webviews.remove(&window_id);
           if webviews.is_empty() {
             *control_flow = ControlFlow::Exit;
+            if let Some(callback) = callback {
+              callback();
+            }
           }
         }
         WryWindowEvent::Resized(_) => {
-          if let Err(e) = webviews[&window_id].resize() {
+          if let Err(e) = webviews[&window_id].inner.resize() {
             eprintln!("{}", e);
           }
         }
@@ -1167,7 +1249,7 @@ fn handle_event_loop(
     Event::UserEvent(message) => match message {
       Message::Window(id, window_message) => {
         if let Some(webview) = webviews.get_mut(&id) {
-          let window = webview.window();
+          let window = webview.inner.window();
           match window_message {
             // Getters
             WindowMessage::ScaleFactor(tx) => tx.send(window.scale_factor()).unwrap(),
@@ -1256,6 +1338,22 @@ fn handle_event_loop(
             WindowMessage::DragWindow => {
               let _ = window.drag_window();
             }
+            #[cfg(feature = "menu")]
+            WindowMessage::UpdateMenuItem(id, update) => {
+              let item = webview
+                .menu_items
+                .get_mut(&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)
+                }
+              }
+            }
           }
         }
       }
@@ -1263,10 +1361,10 @@ fn handle_event_loop(
         if let Some(webview) = webviews.get_mut(&id) {
           match webview_message {
             WebviewMessage::EvaluateScript(script) => {
-              let _ = webview.dispatch_script(&script);
+              let _ = webview.inner.dispatch_script(&script);
             }
             WebviewMessage::Print => {
-              let _ = webview.print();
+              let _ = webview.inner.print();
             }
           }
         }
@@ -1278,7 +1376,7 @@ fn handle_event_loop(
         };
         match handler(event_loop) {
           Ok(webview) => {
-            let window_id = webview.window().id();
+            let window_id = webview.inner.window().id();
             webviews.insert(window_id, webview);
             sender.send(window_id).unwrap();
           }
@@ -1287,6 +1385,34 @@ fn handle_event_loop(
           }
         }
       }
+      #[cfg(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)
+            }
+          }
+        }
+        TrayMessage::UpdateIcon(icon) => {
+          if let Some(tray) = &*tray_context.tray.lock().unwrap() {
+            tray.lock().unwrap().set_icon(icon.into_tray_icon());
+          }
+        }
+        #[cfg(windows)]
+        TrayMessage::Remove => {
+          if let Some(tray) = tray_context.tray.lock().unwrap().as_ref() {
+            use wry::application::platform::windows::SystemTrayExtWindows;
+            tray.lock().unwrap().remove();
+          }
+        }
+      },
     },
     _ => (),
   }
@@ -1300,7 +1426,7 @@ fn create_webview<P: Params<Runtime = Wry>>(
   event_loop: &EventLoopWindowTarget<Message>,
   context: DispatcherContext,
   pending: PendingWindow<P>,
-) -> Result<WebView> {
+) -> Result<WebviewWrapper> {
   let PendingWindow {
     webview_attributes,
     window_builder,
@@ -1311,8 +1437,10 @@ fn create_webview<P: Params<Runtime = Wry>>(
     ..
   } = pending;
 
-  let is_window_transparent = window_builder.0.window.transparent;
-  let window = window_builder.0.build(event_loop).unwrap();
+  let is_window_transparent = window_builder.inner.window.transparent;
+  #[cfg(feature = "menu")]
+  let menu_items = window_builder.menu_items;
+  let window = window_builder.inner.build(event_loop).unwrap();
   let mut webview_builder = WebViewBuilder::new(window)
     .map_err(|e| Error::CreateWebview(Box::new(e)))?
     .with_url(&url)
@@ -1338,9 +1466,15 @@ fn create_webview<P: Params<Runtime = Wry>>(
     webview_builder = webview_builder.with_initialization_script(&script);
   }
 
-  webview_builder
+  let webview = webview_builder
     .build()
-    .map_err(|e| Error::CreateWebview(Box::new(e)))
+    .map_err(|e| Error::CreateWebview(Box::new(e)))?;
+
+  Ok(WebviewWrapper {
+    inner: webview,
+    #[cfg(feature = "menu")]
+    menu_items,
+  })
 }
 
 /// Create a wry rpc handler from a tauri rpc handler.

+ 198 - 34
core/tauri-runtime-wry/src/menu.rs

@@ -3,15 +3,33 @@
 // SPDX-License-Identifier: MIT
 
 pub use tauri_runtime::{
-  menu::{CustomMenuItem, Menu, MenuItem, SystemTrayMenuItem},
+  menu::{
+    CustomMenuItem, Menu, MenuEntry, MenuItem, MenuUpdate, SystemTrayMenu, SystemTrayMenuEntry,
+    SystemTrayMenuItem, TrayHandle,
+  },
   window::MenuEvent,
-  MenuId, SystemTrayEvent,
+  Icon, MenuId, SystemTrayEvent,
 };
-pub use wry::application::menu::{
-  CustomMenu as WryCustomMenu, Menu as WryMenu, MenuId as WryMenuId, MenuItem as WryMenuItem,
-  MenuType,
+pub use wry::application::{
+  event::TrayEvent,
+  event_loop::EventLoopProxy,
+  menu::{
+    ContextMenu as WryContextMenu, CustomMenuItem as WryCustomMenuItem, MenuBar,
+    MenuId as WryMenuId, MenuItem as WryMenuItem, MenuItemAttributes as WryMenuItemAttributes,
+    MenuType,
+  },
 };
 
+#[cfg(target_os = "macos")]
+use tauri_runtime::menu::NativeImage;
+#[cfg(target_os = "macos")]
+pub use wry::application::platform::macos::{
+  CustomMenuItemExtMacOS, NativeImage as WryNativeImage,
+};
+
+#[cfg(feature = "system-tray")]
+use crate::{Error, Message, Result, TrayMessage};
+
 use uuid::Uuid;
 
 use std::{
@@ -21,27 +39,125 @@ use std::{
 
 pub type MenuEventHandler = Box<dyn Fn(&MenuEvent) + Send>;
 pub type MenuEventListeners = Arc<Mutex<HashMap<Uuid, MenuEventHandler>>>;
+
+#[cfg(feature = "system-tray")]
 pub type SystemTrayEventHandler = Box<dyn Fn(&SystemTrayEvent) + Send>;
+#[cfg(feature = "system-tray")]
 pub type SystemTrayEventListeners = Arc<Mutex<HashMap<Uuid, SystemTrayEventHandler>>>;
+#[cfg(feature = "system-tray")]
+pub type SystemTrayItems = Arc<Mutex<HashMap<u32, WryCustomMenuItem>>>;
+
+#[cfg(feature = "system-tray")]
+#[derive(Clone)]
+pub struct SystemTrayHandle {
+  pub(crate) proxy: EventLoopProxy<super::Message>,
+}
+
+#[cfg(feature = "system-tray")]
+impl TrayHandle for SystemTrayHandle {
+  fn set_icon(&self, icon: Icon) -> Result<()> {
+    self
+      .proxy
+      .send_event(Message::Tray(TrayMessage::UpdateIcon(icon)))
+      .map_err(|_| Error::FailedToSendMessage)
+  }
+  fn update_item(&self, id: u32, update: MenuUpdate) -> Result<()> {
+    self
+      .proxy
+      .send_event(Message::Tray(TrayMessage::UpdateItem(id, update)))
+      .map_err(|_| Error::FailedToSendMessage)
+  }
+}
+
+#[cfg(target_os = "macos")]
+pub struct NativeImageWrapper(pub WryNativeImage);
+
+#[cfg(target_os = "macos")]
+impl From<NativeImage> for NativeImageWrapper {
+  fn from(image: NativeImage) -> NativeImageWrapper {
+    let wry_image = match image {
+      NativeImage::Add => WryNativeImage::Add,
+      NativeImage::Advanced => WryNativeImage::Advanced,
+      NativeImage::Bluetooth => WryNativeImage::Bluetooth,
+      NativeImage::Bookmarks => WryNativeImage::Bookmarks,
+      NativeImage::Caution => WryNativeImage::Caution,
+      NativeImage::ColorPanel => WryNativeImage::ColorPanel,
+      NativeImage::ColumnView => WryNativeImage::ColumnView,
+      NativeImage::Computer => WryNativeImage::Computer,
+      NativeImage::EnterFullScreen => WryNativeImage::EnterFullScreen,
+      NativeImage::Everyone => WryNativeImage::Everyone,
+      NativeImage::ExitFullScreen => WryNativeImage::ExitFullScreen,
+      NativeImage::FlowView => WryNativeImage::FlowView,
+      NativeImage::Folder => WryNativeImage::Folder,
+      NativeImage::FolderBurnable => WryNativeImage::FolderBurnable,
+      NativeImage::FolderSmart => WryNativeImage::FolderSmart,
+      NativeImage::FollowLinkFreestanding => WryNativeImage::FollowLinkFreestanding,
+      NativeImage::FontPanel => WryNativeImage::FontPanel,
+      NativeImage::GoLeft => WryNativeImage::GoLeft,
+      NativeImage::GoRight => WryNativeImage::GoRight,
+      NativeImage::Home => WryNativeImage::Home,
+      NativeImage::IChatTheater => WryNativeImage::IChatTheater,
+      NativeImage::IconView => WryNativeImage::IconView,
+      NativeImage::Info => WryNativeImage::Info,
+      NativeImage::InvalidDataFreestanding => WryNativeImage::InvalidDataFreestanding,
+      NativeImage::LeftFacingTriangle => WryNativeImage::LeftFacingTriangle,
+      NativeImage::ListView => WryNativeImage::ListView,
+      NativeImage::LockLocked => WryNativeImage::LockLocked,
+      NativeImage::LockUnlocked => WryNativeImage::LockUnlocked,
+      NativeImage::MenuMixedState => WryNativeImage::MenuMixedState,
+      NativeImage::MenuOnState => WryNativeImage::MenuOnState,
+      NativeImage::MobileMe => WryNativeImage::MobileMe,
+      NativeImage::MultipleDocuments => WryNativeImage::MultipleDocuments,
+      NativeImage::Network => WryNativeImage::Network,
+      NativeImage::Path => WryNativeImage::Path,
+      NativeImage::PreferencesGeneral => WryNativeImage::PreferencesGeneral,
+      NativeImage::QuickLook => WryNativeImage::QuickLook,
+      NativeImage::RefreshFreestanding => WryNativeImage::RefreshFreestanding,
+      NativeImage::Refresh => WryNativeImage::Refresh,
+      NativeImage::Remove => WryNativeImage::Remove,
+      NativeImage::RevealFreestanding => WryNativeImage::RevealFreestanding,
+      NativeImage::RightFacingTriangle => WryNativeImage::RightFacingTriangle,
+      NativeImage::Share => WryNativeImage::Share,
+      NativeImage::Slideshow => WryNativeImage::Slideshow,
+      NativeImage::SmartBadge => WryNativeImage::SmartBadge,
+      NativeImage::StatusAvailable => WryNativeImage::StatusAvailable,
+      NativeImage::StatusNone => WryNativeImage::StatusNone,
+      NativeImage::StatusPartiallyAvailable => WryNativeImage::StatusPartiallyAvailable,
+      NativeImage::StatusUnavailable => WryNativeImage::StatusUnavailable,
+      NativeImage::StopProgressFreestanding => WryNativeImage::StopProgressFreestanding,
+      NativeImage::StopProgress => WryNativeImage::StopProgress,
 
-pub struct CustomMenuWrapper(pub WryCustomMenu);
+      NativeImage::TrashEmpty => WryNativeImage::TrashEmpty,
+      NativeImage::TrashFull => WryNativeImage::TrashFull,
+      NativeImage::User => WryNativeImage::User,
+      NativeImage::UserAccounts => WryNativeImage::UserAccounts,
+      NativeImage::UserGroup => WryNativeImage::UserGroup,
+      NativeImage::UserGuest => WryNativeImage::UserGuest,
+    };
+    Self(wry_image)
+  }
+}
 
-impl<I: MenuId> From<CustomMenuItem<I>> for CustomMenuWrapper {
-  fn from(item: CustomMenuItem<I>) -> Self {
-    Self(WryCustomMenu {
-      id: WryMenuId(item.id_value()),
-      name: item.name,
-      keyboard_accelerators: None,
-    })
+pub struct MenuItemAttributesWrapper<'a>(pub WryMenuItemAttributes<'a>);
+
+impl<'a, I: MenuId> From<&'a CustomMenuItem<I>> for MenuItemAttributesWrapper<'a> {
+  fn from(item: &'a CustomMenuItem<I>) -> Self {
+    let mut attributes = WryMenuItemAttributes::new(&item.title)
+      .with_enabled(item.enabled)
+      .with_selected(item.selected)
+      .with_id(WryMenuId(item.id_value()));
+    if let Some(accelerator) = item.keyboard_accelerator.as_ref() {
+      attributes = attributes.with_accelerators(&accelerator);
+    }
+    Self(attributes)
   }
 }
 
 pub struct MenuItemWrapper(pub WryMenuItem);
 
-impl<I: MenuId> From<MenuItem<I>> for MenuItemWrapper {
-  fn from(item: MenuItem<I>) -> Self {
+impl From<MenuItem> for MenuItemWrapper {
+  fn from(item: MenuItem) -> Self {
     match item {
-      MenuItem::Custom(custom) => Self(WryMenuItem::Custom(CustomMenuWrapper::from(custom).0)),
       MenuItem::About(v) => Self(WryMenuItem::About(v)),
       MenuItem::Hide => Self(WryMenuItem::Hide),
       MenuItem::Services => Self(WryMenuItem::Services),
@@ -64,29 +180,77 @@ impl<I: MenuId> From<MenuItem<I>> for MenuItemWrapper {
   }
 }
 
-pub struct MenuWrapper(pub WryMenu);
-
-impl<I: MenuId> From<Menu<I>> for MenuWrapper {
-  fn from(menu: Menu<I>) -> Self {
-    Self(WryMenu {
-      title: menu.title,
-      items: menu
-        .items
-        .into_iter()
-        .map(|m| MenuItemWrapper::from(m).0)
-        .collect(),
-    })
+impl From<SystemTrayMenuItem> for MenuItemWrapper {
+  fn from(item: SystemTrayMenuItem) -> Self {
+    match item {
+      SystemTrayMenuItem::Separator => Self(WryMenuItem::Separator),
+      _ => unimplemented!(),
+    }
   }
 }
 
-impl<I: MenuId> From<SystemTrayMenuItem<I>> for MenuItemWrapper {
-  fn from(item: SystemTrayMenuItem<I>) -> Self {
+#[cfg(feature = "menu")]
+pub fn to_wry_menu<I: MenuId>(
+  custom_menu_items: &mut HashMap<u32, WryCustomMenuItem>,
+  menu: Menu<I>,
+) -> MenuBar {
+  let mut wry_menu = MenuBar::new();
+  for item in menu.items {
     match item {
-      SystemTrayMenuItem::Custom(custom) => {
-        Self(WryMenuItem::Custom(CustomMenuWrapper::from(custom).0))
+      MenuEntry::CustomItem(c) => {
+        #[allow(unused_mut)]
+        let mut item = wry_menu.add_item(MenuItemAttributesWrapper::from(&c).0);
+        let id = c.id_value();
+        #[cfg(target_os = "macos")]
+        if let Some(native_image) = c.native_image {
+          item.set_native_image(NativeImageWrapper::from(native_image).0);
+        }
+        custom_menu_items.insert(id, item);
+      }
+      MenuEntry::NativeItem(i) => {
+        wry_menu.add_native_item(MenuItemWrapper::from(i).0);
+      }
+      MenuEntry::Submenu(submenu) => {
+        wry_menu.add_submenu(
+          &submenu.title,
+          submenu.enabled,
+          to_wry_menu(custom_menu_items, submenu.inner),
+        );
+      }
+    }
+  }
+  wry_menu
+}
+
+#[cfg(feature = "system-tray")]
+pub fn to_wry_context_menu<I: MenuId>(
+  custom_menu_items: &mut HashMap<u32, WryCustomMenuItem>,
+  menu: SystemTrayMenu<I>,
+) -> WryContextMenu {
+  let mut tray_menu = WryContextMenu::new();
+  for item in menu.items {
+    match item {
+      SystemTrayMenuEntry::CustomItem(c) => {
+        #[allow(unused_mut)]
+        let mut item = tray_menu.add_item(MenuItemAttributesWrapper::from(&c).0);
+        let id = c.id_value();
+        #[cfg(target_os = "macos")]
+        if let Some(native_image) = c.native_image {
+          item.set_native_image(NativeImageWrapper::from(native_image).0);
+        }
+        custom_menu_items.insert(id, item);
+      }
+      SystemTrayMenuEntry::NativeItem(i) => {
+        tray_menu.add_native_item(MenuItemWrapper::from(i).0);
+      }
+      SystemTrayMenuEntry::Submenu(submenu) => {
+        tray_menu.add_submenu(
+          &submenu.title,
+          submenu.enabled,
+          to_wry_context_menu(custom_menu_items, submenu.inner),
+        );
       }
-      SystemTrayMenuItem::Separator => Self(WryMenuItem::Separator),
-      _ => unimplemented!(),
     }
   }
+  tray_menu
 }

+ 94 - 8
core/tauri-runtime/src/lib.rs

@@ -35,6 +35,47 @@ pub trait MenuId: Serialize + Hash + Eq + Debug + Clone + Send + Sync + 'static
 
 impl<T> MenuId for T where T: Serialize + Hash + Eq + Debug + Clone + Send + Sync + 'static {}
 
+#[cfg(feature = "system-tray")]
+#[non_exhaustive]
+pub struct SystemTray<I: MenuId> {
+  pub icon: Option<Icon>,
+  pub menu: Option<menu::SystemTrayMenu<I>>,
+}
+
+#[cfg(feature = "system-tray")]
+impl<I: MenuId> Default for SystemTray<I> {
+  fn default() -> Self {
+    Self {
+      icon: None,
+      menu: None,
+    }
+  }
+}
+
+#[cfg(feature = "system-tray")]
+impl<I: MenuId> SystemTray<I> {
+  /// Creates a new system tray that only renders an icon.
+  pub fn new() -> Self {
+    Default::default()
+  }
+
+  pub fn menu(&self) -> Option<&menu::SystemTrayMenu<I>> {
+    self.menu.as_ref()
+  }
+
+  /// Sets the tray icon. Must be a [`Icon::File`] on Linux and a [`Icon::Raw`] on Windows and macOS.
+  pub fn with_icon(mut self, icon: Icon) -> Self {
+    self.icon.replace(icon);
+    self
+  }
+
+  /// Sets the menu to show when the system tray is right clicked.
+  pub fn with_menu(mut self, menu: menu::SystemTrayMenu<I>) -> Self {
+    self.menu.replace(menu);
+    self
+  }
+}
+
 #[derive(Debug, thiserror::Error)]
 #[non_exhaustive]
 pub enum Error {
@@ -99,9 +140,47 @@ pub enum Icon {
   Raw(Vec<u8>),
 }
 
+impl Icon {
+  /// Converts the icon to a the expected system tray format.
+  /// We expect the code that passes the Icon enum to have already checked the platform.
+  #[cfg(target_os = "linux")]
+  pub fn into_tray_icon(self) -> PathBuf {
+    match self {
+      Icon::File(path) => path,
+      Icon::Raw(_) => {
+        panic!("linux requires the system menu icon to be a file path, not bytes.")
+      }
+    }
+  }
+
+  /// Converts the icon to a the expected system tray format.
+  /// We expect the code that passes the Icon enum to have already checked the platform.
+  #[cfg(not(target_os = "linux"))]
+  pub fn into_tray_icon(self) -> Vec<u8> {
+    match self {
+      Icon::Raw(bytes) => bytes,
+      Icon::File(_) => {
+        panic!("non-linux system menu icons must be bytes, not a file path.")
+      }
+    }
+  }
+}
+
 /// A system tray event.
-pub struct SystemTrayEvent {
-  pub menu_item_id: u32,
+pub enum SystemTrayEvent {
+  MenuItemClick(u32),
+  LeftClick {
+    position: PhysicalPosition<f64>,
+    size: PhysicalSize<f64>,
+  },
+  RightClick {
+    position: PhysicalPosition<f64>,
+    size: PhysicalSize<f64>,
+  },
+  DoubleClick {
+    position: PhysicalPosition<f64>,
+    size: PhysicalSize<f64>,
+  },
 }
 
 /// Metadata for a runtime event loop iteration on `run_iteration`.
@@ -118,6 +197,10 @@ pub trait RuntimeHandle: Send + Sized + Clone + 'static {
     &self,
     pending: PendingWindow<P>,
   ) -> crate::Result<DetachedWindow<P>>;
+
+  #[cfg(all(windows, feature = "system-tray"))]
+  #[cfg_attr(doc_cfg, doc(cfg(all(windows, feature = "system-tray"))))]
+  fn remove_system_tray(&self) -> crate::Result<()>;
 }
 
 /// The webview runtime interface.
@@ -126,6 +209,9 @@ pub trait Runtime: Sized + 'static {
   type Dispatcher: Dispatch<Runtime = Self>;
   /// The runtime handle type.
   type Handle: RuntimeHandle<Runtime = Self>;
+  /// The tray handler type.
+  #[cfg(feature = "system-tray")]
+  type TrayHandler: menu::TrayHandle + Clone + Send;
 
   /// Creates a new webview runtime.
   fn new() -> crate::Result<Self>;
@@ -142,11 +228,7 @@ pub trait Runtime: Sized + 'static {
   /// Adds the icon to the system tray with the specified menu items.
   #[cfg(feature = "system-tray")]
   #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-  fn system_tray<I: MenuId>(
-    &self,
-    icon: Icon,
-    menu: Vec<menu::SystemTrayMenuItem<I>>,
-  ) -> crate::Result<()>;
+  fn system_tray<I: MenuId>(&self, system_tray: SystemTray<I>) -> crate::Result<Self::TrayHandler>;
 
   /// Registers a system tray event handler.
   #[cfg(feature = "system-tray")]
@@ -158,7 +240,7 @@ pub trait Runtime: Sized + 'static {
   fn run_iteration(&mut self) -> RunIteration;
 
   /// Run the webview runtime.
-  fn run(self);
+  fn run<F: Fn() + 'static>(self, callback: F);
 }
 
 /// Webview dispatcher. A thread-safe handle to the webview API.
@@ -306,4 +388,8 @@ pub trait Dispatch: Clone + Send + Sized + 'static {
 
   /// Executes javascript on the window this [`Dispatch`] represents.
   fn eval_script<S: Into<String>>(&self, script: S) -> crate::Result<()>;
+
+  /// Applies the specified `update` to the menu item associated with the given `id`.
+  #[cfg(feature = "menu")]
+  fn update_menu_item(&self, id: u32, update: menu::MenuUpdate) -> crate::Result<()>;
 }

+ 299 - 15
core/tauri-runtime/src/menu.rs

@@ -6,35 +6,245 @@ use std::{collections::hash_map::DefaultHasher, hash::Hasher};
 
 use super::MenuId;
 
+/// Named images defined by the system.
+#[cfg(target_os = "macos")]
+#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
+#[derive(Debug, Clone)]
+pub enum NativeImage {
+  /// An add item template image.
+  Add,
+  /// Advanced preferences toolbar icon for the preferences window.
+  Advanced,
+  /// A Bluetooth template image.
+  Bluetooth,
+  /// Bookmarks image suitable for a template.
+  Bookmarks,
+  /// A caution image.
+  Caution,
+  /// A color panel toolbar icon.
+  ColorPanel,
+  /// A column view mode template image.
+  ColumnView,
+  /// A computer icon.
+  Computer,
+  /// An enter full-screen mode template image.
+  EnterFullScreen,
+  /// Permissions for all users.
+  Everyone,
+  /// An exit full-screen mode template image.
+  ExitFullScreen,
+  /// A cover flow view mode template image.
+  FlowView,
+  /// A folder image.
+  Folder,
+  /// A burnable folder icon.
+  FolderBurnable,
+  /// A smart folder icon.
+  FolderSmart,
+  /// A link template image.
+  FollowLinkFreestanding,
+  /// A font panel toolbar icon.
+  FontPanel,
+  /// A `go back` template image.
+  GoLeft,
+  /// A `go forward` template image.
+  GoRight,
+  /// Home image suitable for a template.
+  Home,
+  /// An iChat Theater template image.
+  IChatTheater,
+  /// An icon view mode template image.
+  IconView,
+  /// An information toolbar icon.
+  Info,
+  /// A template image used to denote invalid data.
+  InvalidDataFreestanding,
+  /// A generic left-facing triangle template image.
+  LeftFacingTriangle,
+  /// A list view mode template image.
+  ListView,
+  /// A locked padlock template image.
+  LockLocked,
+  /// An unlocked padlock template image.
+  LockUnlocked,
+  /// A horizontal dash, for use in menus.
+  MenuMixedState,
+  /// A check mark template image, for use in menus.
+  MenuOnState,
+  /// A MobileMe icon.
+  MobileMe,
+  /// A drag image for multiple items.
+  MultipleDocuments,
+  /// A network icon.
+  Network,
+  /// A path button template image.
+  Path,
+  /// General preferences toolbar icon for the preferences window.
+  PreferencesGeneral,
+  /// A Quick Look template image.
+  QuickLook,
+  /// A refresh template image.
+  RefreshFreestanding,
+  /// A refresh template image.
+  Refresh,
+  /// A remove item template image.
+  Remove,
+  /// A reveal contents template image.
+  RevealFreestanding,
+  /// A generic right-facing triangle template image.
+  RightFacingTriangle,
+  /// A share view template image.
+  Share,
+  /// A slideshow template image.
+  Slideshow,
+  /// A badge for a `smart` item.
+  SmartBadge,
+  /// Small green indicator, similar to iChat’s available image.
+  StatusAvailable,
+  /// Small clear indicator.
+  StatusNone,
+  /// Small yellow indicator, similar to iChat’s idle image.
+  StatusPartiallyAvailable,
+  /// Small red indicator, similar to iChat’s unavailable image.
+  StatusUnavailable,
+  /// A stop progress template image.
+  StopProgressFreestanding,
+  /// A stop progress button template image.
+  StopProgress,
+
+  /// An image of the empty trash can.
+  TrashEmpty,
+  /// An image of the full trash can.
+  TrashFull,
+  /// Permissions for a single user.
+  User,
+  /// User account toolbar icon for the preferences window.
+  UserAccounts,
+  /// Permissions for a group of users.
+  UserGroup,
+  /// Permissions for guests.
+  UserGuest,
+}
+
+#[derive(Debug, Clone)]
+pub enum MenuUpdate {
+  /// Modifies the enabled state of the menu item.
+  SetEnabled(bool),
+  /// Modifies the title (label) of the menu item.
+  SetTitle(String),
+  /// Modifies the selected state of the menu item.
+  SetSelected(bool),
+  /// Update native image.
+  #[cfg(target_os = "macos")]
+  #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
+  SetNativeImage(NativeImage),
+}
+
+pub trait TrayHandle {
+  fn set_icon(&self, icon: crate::Icon) -> crate::Result<()>;
+  fn update_item(&self, id: u32, update: MenuUpdate) -> crate::Result<()>;
+}
+
 /// A window menu.
 #[derive(Debug, Clone)]
+#[non_exhaustive]
 pub struct Menu<I: MenuId> {
+  pub items: Vec<MenuEntry<I>>,
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct Submenu<I: MenuId> {
   pub title: String,
-  pub items: Vec<MenuItem<I>>,
+  pub enabled: bool,
+  pub inner: Menu<I>,
 }
 
-impl<I: MenuId> Menu<I> {
-  /// Creates a new window menu with the given title and items.
-  pub fn new<T: Into<String>>(title: T, items: Vec<MenuItem<I>>) -> Self {
+impl<I: MenuId> Submenu<I> {
+  /// Creates a new submenu with the given title and menu items.
+  pub fn new<S: Into<String>>(title: S, menu: Menu<I>) -> Self {
     Self {
       title: title.into(),
-      items,
+      enabled: true,
+      inner: menu,
     }
   }
 }
 
+impl<I: MenuId> Default for Menu<I> {
+  fn default() -> Self {
+    Self { items: Vec::new() }
+  }
+}
+
+impl<I: MenuId> Menu<I> {
+  /// Creates a new window menu.
+  pub fn new() -> Self {
+    Default::default()
+  }
+
+  /// Adds the custom menu item to the menu.
+  pub fn add_item(mut self, item: CustomMenuItem<I>) -> Self {
+    self.items.push(MenuEntry::CustomItem(item));
+    self
+  }
+
+  /// Adds a native item to the menu.
+  pub fn add_native_item(mut self, item: MenuItem) -> Self {
+    self.items.push(MenuEntry::NativeItem(item));
+    self
+  }
+
+  /// Adds an entry with submenu.
+  pub fn add_submenu(mut self, submenu: Submenu<I>) -> Self {
+    self.items.push(MenuEntry::Submenu(submenu));
+    self
+  }
+}
+
 /// A custom menu item.
 #[derive(Debug, Clone)]
+#[non_exhaustive]
 pub struct CustomMenuItem<I: MenuId> {
   pub id: I,
-  pub name: String,
+  pub title: String,
+  pub keyboard_accelerator: Option<String>,
+  pub enabled: bool,
+  pub selected: bool,
+  #[cfg(target_os = "macos")]
+  pub native_image: Option<NativeImage>,
 }
 
 impl<I: MenuId> CustomMenuItem<I> {
   /// Create new custom menu item.
   pub fn new<T: Into<String>>(id: I, title: T) -> Self {
-    let title = title.into();
-    Self { id, name: title }
+    Self {
+      id,
+      title: title.into(),
+      keyboard_accelerator: None,
+      enabled: true,
+      selected: false,
+      #[cfg(target_os = "macos")]
+      native_image: None,
+    }
+  }
+
+  #[cfg(target_os = "macos")]
+  pub fn native_image(mut self, image: NativeImage) -> Self {
+    self.native_image.replace(image);
+    self
+  }
+
+  /// Mark the item as disabled.
+  pub fn disabled(mut self) -> Self {
+    self.enabled = false;
+    self
+  }
+
+  /// Mark the item as selected.
+  pub fn selected(mut self) -> Self {
+    self.selected = true;
+    self
   }
 
   #[doc(hidden)]
@@ -45,25 +255,99 @@ impl<I: MenuId> CustomMenuItem<I> {
   }
 }
 
+/// A system tray menu.
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct SystemTrayMenu<I: MenuId> {
+  pub items: Vec<SystemTrayMenuEntry<I>>,
+}
+
+impl<I: MenuId> Default for SystemTrayMenu<I> {
+  fn default() -> Self {
+    Self { items: Vec::new() }
+  }
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub struct SystemTraySubmenu<I: MenuId> {
+  pub title: String,
+  pub enabled: bool,
+  pub inner: SystemTrayMenu<I>,
+}
+
+impl<I: MenuId> SystemTraySubmenu<I> {
+  /// Creates a new submenu with the given title and menu items.
+  pub fn new<S: Into<String>>(title: S, menu: SystemTrayMenu<I>) -> Self {
+    Self {
+      title: title.into(),
+      enabled: true,
+      inner: menu,
+    }
+  }
+}
+
+impl<I: MenuId> SystemTrayMenu<I> {
+  /// Creates a new system tray menu.
+  pub fn new() -> Self {
+    Default::default()
+  }
+
+  /// Adds the custom menu item to the system tray menu.
+  pub fn add_item(mut self, item: CustomMenuItem<I>) -> Self {
+    self.items.push(SystemTrayMenuEntry::CustomItem(item));
+    self
+  }
+
+  /// Adds a native item to the system tray menu.
+  pub fn add_native_item(mut self, item: SystemTrayMenuItem) -> Self {
+    self.items.push(SystemTrayMenuEntry::NativeItem(item));
+    self
+  }
+
+  /// Adds an entry with submenu.
+  pub fn add_submenu(mut self, submenu: SystemTraySubmenu<I>) -> Self {
+    self.items.push(SystemTrayMenuEntry::Submenu(submenu));
+    self
+  }
+}
+
+/// An entry on the system tray menu.
+#[derive(Debug, Clone)]
+pub enum SystemTrayMenuEntry<I: MenuId> {
+  /// A custom item.
+  CustomItem(CustomMenuItem<I>),
+  /// A native item.
+  NativeItem(SystemTrayMenuItem),
+  /// An entry with submenu.
+  Submenu(SystemTraySubmenu<I>),
+}
+
 /// System tray menu item.
 #[derive(Debug, Clone)]
 #[non_exhaustive]
-pub enum SystemTrayMenuItem<I: MenuId> {
-  /// A custom menu item.
-  Custom(CustomMenuItem<I>),
+pub enum SystemTrayMenuItem {
   /// A separator.
   Separator,
 }
 
+/// An entry on the system tray menu.
+#[derive(Debug, Clone)]
+pub enum MenuEntry<I: MenuId> {
+  /// A custom item.
+  CustomItem(CustomMenuItem<I>),
+  /// A native item.
+  NativeItem(MenuItem),
+  /// An entry with submenu.
+  Submenu(Submenu<I>),
+}
+
 /// A menu item, bound to a pre-defined action or `Custom` emit an event. Note that status bar only
 /// supports `Custom` menu item variants. And on the menu bar, some platforms might not support some
 /// of the variants. Unsupported variant will be no-op on such platform.
 #[derive(Debug, Clone)]
 #[non_exhaustive]
-pub enum MenuItem<I: MenuId> {
-  /// A custom menu item..
-  Custom(CustomMenuItem<I>),
-
+pub enum MenuItem {
   /// Shows a standard "About" item
   ///
   /// ## Platform-specific

+ 1 - 1
core/tauri-runtime/src/webview.rs

@@ -101,7 +101,7 @@ pub trait WindowBuilder: WindowBuilderBase {
   /// Sets the menu for the window.
   #[cfg(feature = "menu")]
   #[cfg_attr(doc_cfg, doc(cfg(feature = "menu")))]
-  fn menu<I: MenuId>(self, menu: Vec<Menu<I>>) -> Self;
+  fn menu<I: MenuId>(self, menu: Menu<I>) -> Self;
 
   /// The initial position of the window's.
   fn position(self, x: f64, y: f64) -> Self;

+ 8 - 8
core/tauri-runtime/src/window/dpi.rs

@@ -68,7 +68,7 @@ impl Pixel for f64 {
 }
 
 /// A position represented in physical pixels.
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash, Serialize, Deserialize)]
 pub struct PhysicalPosition<P> {
   /// Vertical axis value.
   pub x: P,
@@ -79,7 +79,7 @@ pub struct PhysicalPosition<P> {
 impl<P: Pixel> PhysicalPosition<P> {
   /// Converts the physical position to a logical one, using the scale factor.
   #[inline]
-  pub fn to_logical<X: Pixel>(&self, scale_factor: f64) -> LogicalPosition<X> {
+  pub fn to_logical<X: Pixel>(self, scale_factor: f64) -> LogicalPosition<X> {
     assert!(validate_scale_factor(scale_factor));
     let x = self.x.into() / scale_factor;
     let y = self.y.into() / scale_factor;
@@ -88,7 +88,7 @@ impl<P: Pixel> PhysicalPosition<P> {
 }
 
 /// A position represented in logical pixels.
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash, Serialize, Deserialize)]
 pub struct LogicalPosition<P> {
   /// Vertical axis value.
   pub x: P,
@@ -108,7 +108,7 @@ impl<T: Pixel> LogicalPosition<T> {
 }
 
 /// A position that's either physical or logical.
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
 #[serde(tag = "type", content = "data")]
 pub enum Position {
   /// Physical position.
@@ -118,7 +118,7 @@ pub enum Position {
 }
 
 /// A size represented in physical pixels.
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash, Serialize, Deserialize)]
 pub struct PhysicalSize<T> {
   /// Width.
   pub width: T,
@@ -129,7 +129,7 @@ pub struct PhysicalSize<T> {
 impl<T: Pixel> PhysicalSize<T> {
   /// Converts the physical size to a logical one, applying the scale factor.
   #[inline]
-  pub fn to_logical<X: Pixel>(&self, scale_factor: f64) -> LogicalSize<X> {
+  pub fn to_logical<X: Pixel>(self, scale_factor: f64) -> LogicalSize<X> {
     assert!(validate_scale_factor(scale_factor));
     let width = self.width.into() / scale_factor;
     let height = self.height.into() / scale_factor;
@@ -138,7 +138,7 @@ impl<T: Pixel> PhysicalSize<T> {
 }
 
 /// A size represented in logical pixels.
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash, Serialize, Deserialize)]
 pub struct LogicalSize<T> {
   /// Width.
   pub width: T,
@@ -158,7 +158,7 @@ impl<T: Pixel> LogicalSize<T> {
 }
 
 /// A size that's either physical or logical.
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
 #[serde(tag = "type", content = "data")]
 pub enum Size {
   /// Physical size.

+ 123 - 83
core/tauri/src/app.rs

@@ -2,6 +2,10 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
+#[cfg(feature = "system-tray")]
+#[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
+pub(crate) mod tray;
+
 use crate::{
   api::assets::Assets,
   api::config::WindowUrl,
@@ -22,8 +26,11 @@ use std::{collections::HashMap, sync::Arc};
 
 #[cfg(feature = "menu")]
 use crate::runtime::menu::Menu;
+
+#[cfg(all(windows, feature = "system-tray"))]
+use crate::runtime::RuntimeHandle;
 #[cfg(feature = "system-tray")]
-use crate::runtime::{menu::SystemTrayMenuItem, Icon};
+use crate::runtime::{Icon, SystemTrayEvent as RuntimeSystemTrayEvent};
 
 #[cfg(feature = "updater")]
 use crate::updater;
@@ -33,22 +40,7 @@ pub(crate) type GlobalMenuEventListener<P> = Box<dyn Fn(WindowMenuEvent<P>) + Se
 pub(crate) type GlobalWindowEventListener<P> = Box<dyn Fn(GlobalWindowEvent<P>) + Send + Sync>;
 #[cfg(feature = "system-tray")]
 type SystemTrayEventListener<P> =
-  Box<dyn Fn(&AppHandle<P>, SystemTrayEvent<<P as Params>::SystemTrayMenuId>) + Send + Sync>;
-
-/// System tray event.
-#[cfg(feature = "system-tray")]
-#[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-pub struct SystemTrayEvent<I: MenuId> {
-  menu_item_id: I,
-}
-
-#[cfg(feature = "system-tray")]
-impl<I: MenuId> SystemTrayEvent<I> {
-  /// The menu item id.
-  pub fn menu_item_id(&self) -> &I {
-    &self.menu_item_id
-  }
-}
+  Box<dyn Fn(&AppHandle<P>, tray::SystemTrayEvent<<P as Params>::SystemTrayMenuId>) + Send + Sync>;
 
 crate::manager::default_args! {
   /// A menu event that was triggered on a window.
@@ -100,6 +92,8 @@ crate::manager::default_args! {
   pub struct AppHandle<P: Params> {
     runtime_handle: <P::Runtime as Runtime>::Handle,
     manager: WindowManager<P>,
+    #[cfg(feature = "system-tray")]
+    tray_handle: Option<tray::SystemTrayHandle<P>>,
   }
 }
 
@@ -108,10 +102,21 @@ impl<P: Params> Clone for AppHandle<P> {
     Self {
       runtime_handle: self.runtime_handle.clone(),
       manager: self.manager.clone(),
+      #[cfg(feature = "system-tray")]
+      tray_handle: self.tray_handle.clone(),
     }
   }
 }
 
+impl<P: Params> AppHandle<P> {
+  /// 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)
+  }
+}
+
 impl<P: Params> Manager<P> for AppHandle<P> {}
 impl<P: Params> ManagerBase<P> for AppHandle<P> {
   fn manager(&self) -> &WindowManager<P> {
@@ -130,19 +135,9 @@ crate::manager::default_args! {
   pub struct App<P: Params> {
     runtime: Option<P::Runtime>,
     manager: WindowManager<P>,
-    #[cfg(shell_execute)]
-    cleanup_on_drop: bool,
-  }
-}
-
-impl<P: Params> Drop for App<P> {
-  fn drop(&mut self) {
-    #[cfg(shell_execute)]
-    {
-      if self.cleanup_on_drop {
-        crate::api::process::kill_children();
-      }
-    }
+    #[cfg(feature = "system-tray")]
+    tray_handle: Option<tray::SystemTrayHandle<P>>,
+    handle: AppHandle<P>,
   }
 }
 
@@ -182,6 +177,16 @@ macro_rules! shared_app_impl {
         ))?;
         Ok(())
       }
+
+      #[cfg(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<P> {
+        self
+          .tray_handle
+          .clone()
+          .expect("tray not configured; use the `Builder#system_tray` API first.")
+      }
     }
   };
 }
@@ -192,14 +197,15 @@ shared_app_impl!(AppHandle<P>);
 impl<P: Params> App<P> {
   /// Gets a handle to the application instance.
   pub fn handle(&self) -> AppHandle<P> {
-    AppHandle {
-      runtime_handle: self.runtime.as_ref().unwrap().handle(),
-      manager: self.manager.clone(),
-    }
+    self.handle.clone()
   }
 
   /// Runs a iteration of the runtime event loop and immediately return.
   ///
+  /// Note that when using this API, app cleanup is not automatically done.
+  /// The cleanup calls [`crate::api::process::kill_children`] so you may want to call that function before exiting the application.
+  /// Additionally, the cleanup calls [AppHandle#remove_system_tray](`AppHandle#method.remove_system_tray`) (Windows only).
+  ///
   /// # Example
   /// ```rust,ignore
   /// fn main() {
@@ -312,7 +318,7 @@ where
 
   /// The menu set to all windows.
   #[cfg(feature = "menu")]
-  menu: Vec<Menu<MID>>,
+  menu: Option<Menu<MID>>,
 
   /// Menu event handlers that listens to all windows.
   #[cfg(feature = "menu")]
@@ -321,16 +327,13 @@ where
   /// Window event handlers that listens to all windows.
   window_event_listeners: Vec<GlobalWindowEventListener<Args<E, L, MID, TID, A, R>>>,
 
-  /// The app system tray menu items.
+  /// The app system tray.
   #[cfg(feature = "system-tray")]
-  system_tray: Vec<SystemTrayMenuItem<TID>>,
+  system_tray: Option<tray::SystemTray<TID>>,
 
   /// System tray event handlers.
   #[cfg(feature = "system-tray")]
   system_tray_event_listeners: Vec<SystemTrayEventListener<Args<E, L, MID, TID, A, R>>>,
-
-  #[cfg(shell_execute)]
-  cleanup_on_drop: bool,
 }
 
 impl<E, L, MID, TID, A, R> Builder<E, L, MID, TID, A, R>
@@ -353,16 +356,14 @@ where
       uri_scheme_protocols: Default::default(),
       state: StateManager::new(),
       #[cfg(feature = "menu")]
-      menu: Vec::new(),
+      menu: None,
       #[cfg(feature = "menu")]
       menu_event_listeners: Vec::new(),
       window_event_listeners: Vec::new(),
       #[cfg(feature = "system-tray")]
-      system_tray: Vec::new(),
+      system_tray: None,
       #[cfg(feature = "system-tray")]
       system_tray_event_listeners: Vec::new(),
-      #[cfg(shell_execute)]
-      cleanup_on_drop: true,
     }
   }
 
@@ -514,16 +515,16 @@ where
   /// Adds the icon configured on `tauri.conf.json` to the system tray with the specified menu items.
   #[cfg(feature = "system-tray")]
   #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-  pub fn system_tray(mut self, items: Vec<SystemTrayMenuItem<TID>>) -> Self {
-    self.system_tray = items;
+  pub fn system_tray(mut self, system_tray: tray::SystemTray<TID>) -> Self {
+    self.system_tray.replace(system_tray);
     self
   }
 
   /// Sets the menu to use on all windows.
   #[cfg(feature = "menu")]
   #[cfg_attr(doc_cfg, doc(cfg(feature = "menu")))]
-  pub fn menu(mut self, menu: Vec<Menu<MID>>) -> Self {
-    self.menu = menu;
+  pub fn menu(mut self, menu: Menu<MID>) -> Self {
+    self.menu.replace(menu);
     self
   }
 
@@ -555,7 +556,7 @@ where
   #[cfg(feature = "system-tray")]
   #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
   pub fn on_system_tray_event<
-    F: Fn(&AppHandle<Args<E, L, MID, TID, A, R>>, SystemTrayEvent<TID>) + Send + Sync + 'static,
+    F: Fn(&AppHandle<Args<E, L, MID, TID, A, R>>, tray::SystemTrayEvent<TID>) + Send + Sync + 'static,
   >(
     mut self,
     handler: F,
@@ -590,15 +591,6 @@ where
     self
   }
 
-  /// Skips Tauri cleanup on [`App`] drop. Useful if your application has multiple [`App`] instances.
-  ///
-  /// The cleanup calls [`crate::api::process::kill_children`] so you may want to call that function before exiting the application.
-  #[cfg(shell_execute)]
-  pub fn skip_cleanup_on_drop(mut self) -> Self {
-    self.cleanup_on_drop = false;
-    self
-  }
-
   /// Builds the application.
   #[allow(clippy::type_complexity)]
   pub fn build(mut self, context: Context<A>) -> crate::Result<App<Args<E, L, MID, TID, A, R>>> {
@@ -606,8 +598,8 @@ where
     let system_tray_icon = {
       let icon = context.system_tray_icon.clone();
 
-      // check the icon format if the system tray is supposed to be ran
-      if !self.system_tray.is_empty() {
+      // check the icon format if the system tray is configured
+      if self.system_tray.is_some() {
         use std::io::{Error, ErrorKind};
         #[cfg(target_os = "linux")]
         if let Some(Icon::Raw(_)) = icon {
@@ -656,11 +648,20 @@ where
       ));
     }
 
+    let runtime = R::new()?;
+    let runtime_handle = runtime.handle();
+
     let mut app = App {
-      runtime: Some(R::new()?),
-      manager,
-      #[cfg(shell_execute)]
-      cleanup_on_drop: self.cleanup_on_drop,
+      runtime: Some(runtime),
+      manager: manager.clone(),
+      #[cfg(feature = "system-tray")]
+      tray_handle: None,
+      handle: AppHandle {
+        runtime_handle,
+        manager,
+        #[cfg(feature = "system-tray")]
+        tray_handle: None,
+      },
     };
 
     app.manager.initialize_plugins(&app)?;
@@ -690,17 +691,34 @@ where
     (self.setup)(&mut app).map_err(|e| crate::Error::Setup(e))?;
 
     #[cfg(feature = "system-tray")]
-    if !self.system_tray.is_empty() {
-      let ids = get_menu_ids(&self.system_tray);
-      app
+    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 mut tray = tray::SystemTray::new();
+      if let Some(menu) = system_tray.menu {
+        tray = tray.with_menu(menu);
+      }
+      let tray_handler = app
         .runtime
         .as_ref()
         .unwrap()
         .system_tray(
-          system_tray_icon.expect("tray icon not found; please configure it on tauri.conf.json"),
-          self.system_tray,
+          tray.with_icon(
+            system_tray
+              .icon
+              .or(system_tray_icon)
+              .expect("tray icon not found; please configure it on tauri.conf.json"),
+          ),
         )
         .expect("failed to run tray");
+      let tray_handle = tray::SystemTrayHandle {
+        ids: Arc::new(ids.clone()),
+        inner: tray_handler,
+      };
+      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();
@@ -711,10 +729,32 @@ where
           .unwrap()
           .on_system_tray_event(move |event| {
             let app_handle = app_handle.clone();
-            let menu_item_id = ids.get(&event.menu_item_id).unwrap().clone();
+            let event = match event {
+              RuntimeSystemTrayEvent::MenuItemClick(id) => tray::SystemTrayEvent::MenuItemClick {
+                id: ids.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();
             crate::async_runtime::spawn(async move {
-              listener.lock().unwrap()(&app_handle, SystemTrayEvent { menu_item_id });
+              listener.lock().unwrap()(&app_handle, event);
             });
           });
       }
@@ -726,22 +766,22 @@ where
   /// Runs the configured Tauri application.
   pub fn run(self, context: Context<A>) -> crate::Result<()> {
     let mut app = self.build(context)?;
-    app.runtime.take().unwrap().run();
+    #[cfg(all(windows, feature = "system-tray"))]
+    let app_handle = app.handle();
+    app.runtime.take().unwrap().run(move || {
+      #[cfg(shell_execute)]
+      {
+        crate::api::process::kill_children();
+      }
+      #[cfg(all(windows, feature = "system-tray"))]
+      {
+        let _ = app_handle.remove_system_tray();
+      }
+    });
     Ok(())
   }
 }
 
-#[cfg(feature = "system-tray")]
-fn get_menu_ids<I: MenuId>(items: &[SystemTrayMenuItem<I>]) -> HashMap<u32, I> {
-  let mut map = HashMap::new();
-  for item in items {
-    if let SystemTrayMenuItem::Custom(i) = item {
-      map.insert(i.id_value(), i.id.clone());
-    }
-  }
-  map
-}
-
 /// Make `Wry` the default `Runtime` for `Builder`
 #[cfg(feature = "wry")]
 impl<A: Assets> Default for Builder<String, String, String, String, A, crate::Wry> {

+ 164 - 0
core/tauri/src/app/tray.rs

@@ -0,0 +1,164 @@
+// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+pub use crate::{
+  runtime::{
+    menu::{MenuUpdate, SystemTrayMenu, SystemTrayMenuEntry, TrayHandle},
+    window::dpi::{PhysicalPosition, PhysicalSize},
+    Icon, MenuId, Runtime, SystemTray,
+  },
+  Params,
+};
+
+use std::{collections::HashMap, sync::Arc};
+
+pub(crate) fn get_menu_ids<I: MenuId>(map: &mut HashMap<u32, I>, menu: &SystemTrayMenu<I>) {
+  for item in &menu.items {
+    match item {
+      SystemTrayMenuEntry::CustomItem(c) => {
+        map.insert(c.id_value(), c.id.clone());
+      }
+      SystemTrayMenuEntry::Submenu(s) => get_menu_ids(map, &s.inner),
+      _ => {}
+    }
+  }
+}
+
+/// System tray event.
+#[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
+#[non_exhaustive]
+pub enum SystemTrayEvent<I: MenuId> {
+  /// Tray context menu item was clicked.
+  #[non_exhaustive]
+  MenuItemClick {
+    /// The id of the menu item.
+    id: I,
+  },
+  /// Tray icon received a left click.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Linux:** Unsupported
+  #[non_exhaustive]
+  LeftClick {
+    /// The position of the tray icon.
+    position: PhysicalPosition<f64>,
+    /// The size of the tray icon.
+    size: PhysicalSize<f64>,
+  },
+  /// Tray icon received a right click.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Linux:** Unsupported
+  /// - **macOS:** `Ctrl` + `Left click` fire this event.
+  #[non_exhaustive]
+  RightClick {
+    /// The position of the tray icon.
+    position: PhysicalPosition<f64>,
+    /// The size of the tray icon.
+    size: PhysicalSize<f64>,
+  },
+  /// Fired when a menu item receive a `Double click`
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **macOS / Linux:** Unsupported
+  ///
+  #[non_exhaustive]
+  DoubleClick {
+    /// The position of the tray icon.
+    position: PhysicalPosition<f64>,
+    /// The size of the tray icon.
+    size: PhysicalSize<f64>,
+  },
+}
+
+crate::manager::default_args! {
+  /// A handle to a system tray. Allows updating the context menu items.
+  pub struct SystemTrayHandle<P: Params> {
+    pub(crate) ids: Arc<HashMap<u32, P::SystemTrayMenuId>>,
+    pub(crate) inner: <P::Runtime as Runtime>::TrayHandler,
+  }
+}
+
+impl<P: Params> Clone for SystemTrayHandle<P> {
+  fn clone(&self) -> Self {
+    Self {
+      ids: self.ids.clone(),
+      inner: self.inner.clone(),
+    }
+  }
+}
+
+crate::manager::default_args! {
+  /// A handle to a system tray menu item.
+  pub struct SystemTrayMenuItemHandle<P: Params> {
+    id: u32,
+    tray_handler: <P::Runtime as Runtime>::TrayHandler,
+  }
+}
+
+impl<P: Params> Clone for SystemTrayMenuItemHandle<P> {
+  fn clone(&self) -> Self {
+    Self {
+      id: self.id,
+      tray_handler: self.tray_handler.clone(),
+    }
+  }
+}
+
+impl<P: Params> SystemTrayHandle<P> {
+  pub fn get_item(&self, id: &P::SystemTrayMenuId) -> SystemTrayMenuItemHandle<P> {
+    for (raw, item_id) in self.ids.iter() {
+      if item_id == id {
+        return SystemTrayMenuItemHandle {
+          id: *raw,
+          tray_handler: self.inner.clone(),
+        };
+      }
+    }
+    panic!("item id not found")
+  }
+
+  /// Updates the tray icon. Must be a [`Icon::File`] on Linux and a [`Icon::Raw`] on Windows and macOS.
+  pub fn set_icon(&self, icon: Icon) -> crate::Result<()> {
+    self.inner.set_icon(icon).map_err(Into::into)
+  }
+}
+
+impl<P: Params> SystemTrayMenuItemHandle<P> {
+  /// Modifies the enabled state of the menu item.
+  pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
+    self
+      .tray_handler
+      .update_item(self.id, MenuUpdate::SetEnabled(enabled))
+      .map_err(Into::into)
+  }
+
+  /// Modifies the title (label) of the menu item.
+  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)
+  }
+
+  /// Modifies the selected state of the menu item.
+  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)
+  }
+}

+ 17 - 3
core/tauri/src/lib.rs

@@ -65,6 +65,14 @@ use std::{borrow::Borrow, collections::HashMap, sync::Arc};
 #[cfg(any(feature = "menu", feature = "system-tray"))]
 #[cfg_attr(doc_cfg, doc(cfg(any(feature = "menu", feature = "system-tray"))))]
 pub use runtime::menu::CustomMenuItem;
+
+#[cfg(all(target_os = "macos", any(feature = "menu", feature = "system-tray")))]
+#[cfg_attr(
+  doc_cfg,
+  doc(cfg(all(target_os = "macos", any(feature = "menu", feature = "system-tray"))))
+)]
+pub use runtime::menu::NativeImage;
+
 pub use {
   self::api::assets::Assets,
   self::api::{
@@ -90,13 +98,19 @@ pub use {
 };
 #[cfg(feature = "system-tray")]
 #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-pub use {self::app::SystemTrayEvent, self::runtime::menu::SystemTrayMenuItem};
+pub use {
+  self::app::tray::SystemTrayEvent,
+  self::runtime::{
+    menu::{SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu},
+    SystemTray,
+  },
+};
 #[cfg(feature = "menu")]
 #[cfg_attr(doc_cfg, doc(cfg(feature = "menu")))]
 pub use {
   self::app::WindowMenuEvent,
-  self::runtime::menu::{Menu, MenuItem},
-  self::window::MenuEvent,
+  self::runtime::menu::{Menu, MenuItem, Submenu},
+  self::window::menu::MenuEvent,
 };
 
 /// Reads the config file at compile time and generates a [`Context`] based on its content.

+ 21 - 13
core/tauri/src/manager.rs

@@ -34,7 +34,7 @@ use crate::app::{GlobalMenuEventListener, WindowMenuEvent};
 
 #[cfg(feature = "menu")]
 use crate::{
-  runtime::menu::{Menu, MenuItem},
+  runtime::menu::{Menu, MenuEntry},
   MenuEvent,
 };
 
@@ -98,7 +98,7 @@ crate::manager::default_args! {
     uri_scheme_protocols: HashMap<String, Arc<CustomProtocol>>,
     /// The menu set to all windows.
     #[cfg(feature = "menu")]
-    menu: Vec<Menu<P::MenuId>>,
+    menu: Option<Menu<P::MenuId>>,
     /// Maps runtime id to a strongly typed menu id.
     #[cfg(feature = "menu")]
     menu_ids: HashMap<u32, P::MenuId>,
@@ -209,16 +209,16 @@ impl<P: Params> Clone for WindowManager<P> {
 }
 
 #[cfg(feature = "menu")]
-fn get_menu_ids<I: MenuId>(menu: &[Menu<I>]) -> HashMap<u32, I> {
-  let mut map = HashMap::new();
-  for m in menu {
-    for item in &m.items {
-      if let MenuItem::Custom(i) = item {
-        map.insert(i.id_value(), i.id.clone());
+fn get_menu_ids<I: MenuId>(map: &mut HashMap<u32, I>, menu: &Menu<I>) {
+  for item in &menu.items {
+    match item {
+      MenuEntry::CustomItem(c) => {
+        map.insert(c.id_value(), c.id.clone());
       }
+      MenuEntry::Submenu(s) => get_menu_ids(map, &s.inner),
+      _ => {}
     }
   }
-  map
 }
 
 impl<P: Params> WindowManager<P> {
@@ -232,7 +232,7 @@ impl<P: Params> WindowManager<P> {
     state: StateManager,
     window_event_listeners: Vec<GlobalWindowEventListener<P>>,
     #[cfg(feature = "menu")] (menu, menu_event_listeners): (
-      Vec<Menu<P::MenuId>>,
+      Option<Menu<P::MenuId>>,
       Vec<GlobalMenuEventListener<P>>,
     ),
   ) -> Self {
@@ -251,7 +251,13 @@ impl<P: Params> WindowManager<P> {
         package_info: context.package_info,
         uri_scheme_protocols,
         #[cfg(feature = "menu")]
-        menu_ids: get_menu_ids(&menu),
+        menu_ids: {
+          let mut map = HashMap::new();
+          if let Some(menu) = &menu {
+            get_menu_ids(&mut map, menu)
+          }
+          map
+        },
         #[cfg(feature = "menu")]
         menu,
         #[cfg(feature = "menu")]
@@ -329,7 +335,9 @@ impl<P: Params> WindowManager<P> {
 
     #[cfg(feature = "menu")]
     if !pending.window_builder.has_menu() {
-      pending.window_builder = pending.window_builder.menu(self.inner.menu.clone());
+      if let Some(menu) = &self.inner.menu {
+        pending.window_builder = pending.window_builder.menu(menu.clone());
+      }
     }
 
     for (uri_scheme, protocol) in &self.inner.uri_scheme_protocols {
@@ -822,7 +830,7 @@ fn on_window_event<P: Params>(window: &Window<P>, event: &WindowEvent) -> crate:
         .unwrap_or_else(|_| panic!("unhandled event")),
       Some(ScaleFactorChanged {
         scale_factor: *scale_factor,
-        size: new_inner_size.clone(),
+        size: *new_inner_size,
       }),
     )?,
     _ => unimplemented!(),

+ 5 - 5
core/tauri/src/updater/core.rs

@@ -1020,7 +1020,7 @@ mod test {
       .prefix("tauri_updater_test")
       .tempdir_in(parent_path);
 
-    assert_eq!(tmp_dir.is_ok(), true);
+    assert!(tmp_dir.is_ok());
     let tmp_dir_unwrap = tmp_dir.expect("Can't find tmp_dir");
     let tmp_dir_path = tmp_dir_unwrap.path();
 
@@ -1035,24 +1035,24 @@ mod test {
       .build());
 
     // make sure the process worked
-    assert_eq!(check_update.is_ok(), true);
+    assert!(check_update.is_ok());
 
     // unwrap our results
     let updater = check_update.expect("Can't check remote update");
 
     // make sure we need to update
-    assert_eq!(updater.should_update, true);
+    assert!(updater.should_update);
     // make sure we can read announced version
     assert_eq!(updater.version, "2.0.1");
 
     // download, install and validate signature
     let install_process = block!(updater.download_and_install(Some(pubkey)));
-    assert_eq!(install_process.is_ok(), true);
+    assert!(install_process.is_ok());
 
     // make sure the extraction went well (it should have skipped the main app.app folder)
     // as we can't extract in /Applications directly
     let bin_file = tmp_dir_path.join("Contents").join("MacOS").join("app");
     let bin_file_exist = Path::new(&bin_file).exists();
-    assert_eq!(bin_file_exist, true);
+    assert!(bin_file_exist);
   }
 }

+ 14 - 19
core/tauri/src/window.rs

@@ -3,7 +3,9 @@
 // SPDX-License-Identifier: MIT
 
 #[cfg(feature = "menu")]
-use crate::runtime::MenuId;
+#[cfg_attr(doc_cfg, doc(cfg(feature = "menu")))]
+pub(crate) mod menu;
+
 use crate::{
   api::config::WindowUrl,
   command::{CommandArg, CommandItem},
@@ -31,22 +33,6 @@ use std::{
   hash::{Hash, Hasher},
 };
 
-/// The window menu event.
-#[cfg(feature = "menu")]
-#[cfg_attr(doc_cfg, doc(cfg(feature = "menu")))]
-#[derive(Debug, Clone)]
-pub struct MenuEvent<I: MenuId> {
-  pub(crate) menu_item_id: I,
-}
-
-#[cfg(feature = "menu")]
-impl<I: MenuId> MenuEvent<I> {
-  /// The menu item id.
-  pub fn menu_item_id(&self) -> &I {
-    &self.menu_item_id
-  }
-}
-
 /// Monitor descriptor.
 #[derive(Debug, Clone, Serialize)]
 #[serde(rename_all = "camelCase")]
@@ -301,10 +287,10 @@ impl<P: Params> Window<P> {
   /// Registers a menu event listener.
   #[cfg(feature = "menu")]
   #[cfg_attr(doc_cfg, doc(cfg(feature = "menu")))]
-  pub fn on_menu_event<F: Fn(MenuEvent<P::MenuId>) + Send + 'static>(&self, f: F) {
+  pub fn on_menu_event<F: Fn(menu::MenuEvent<P::MenuId>) + Send + 'static>(&self, f: F) {
     let menu_ids = self.manager.menu_ids();
     self.window.dispatcher.on_menu_event(move |event| {
-      f(MenuEvent {
+      f(menu::MenuEvent {
         menu_item_id: menu_ids.get(&event.menu_item_id).unwrap().clone(),
       })
     });
@@ -312,6 +298,15 @@ impl<P: Params> Window<P> {
 
   // Getters
 
+  /// Gets a handle to the window menu.
+  #[cfg(feature = "menu")]
+  pub fn menu_handle(&self) -> menu::MenuHandle<P> {
+    menu::MenuHandle {
+      ids: self.manager.menu_ids(),
+      dispatcher: self.dispatcher(),
+    }
+  }
+
   /// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa.
   pub fn scale_factor(&self) -> crate::Result<f64> {
     self.window.dispatcher.scale_factor().map_err(Into::into)

+ 108 - 0
core/tauri/src/window/menu.rs

@@ -0,0 +1,108 @@
+// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::{
+  runtime::{menu::MenuUpdate, Dispatch, MenuId, Runtime},
+  Params,
+};
+
+use std::collections::HashMap;
+
+/// The window menu event.
+#[cfg_attr(doc_cfg, doc(cfg(feature = "menu")))]
+#[derive(Debug, Clone)]
+pub struct MenuEvent<I: MenuId> {
+  pub(crate) menu_item_id: I,
+}
+
+#[cfg(feature = "menu")]
+impl<I: MenuId> MenuEvent<I> {
+  /// The menu item id.
+  pub fn menu_item_id(&self) -> &I {
+    &self.menu_item_id
+  }
+}
+
+crate::manager::default_args! {
+  /// A handle to a system tray. Allows updating the context menu items.
+  pub struct MenuHandle<P: Params> {
+    pub(crate) ids: HashMap<u32, P::MenuId>,
+    pub(crate) dispatcher: <P::Runtime as Runtime>::Dispatcher,
+  }
+}
+
+impl<P: Params> Clone for MenuHandle<P> {
+  fn clone(&self) -> Self {
+    Self {
+      ids: self.ids.clone(),
+      dispatcher: self.dispatcher.clone(),
+    }
+  }
+}
+
+crate::manager::default_args! {
+  /// A handle to a system tray menu item.
+  pub struct MenuItemHandle<P: Params> {
+    id: u32,
+    dispatcher: <P::Runtime as Runtime>::Dispatcher,
+  }
+}
+
+impl<P: Params> Clone for MenuItemHandle<P> {
+  fn clone(&self) -> Self {
+    Self {
+      id: self.id,
+      dispatcher: self.dispatcher.clone(),
+    }
+  }
+}
+
+impl<P: Params> MenuHandle<P> {
+  pub fn get_item(&self, id: &P::MenuId) -> MenuItemHandle<P> {
+    for (raw, item_id) in self.ids.iter() {
+      if item_id == id {
+        return MenuItemHandle {
+          id: *raw,
+          dispatcher: self.dispatcher.clone(),
+        };
+      }
+    }
+    panic!("item id not found")
+  }
+}
+
+impl<P: Params> MenuItemHandle<P> {
+  /// Modifies the enabled state of the menu item.
+  pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
+    self
+      .dispatcher
+      .update_menu_item(self.id, MenuUpdate::SetEnabled(enabled))
+      .map_err(Into::into)
+  }
+
+  /// Modifies the title (label) of the menu item.
+  pub fn set_title<S: Into<String>>(&self, title: S) -> crate::Result<()> {
+    self
+      .dispatcher
+      .update_menu_item(self.id, MenuUpdate::SetTitle(title.into()))
+      .map_err(Into::into)
+  }
+
+  /// Modifies the selected state of the menu item.
+  pub fn set_selected(&self, selected: bool) -> crate::Result<()> {
+    self
+      .dispatcher
+      .update_menu_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
+      .dispatcher
+      .update_menu_item(self.id, MenuUpdate::SetNativeImage(image))
+      .map_err(Into::into)
+  }
+}

+ 161 - 0
docs/usage/guides/visual/menu.md

@@ -0,0 +1,161 @@
+---
+title: Window Menu
+---
+
+Native application menus can be attached to a window.
+
+### Setup
+
+Enable the `menu` feature flag on `src-tauri/Cargo.toml`:
+
+```toml
+[dependencies]
+tauri = { version = "1.0.0-beta.0", features = ["menu"] }
+```
+
+### Creating a menu
+
+To create a native window menu, import the `Menu`, `Submenu`, `MenuItem` and `CustomMenuItem` types.
+The `MenuItem` enum contains a collection of platform-specific items (currently not implemented on Windows).
+The `CustomMenuItem` allows you to create your own menu items and add special functionality to them.
+
+```rust
+use tauri::{CustomMenuItem, Menu, MenuItem, Submenu};
+```
+
+Create a `Menu` instance:
+
+```rust
+// here `"quit".to_string()` defines the menu item id, and the second parameter is the menu item label.
+let quit = CustomMenuItem::new("quit".to_string(), "Quit");
+let close = CustomMenuItem::new("close".to_string(), "Close");
+let submenu = Menu::new().add_item(quit).add_item(close);
+let menu = Menu::new()
+  .add_native_item(MenuItem::Copy)
+  .add_item(CustomMenuItem::new("hide", "Hide"))
+  .add_submenu(submenu);
+```
+
+### Adding the menu to all windows
+
+The defined menu can be set to all windows using the `menu` API on the `tauri::Builder` struct:
+
+```rust
+use tauri::{CustomMenuItem, Menu, MenuItem, Submenu};
+
+fn main() {
+  let menu = Menu::new(); // configure the menu
+  tauri::Builder::default()
+    .menu(menu)
+    .run(tauri::generate_context!())
+    .expect("error while running tauri application");
+}
+```
+
+### Adding the menu to a specific window
+
+You can create a window and set the menu to be used. This allows defining a specific menu set for each application window.
+
+```rust
+use tauri::{CustomMenuItem, Menu, MenuItem, Submenu};
+use tauri::WindowBuilder;
+
+fn main() {
+  let menu = Menu::new(); // configure the menu
+  tauri::Builder::default()
+    .create_window(
+      "main-window".to_string(),
+      tauri::WindowUrl::App("index.html".into()),
+      move |window_builder, webview_attributes| {
+        (window_builder.menu(menu), webview_attributes)
+      },
+    )
+    .run(tauri::generate_context!())
+    .expect("error while running tauri application");
+}
+```
+
+### Listening to events on custom menu items
+
+Each `CustomMenuItem` triggers an event when clicked. Use the `on_menu_event` API to handle them, either on the global `tauri::Builder` or on an specific window.
+
+#### Listening to events on global menus
+
+```rust
+use tauri::{CustomMenuItem, Menu, MenuItem};
+
+fn main() {
+  let menu = vec![]; // insert the menu array here
+  tauri::Builder::default()
+    .menu(menu)
+    .on_menu_event(|event| {
+      match event.menu_item_id().as_str() {
+        "quit" => {
+          std::process::exit(0);
+        }
+        "close" => {
+          event.window().close().unwrap();
+        }
+        _ => {}
+      }
+    })
+    .run(tauri::generate_context!())
+    .expect("error while running tauri application");
+}
+```
+
+#### Listening to events on window menus
+
+```rust
+use tauri::{CustomMenuItem, Menu, MenuItem};
+use tauri::{Manager, WindowBuilder};
+
+fn main() {
+  let menu = vec![]; // insert the menu array here
+  tauri::Builder::default()
+    .create_window(
+      "main-window".to_string(),
+      tauri::WindowUrl::App("index.html".into()),
+      move |window_builder, webview_attributes| {
+        (window_builder.menu(menu), webview_attributes)
+      },
+    )
+    .setup(|app| {
+      let window = app.get_window("main-window").unwrap();
+      let window_ = window.clone();
+      window.on_menu_event(move |event| {
+        match event.menu_item_id().as_str() {
+          "quit" => {
+            std::process::exit(0);
+          }
+          "close" => {
+            window_.close().unwrap();
+          }
+          _ => {}
+        }
+      });
+      Ok(())
+    })
+    .run(tauri::generate_context!())
+    .expect("error while running tauri application");
+}
+```
+
+### Updating menu items
+
+The `Window` struct has a `menu_handle` method, which allows updating menu items:
+
+```rust
+fn main() {
+  tauri::Builder::default()
+    .setup(|app| {
+      let main_window = app.get_window("main").unwrap();
+      let menu_handle = main_window.menu_handle();
+      std::thread::spawn(move || {
+        // you can also `set_selected`, `set_enabled` and `set_native_image` (macOS only).
+        menu_handle.get_item("item_id").set_title("New title");
+      })
+      Ok(())
+    })
+}
+```

+ 180 - 0
docs/usage/guides/visual/system-tray.md

@@ -0,0 +1,180 @@
+---
+title: System Tray
+---
+
+Native application system tray.
+
+### Setup
+
+Configure the `systemTray` object on `tauri.conf.json`:
+
+```json
+{
+  "tauri": {
+    "systemTray": {
+      "iconPath": "icons/icon.png"
+    }
+  }
+}
+```
+
+The `iconPath` is pointed to a PNG file on macOS and Linux, and a `.ico` file must exist for Windows support.
+
+### Creating a system tray
+
+To create a native system tray, import the `SystemTray` type:
+
+```rust
+use tauri::SystemTray;
+```
+
+Initialize a new tray instance:
+
+```rust
+let tray = SystemTray::new();
+```
+
+### Configuring a system tray context menu
+
+Optionally you can add a context menu that is visible when the tray icon is right clicked. Import the `SystemTrayMenu`, `SystemTrayMenuItem` and `CustomMenuItem` types:
+
+```rust
+use tauri::{CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem};
+```
+
+Create the `SystemTrayMenu`:
+
+```rust
+// here `"quit".to_string()` defines the menu item id, and the second parameter is the menu item label.
+let quit = CustomMenuItem::new("quit".to_string(), "Quit");
+let hide = CustomMenuItem::new("hide".to_string(), "Hide");
+let tray_menu = SystemTrayMenu::new()
+  .add_item(quit)
+  .add_native_item(SystemTrayMenuItem::Separator)
+  .add_item(hide);
+```
+
+Add the tray menu to the `SystemTray` instance:
+
+```rust
+let tray = SystemTray::new().with_menu(tray_menu);
+```
+
+### Configure the app system tray
+
+The created `SystemTray` instance can be set using the `system_tray` API on the `tauri::Builder` struct:
+
+```rust
+use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu};
+
+fn main() {
+  let tray_menu = SystemTrayMenu::new(); // insert the menu items here
+  let system_tray = SystemTray::new()
+    .with_menu(tray_menu);
+  tauri::Builder::default()
+    .system_tray(system_tray)
+    .run(tauri::generate_context!())
+    .expect("error while running tauri application");
+}
+```
+
+### Listening to system tray events
+
+Each `CustomMenuItem` triggers an event when clicked.
+Also, Tauri emits tray icon click events.
+Use the `on_system_tray_event` API to handle them:
+
+```rust
+use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu};
+use tauri::Manager;
+
+fn main() {
+  let tray_menu = SystemTrayMenu::new(); // insert the menu items here
+  tauri::Builder::default()
+    .system_tray(SystemTray::new().with_menu(tray_menu))
+    .on_system_tray_event(|app, event| match event {
+      SystemTrayEvent::LeftClick {
+        position: _,
+        size: _,
+        ..
+      } => {
+        println!("system tray received a left click");
+      }
+      SystemTrayEvent::RightClick {
+        position: _,
+        size: _,
+        ..
+      } => {
+        println!("system tray received a right click");
+      }
+      SystemTrayEvent::DoubleClick {
+        position: _,
+        size: _,
+        ..
+      } => {
+        println!("system tray received a double click");
+      }
+      SystemTrayEvent::MenuItemClick { id, .. } => {
+        match id.as_str() {
+          "quit" => {
+            std::process::exit(0);
+          }
+          "hide" => {
+            let window = app.get_window("main").unwrap();
+            window.hide().unwrap();
+          }
+          _ => {}
+        }
+      }
+      _ => {}
+    })
+    .run(tauri::generate_context!())
+    .expect("error while running tauri application");
+}
+```
+
+### Updating system tray
+
+The `AppHandle` struct has a `tray_handle` method, which returns a handle to the system tray allowing updating tray icon and context menu items:
+
+#### Updating context menu items
+
+```rust
+use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu};
+use tauri::Manager;
+
+fn main() {
+  let tray_menu = SystemTrayMenu::new(); // insert the menu items here
+  tauri::Builder::default()
+    .system_tray(SystemTray::new().with_menu(tray_menu))
+    .on_system_tray_event(|app, event| match event {
+      SystemTrayEvent::MenuItemClick { id, .. } => {
+        // get a handle to the clicked menu item
+        // note that `tray_handle` can be called anywhere,
+        // just get a `AppHandle` instance with `app.handle()` on the setup hook
+        // and move it to another function or thread
+        let item_handle = app.tray_handle().get_item(&id);
+        match id.as_str() {
+          "hide" => {
+            let window = app.get_window("main").unwrap();
+            window.hide().unwrap();
+            // you can also `set_selected`, `set_enabled` and `set_native_image` (macOS only).
+            item_handle.set_title("Show").unwrap();
+          }
+          _ => {}
+        }
+      }
+      _ => {}
+    })
+    .run(tauri::generate_context!())
+    .expect("error while running tauri application");
+}
+```
+
+#### Updating tray icon
+
+Note that `tauri::Icon` must be a `Path` variant on Linux, and `Raw` variant on Windows and macOS.
+
+```rust
+app.tray_handle().set_icon(tauri::Icon::Raw(include_bytes!("../path/to/myicon.ico"))).unwrap();
+```

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
examples/api/public/build/bundle.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
examples/api/public/build/bundle.js.map


+ 43 - 18
examples/api/src-tauri/src/main.rs

@@ -11,7 +11,9 @@ mod cmd;
 mod menu;
 
 use serde::Serialize;
-use tauri::{CustomMenuItem, Manager, SystemTrayMenuItem, WindowBuilder, WindowUrl};
+use tauri::{
+  CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowBuilder, WindowUrl,
+};
 
 #[derive(Serialize)]
 struct Reply {
@@ -37,26 +39,49 @@ fn main() {
     .on_menu_event(|event| {
       println!("{:?}", event.menu_item_id());
     })
-    .system_tray(vec![
-      SystemTrayMenuItem::Custom(CustomMenuItem::new("toggle".into(), "Toggle")),
-      SystemTrayMenuItem::Custom(CustomMenuItem::new("new".into(), "New window")),
-    ])
-    .on_system_tray_event(|app, event| match event.menu_item_id().as_str() {
-      "toggle" => {
+    .system_tray(
+      SystemTray::new().with_menu(
+        SystemTrayMenu::new()
+          .add_item(CustomMenuItem::new("toggle".into(), "Toggle"))
+          .add_item(CustomMenuItem::new("new".into(), "New window")),
+      ),
+    )
+    .on_system_tray_event(|app, event| match event {
+      SystemTrayEvent::LeftClick {
+        position: _,
+        size: _,
+        ..
+      } => {
         let window = app.get_window("main").unwrap();
-        if window.is_visible().unwrap() {
-          window.hide().unwrap();
-        } else {
-          window.show().unwrap();
+        window.show().unwrap();
+        window.set_focus().unwrap();
+      }
+      SystemTrayEvent::MenuItemClick { id, .. } => {
+        let item_handle = app.tray_handle().get_item(&id);
+        match id.as_str() {
+          "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" => app
+            .create_window(
+              "new".into(),
+              WindowUrl::App("index.html".into()),
+              |window_builder, webview_attributes| {
+                (window_builder.title("Tauri"), webview_attributes)
+              },
+            )
+            .unwrap(),
+          _ => {}
         }
       }
-      "new" => app
-        .create_window(
-          "new".into(),
-          WindowUrl::App("index.html".into()),
-          |window_builder, webview_attributes| (window_builder.title("Tauri"), webview_attributes),
-        )
-        .unwrap(),
       _ => {}
     })
     .invoke_handler(tauri::generate_handler![

+ 29 - 69
examples/api/src-tauri/src/menu.rs

@@ -2,76 +2,36 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use tauri::{CustomMenuItem, Menu, MenuItem};
+use tauri::{CustomMenuItem, Menu, MenuItem, Submenu};
 
-pub fn get_menu() -> Vec<Menu<String>> {
-  let other_test_menu = MenuItem::Custom(CustomMenuItem::new("custom".into(), "Custom"));
-  let quit_menu = MenuItem::Custom(CustomMenuItem::new("quit".into(), "Quit"));
+pub fn get_menu() -> Menu<String> {
+  #[allow(unused_mut)]
+  let mut disable_item = CustomMenuItem::new("disable-menu".into(), "Disable menu");
+  #[allow(unused_mut)]
+  let mut test_item = CustomMenuItem::new("test".into(), "Test");
+  #[cfg(target_os = "macos")]
+  {
+    disable_item = disable_item.native_image(tauri::NativeImage::MenuOnState);
+    test_item = test_item.native_image(tauri::NativeImage::Add);
+  }
 
-  // macOS require to have at least Copy, Paste, Select all etc..
-  // to works fine. You should always add them.
-  #[cfg(any(target_os = "linux", target_os = "macos"))]
-  let menu = {
-    let custom_print_menu = MenuItem::Custom(CustomMenuItem::new("print".into(), "Print"));
-    vec![
-      Menu::new(
-        // on macOS first menu is always app name
-        "Tauri API",
-        vec![
-          // All's non-custom menu, do NOT return event's
-          // they are handled by the system automatically
-          MenuItem::About("Tauri".to_string()),
-          MenuItem::Services,
-          MenuItem::Separator,
-          MenuItem::Hide,
-          MenuItem::HideOthers,
-          MenuItem::ShowAll,
-          MenuItem::Separator,
-          quit_menu,
-        ],
-      ),
-      Menu::new(
-        "File",
-        vec![
-          custom_print_menu,
-          MenuItem::Separator,
-          other_test_menu,
-          MenuItem::CloseWindow,
-        ],
-      ),
-      Menu::new(
-        "Edit",
-        vec![
-          MenuItem::Undo,
-          MenuItem::Redo,
-          MenuItem::Separator,
-          MenuItem::Cut,
-          MenuItem::Copy,
-          MenuItem::Paste,
-          MenuItem::Separator,
-          MenuItem::SelectAll,
-        ],
-      ),
-      Menu::new("View", vec![MenuItem::EnterFullScreen]),
-      Menu::new("Window", vec![MenuItem::Minimize, MenuItem::Zoom]),
-      Menu::new(
-        "Help",
-        vec![MenuItem::Custom(CustomMenuItem::new(
-          "help".into(),
-          "Custom help",
-        ))],
-      ),
-    ]
-  };
+  // create a submenu
+  let my_sub_menu = Menu::new().add_item(disable_item);
 
-  // Attention, Windows only support custom menu for now.
-  // If we add any `MenuItem::*` they'll not render
-  // We need to use custom menu with `Menu::new()` and catch
-  // the events in the EventLoop.
-  #[cfg(target_os = "windows")]
-  let menu = vec![
-    Menu::new("File", vec![other_test_menu]),
-    Menu::new("Other menu", vec![quit_menu]),
-  ];
-  menu
+  let my_app_menu = Menu::new()
+    .add_native_item(MenuItem::Copy)
+    .add_submenu(Submenu::new("Sub menu", my_sub_menu));
+
+  let test_menu = Menu::new()
+    .add_item(CustomMenuItem::new(
+      "selected/disabled".into(),
+      "Selected and disabled",
+    ))
+    .add_native_item(MenuItem::Separator)
+    .add_item(test_item);
+
+  // add all our childs to the menu (order is how they'll appear)
+  Menu::new()
+    .add_submenu(Submenu::new("My app", my_app_menu))
+    .add_submenu(Submenu::new("Other menu", test_menu))
 }

+ 1 - 1
tooling/cli.js/test/jest/__tests__/template.spec.js

@@ -27,7 +27,7 @@ describe('[CLI] cli.js template', () => {
     const manifestFile = readFileSync(manifestPath).toString()
     writeFileSync(
       manifestPath,
-      `workspace = { }\n[patch.crates-io]\ntao = { git = "https://github.com/tauri-apps/tao", rev = "a3f533232df25dc30998809094ed5431b449489c" }\n\n${manifestFile}`
+      `workspace = { }\n[patch.crates-io]\ntao = { git = "https://github.com/tauri-apps/tao", rev = "5be88eb9488e3ad27194b5eff2ea31a473128f9c" }\n\n${manifestFile}`
     )
 
     const { promise: buildPromise } = await build()

+ 1 - 0
tooling/cli.rs/src/interface/rust.rs

@@ -377,6 +377,7 @@ fn tauri_config_to_bundle_settings(
     if let Some(system_tray_config) = &system_tray_config {
       let mut icon_path = system_tray_config.icon_path.clone();
       icon_path.set_extension("png");
+      resources.push(icon_path.display().to_string());
       depends.push("libappindicator3-1".to_string());
     }
 

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно