Sfoglia il codice sorgente

feat(core): system tray, closes #157 (#1749)

Lucas Fernandes Nogueira 4 anni fa
parent
commit
c090927021

+ 5 - 0
.changes/tray.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Adds system tray support.

+ 42 - 0
core/tauri-codegen/src/context.rs

@@ -62,6 +62,47 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
     quote!(None)
   };
 
+  #[cfg(target_os = "linux")]
+  let system_tray_icon = if let Some(tray) = &config.tauri.system_tray {
+    let mut system_tray_icon_path = tray.icon_path.clone();
+    system_tray_icon_path.set_extension("png");
+    if dev {
+      let system_tray_icon_file_name = system_tray_icon_path
+        .file_name()
+        .expect("failed to get tray path file_name")
+        .to_string_lossy()
+        .to_string();
+      quote!(Some(
+        ::tauri::platform::resource_dir()
+          .expect("failed to read resource dir")
+          .join(
+            #system_tray_icon_file_name
+          )
+      ))
+    } else {
+      let system_tray_icon_path = config_parent
+        .join(system_tray_icon_path)
+        .display()
+        .to_string();
+      quote!(Some(::std::path::PathBuf::from(#system_tray_icon_path)))
+    }
+  } else {
+    quote!(None)
+  };
+
+  #[cfg(not(target_os = "linux"))]
+  let system_tray_icon = if let Some(tray) = &config.tauri.system_tray {
+    let mut system_tray_icon_path = tray.icon_path.clone();
+    system_tray_icon_path.set_extension(if cfg!(windows) { "ico" } else { "png" });
+    let system_tray_icon_path = config_parent
+      .join(system_tray_icon_path)
+      .display()
+      .to_string();
+    quote!(Some(include_bytes!(#system_tray_icon_path).to_vec()))
+  } else {
+    quote!(None)
+  };
+
   let package_name = if let Some(product_name) = &config.package.product_name {
     quote!(#product_name.to_string())
   } else {
@@ -78,6 +119,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
     config: #config,
     assets: ::std::sync::Arc::new(#assets),
     default_window_icon: #default_window_icon,
+    system_tray_icon: #system_tray_icon,
     package_info: #root::api::PackageInfo {
       name: #package_name,
       version: #package_version,

+ 38 - 7
core/tauri-utils/src/config.rs

@@ -134,7 +134,7 @@ impl Default for WindowConfig {
 
 /// The Updater configuration object.
 #[derive(PartialEq, Deserialize, Debug, Clone)]
-#[serde(tag = "updater", rename_all = "camelCase")]
+#[serde(rename_all = "camelCase")]
 pub struct UpdaterConfig {
   /// Whether the updater is active or not.
   #[serde(default)]
@@ -167,12 +167,21 @@ impl Default for UpdaterConfig {
 
 /// Security configuration.
 #[derive(PartialEq, Deserialize, Debug, Clone, Default)]
-#[serde(tag = "updater", rename_all = "camelCase")]
+#[serde(rename_all = "camelCase")]
 pub struct SecurityConfig {
   /// Content security policy to inject to HTML files with the custom protocol.
   pub csp: Option<String>,
 }
 
+/// Configuration for application system tray icon.
+#[derive(PartialEq, Deserialize, Debug, Clone, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct SystemTrayConfig {
+  /// Path to the icon to use on the system tray.
+  /// Automatically set to be an `.png` on macOS and Linux, and `.ico` on Windows.
+  pub icon_path: PathBuf,
+}
+
 /// A CLI argument definition
 #[derive(PartialEq, Deserialize, Debug, Default, Clone)]
 #[serde(rename_all = "camelCase")]
@@ -262,7 +271,7 @@ pub struct CliArg {
 
 /// The CLI root command definition.
 #[derive(PartialEq, Deserialize, Debug, Clone)]
-#[serde(tag = "cli", rename_all = "camelCase")]
+#[serde(rename_all = "camelCase")]
 #[allow(missing_docs)] // TODO
 pub struct CliConfig {
   pub description: Option<String>,
@@ -311,7 +320,7 @@ impl CliConfig {
 
 /// The bundler configuration object.
 #[derive(PartialEq, Deserialize, Debug)]
-#[serde(tag = "bundle", rename_all = "camelCase")]
+#[serde(rename_all = "camelCase")]
 pub struct BundleConfig {
   /// The bundle identifier.
   pub identifier: String,
@@ -335,7 +344,7 @@ fn default_window_config() -> Vec<WindowConfig> {
 
 /// The Tauri configuration object.
 #[derive(PartialEq, Deserialize, Debug)]
-#[serde(tag = "tauri", rename_all = "camelCase")]
+#[serde(rename_all = "camelCase")]
 pub struct TauriConfig {
   /// The window configuration.
   #[serde(default = "default_window_config")]
@@ -352,6 +361,8 @@ pub struct TauriConfig {
   /// The security configuration.
   #[serde(default)]
   pub security: SecurityConfig,
+  /// System tray configuration.
+  pub system_tray: Option<SystemTrayConfig>,
 }
 
 impl Default for TauriConfig {
@@ -362,13 +373,14 @@ impl Default for TauriConfig {
       bundle: BundleConfig::default(),
       updater: UpdaterConfig::default(),
       security: SecurityConfig::default(),
+      system_tray: None,
     }
   }
 }
 
 /// The Build configuration object.
 #[derive(PartialEq, Deserialize, Debug)]
-#[serde(tag = "build", rename_all = "camelCase")]
+#[serde(rename_all = "camelCase")]
 pub struct BuildConfig {
   /// the devPath config.
   #[serde(default = "default_dev_path")]
@@ -777,6 +789,14 @@ mod build {
     }
   }
 
+  impl ToTokens for SystemTrayConfig {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+      let icon_path = self.icon_path.to_string_lossy().to_string();
+      let icon_path = quote! { ::std::path::PathBuf::from(#icon_path) };
+      literal_struct!(tokens, SystemTrayConfig, icon_path);
+    }
+  }
+
   impl ToTokens for TauriConfig {
     fn to_tokens(&self, tokens: &mut TokenStream) {
       let windows = vec_lit(&self.windows, identity);
@@ -784,8 +804,18 @@ mod build {
       let bundle = &self.bundle;
       let updater = &self.updater;
       let security = &self.security;
+      let system_tray = opt_lit(self.system_tray.as_ref());
 
-      literal_struct!(tokens, TauriConfig, windows, cli, bundle, updater, security);
+      literal_struct!(
+        tokens,
+        TauriConfig,
+        windows,
+        cli,
+        bundle,
+        updater,
+        security,
+        system_tray
+      );
     }
   }
 
@@ -880,6 +910,7 @@ mod test {
         endpoints: None,
       },
       security: SecurityConfig { csp: None },
+      system_tray: None,
     };
 
     // create a build config

+ 1 - 0
core/tauri/src/api/process.rs

@@ -365,6 +365,7 @@ impl Command {
 // tests for the commands functions.
 #[cfg(test)]
 mod test {
+  #[cfg(not(windows))]
   use super::*;
 
   #[cfg(not(windows))]

+ 16 - 13
core/tauri/src/endpoints/window.rs

@@ -3,8 +3,7 @@
 // SPDX-License-Identifier: MIT
 
 #[cfg(window_create)]
-use crate::sealed::ManagerBase;
-
+use crate::runtime::{webview::WindowBuilder, Dispatch, Runtime};
 use crate::{
   api::config::WindowConfig,
   endpoints::InvokeResponse,
@@ -106,17 +105,21 @@ impl Cmd {
           });
 
           let url = options.url.clone();
-          let pending = crate::runtime::window::PendingWindow::with_config(
-            options,
-            crate::runtime::webview::WebviewAttributes::new(url),
-            label.clone(),
-          );
-          window.create_new_window(pending)?.emit_others(
-            &crate::runtime::manager::tauri_event::<P::Event>("tauri://window-created"),
-            Some(WindowCreatedEvent {
-              label: label.to_string(),
-            }),
-          )?;
+          window
+            .create_window(label.clone(), url, |_, webview_attributes| {
+              (
+                <<<P::Runtime as Runtime>::Dispatcher as Dispatch>::WindowBuilder>::with_config(
+                  options,
+                ),
+                webview_attributes,
+              )
+            })?
+            .emit_others(
+              &crate::runtime::manager::tauri_event::<P::Event>("tauri://window-created"),
+              Some(WindowCreatedEvent {
+                label: label.to_string(),
+              }),
+            )?;
         }
         // Getters
         Self::ScaleFactor => return Ok(window.scale_factor()?.into()),

+ 3 - 0
core/tauri/src/error.rs

@@ -66,6 +66,9 @@ pub enum Error {
   /// `default_path` provided to dialog API doesn't exist.
   #[error("failed to setup dialog: provided default path `{0}` doesn't exist")]
   DialogDefaultPathNotExists(PathBuf),
+  /// Encountered an error creating the app system tray,
+  #[error("error encountered during tray setup: {0}")]
+  SystemTray(Box<dyn std::error::Error + Send>),
 }
 
 impl From<serde_json::Error> for Error {

+ 23 - 11
core/tauri/src/lib.rs

@@ -59,12 +59,11 @@ pub use {
     Invoke, InvokeError, InvokeHandler, InvokeMessage, InvokeResolver, InvokeResponse, OnPageLoad,
     PageLoadPayload, SetupHook,
   },
-  self::runtime::app::{App, Builder, WindowMenuEvent},
+  self::runtime::app::{App, Builder, SystemTrayEvent, WindowMenuEvent},
   self::runtime::flavors::wry::Wry,
+  self::runtime::menu::{CustomMenuItem, Menu, MenuId, MenuItem, SystemTrayMenuItem},
   self::runtime::monitor::Monitor,
-  self::runtime::webview::{
-    CustomMenuItem, Menu, MenuItem, MenuItemId, WebviewAttributes, WindowBuilder,
-  },
+  self::runtime::webview::{WebviewAttributes, WindowBuilder},
   self::runtime::window::{
     export::{
       dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Pixel, Position, Size},
@@ -73,6 +72,7 @@ pub use {
     WindowEvent,
   },
   self::state::{State, StateManager},
+  tauri_utils::platform,
 };
 
 /// Reads the config file at compile time and generates a [`Context`] based on its content.
@@ -130,6 +130,14 @@ pub struct Context<A: Assets> {
   /// The default window icon Tauri should use when creating windows.
   pub default_window_icon: Option<Vec<u8>>,
 
+  /// The icon to use use on the system tray UI.
+  #[cfg(target_os = "linux")]
+  pub system_tray_icon: Option<PathBuf>,
+
+  /// The icon to use use on the system tray UI.
+  #[cfg(not(target_os = "linux"))]
+  pub system_tray_icon: Option<Vec<u8>>,
+
   /// Package information.
   pub package_info: crate::api::PackageInfo,
 }
@@ -142,6 +150,12 @@ pub trait Params: sealed::ParamsBase {
   /// The type used to determine the name of windows.
   type Label: Tag;
 
+  /// The type used to determine window menu ids.
+  type MenuId: MenuId;
+
+  /// The type used to determine system tray menu ids.
+  type SystemTrayMenuId: MenuId;
+
   /// Assets that Tauri should serve from itself.
   type Assets: Assets;
 
@@ -258,8 +272,8 @@ pub(crate) mod sealed {
 
   /// A running [`Runtime`] or a dispatcher to it.
   pub enum RuntimeOrDispatch<'r, P: Params> {
-    /// Mutable reference to the running [`Runtime`].
-    Runtime(&'r mut P::Runtime),
+    /// Reference to the running [`Runtime`].
+    Runtime(&'r P::Runtime),
 
     /// A dispatcher to the running [`Runtime`].
     Dispatch(<P::Runtime as Runtime>::Dispatcher),
@@ -270,18 +284,16 @@ pub(crate) mod sealed {
     /// The manager behind the [`Managed`] item.
     fn manager(&self) -> &WindowManager<P>;
 
-    /// The runtime or runtime dispatcher of the [`Managed`] item.
-    fn runtime(&mut self) -> RuntimeOrDispatch<'_, P>;
-
     /// Creates a new [`Window`] on the [`Runtime`] and attaches it to the [`Manager`].
     fn create_new_window(
-      &mut self,
+      &self,
+      runtime: RuntimeOrDispatch<'_, P>,
       pending: crate::PendingWindow<P>,
     ) -> crate::Result<crate::Window<P>> {
       use crate::runtime::Dispatch;
       let labels = self.manager().labels().into_iter().collect::<Vec<_>>();
       let pending = self.manager().prepare_window(pending, &labels)?;
-      match self.runtime() {
+      match runtime {
         RuntimeOrDispatch::Runtime(runtime) => runtime.create_window(pending),
         RuntimeOrDispatch::Dispatch(mut dispatcher) => dispatcher.create_window(pending),
       }

+ 119 - 29
core/tauri/src/runtime/app.rs

@@ -9,8 +9,9 @@ use crate::{
   runtime::{
     flavors::wry::Wry,
     manager::{Args, WindowManager},
+    menu::{Menu, MenuId, SystemTrayMenuItem},
     tag::Tag,
-    webview::{CustomProtocol, Menu, MenuItemId, WebviewAttributes, WindowBuilder},
+    webview::{CustomProtocol, WebviewAttributes, WindowBuilder},
     window::PendingWindow,
     Dispatch, Runtime,
   },
@@ -24,17 +25,31 @@ use std::{collections::HashMap, sync::Arc};
 use crate::updater;
 
 pub(crate) type GlobalMenuEventListener<P> = Box<dyn Fn(WindowMenuEvent<P>) + Send + Sync>;
+type SystemTrayEventListener<P> =
+  Box<dyn Fn(&AppHandle<P>, SystemTrayEvent<<P as Params>::SystemTrayMenuId>) + Send + Sync>;
+
+/// System tray event.
+pub struct SystemTrayEvent<I: MenuId> {
+  menu_item_id: I,
+}
+
+impl<I: MenuId> SystemTrayEvent<I> {
+  /// The menu item id.
+  pub fn menu_item_id(&self) -> &I {
+    &self.menu_item_id
+  }
+}
 
 /// A menu event that was triggered on a window.
 pub struct WindowMenuEvent<P: Params> {
-  pub(crate) menu_item_id: MenuItemId,
+  pub(crate) menu_item_id: P::MenuId,
   pub(crate) window: Window<P>,
 }
 
 impl<P: Params> WindowMenuEvent<P> {
   /// The menu item id.
-  pub fn menu_item_id(&self) -> MenuItemId {
-    self.menu_item_id
+  pub fn menu_item_id(&self) -> &P::MenuId {
+    &self.menu_item_id
   }
 
   /// The window that the menu belongs to.
@@ -44,6 +59,18 @@ impl<P: Params> WindowMenuEvent<P> {
 }
 
 /// A handle to the currently running application.
+pub struct AppHandle<P: Params> {
+  manager: WindowManager<P>,
+}
+
+impl<P: Params> Manager<P> for AppHandle<P> {}
+impl<P: Params> ManagerBase<P> for AppHandle<P> {
+  fn manager(&self) -> &WindowManager<P> {
+    &self.manager
+  }
+}
+
+/// The instance of the currently running application.
 ///
 /// This type implements [`Manager`] which allows for manipulation of global application items.
 pub struct App<P: Params> {
@@ -56,10 +83,6 @@ impl<P: Params> ManagerBase<P> for App<P> {
   fn manager(&self) -> &WindowManager<P> {
     &self.manager
   }
-
-  fn runtime(&mut self) -> RuntimeOrDispatch<'_, P> {
-    RuntimeOrDispatch::Runtime(&mut self.runtime)
-  }
 }
 
 impl<P: Params> App<P> {
@@ -78,11 +101,10 @@ impl<P: Params> App<P> {
       <<P::Runtime as Runtime>::Dispatcher as Dispatch>::WindowBuilder::new(),
       WebviewAttributes::new(url),
     );
-    self.create_new_window(PendingWindow::new(
-      window_attributes,
-      webview_attributes,
-      label,
-    ))?;
+    self.create_new_window(
+      RuntimeOrDispatch::Runtime(&self.runtime),
+      PendingWindow::new(window_attributes, webview_attributes, label),
+    )?;
     Ok(())
   }
 }
@@ -147,27 +169,30 @@ impl<M: Params> App<M> {
 }
 
 /// Builds a Tauri application.
-pub struct Builder<E, L, A, R>
+#[allow(clippy::type_complexity)]
+pub struct Builder<E, L, MID, TID, A, R>
 where
   E: Tag,
   L: Tag,
+  MID: MenuId,
+  TID: MenuId,
   A: Assets,
   R: Runtime,
 {
   /// The JS message handler.
-  invoke_handler: Box<InvokeHandler<Args<E, L, A, R>>>,
+  invoke_handler: Box<InvokeHandler<Args<E, L, MID, TID, A, R>>>,
 
   /// The setup hook.
-  setup: SetupHook<Args<E, L, A, R>>,
+  setup: SetupHook<Args<E, L, MID, TID, A, R>>,
 
   /// Page load hook.
-  on_page_load: Box<OnPageLoad<Args<E, L, A, R>>>,
+  on_page_load: Box<OnPageLoad<Args<E, L, MID, TID, A, R>>>,
 
   /// windows to create when starting up.
-  pending_windows: Vec<PendingWindow<Args<E, L, A, R>>>,
+  pending_windows: Vec<PendingWindow<Args<E, L, MID, TID, A, R>>>,
 
   /// All passed plugins
-  plugins: PluginStore<Args<E, L, A, R>>,
+  plugins: PluginStore<Args<E, L, MID, TID, A, R>>,
 
   /// The webview protocols available to all windows.
   uri_scheme_protocols: HashMap<String, Arc<CustomProtocol>>,
@@ -176,16 +201,24 @@ where
   state: StateManager,
 
   /// The menu set to all windows.
-  menu: Vec<Menu>,
+  menu: Vec<Menu<MID>>,
 
   /// Menu event handlers that listens to all windows.
-  menu_event_listeners: Vec<GlobalMenuEventListener<Args<E, L, A, R>>>,
+  menu_event_listeners: Vec<GlobalMenuEventListener<Args<E, L, MID, TID, A, R>>>,
+
+  /// The app system tray menu items.
+  system_tray: Vec<SystemTrayMenuItem<TID>>,
+
+  /// System tray event handlers.
+  system_tray_event_listeners: Vec<SystemTrayEventListener<Args<E, L, MID, TID, A, R>>>,
 }
 
-impl<E, L, A, R> Builder<E, L, A, R>
+impl<E, L, MID, TID, A, R> Builder<E, L, MID, TID, A, R>
 where
   E: Tag,
   L: Tag,
+  MID: MenuId,
+  TID: MenuId,
   A: Assets,
   R: Runtime,
 {
@@ -201,13 +234,15 @@ where
       state: StateManager::new(),
       menu: Vec::new(),
       menu_event_listeners: Vec::new(),
+      system_tray: Vec::new(),
+      system_tray_event_listeners: Vec::new(),
     }
   }
 
   /// Defines the JS message handler callback.
   pub fn invoke_handler<F>(mut self, invoke_handler: F) -> Self
   where
-    F: Fn(Invoke<Args<E, L, A, R>>) + Send + Sync + 'static,
+    F: Fn(Invoke<Args<E, L, MID, TID, A, R>>) + Send + Sync + 'static,
   {
     self.invoke_handler = Box::new(invoke_handler);
     self
@@ -216,7 +251,7 @@ where
   /// Defines the setup hook.
   pub fn setup<F>(mut self, setup: F) -> Self
   where
-    F: Fn(&mut App<Args<E, L, A, R>>) -> Result<(), Box<dyn std::error::Error + Send>>
+    F: Fn(&mut App<Args<E, L, MID, TID, A, R>>) -> Result<(), Box<dyn std::error::Error + Send>>
       + Send
       + 'static,
   {
@@ -227,14 +262,14 @@ where
   /// Defines the page load hook.
   pub fn on_page_load<F>(mut self, on_page_load: F) -> Self
   where
-    F: Fn(Window<Args<E, L, A, R>>, PageLoadPayload) + Send + Sync + 'static,
+    F: Fn(Window<Args<E, L, MID, TID, A, R>>, PageLoadPayload) + Send + Sync + 'static,
   {
     self.on_page_load = Box::new(on_page_load);
     self
   }
 
   /// Adds a plugin to the runtime.
-  pub fn plugin<P: Plugin<Args<E, L, A, R>> + 'static>(mut self, plugin: P) -> Self {
+  pub fn plugin<P: Plugin<Args<E, L, MID, TID, A, R>> + 'static>(mut self, plugin: P) -> Self {
     self.plugins.register(plugin);
     self
   }
@@ -314,14 +349,22 @@ where
     self
   }
 
+  /// Adds the icon configured on `tauri.conf.json` to the system tray with the specified menu items.
+  pub fn system_tray(mut self, items: Vec<SystemTrayMenuItem<TID>>) -> Self {
+    self.system_tray = items;
+    self
+  }
+
   /// Sets the menu to use on all windows.
-  pub fn menu(mut self, menu: Vec<Menu>) -> Self {
+  pub fn menu(mut self, menu: Vec<Menu<MID>>) -> Self {
     self.menu = menu;
     self
   }
 
   /// Registers a menu event handler for all windows.
-  pub fn on_menu_event<F: Fn(WindowMenuEvent<Args<E, L, A, R>>) + Send + Sync + 'static>(
+  pub fn on_menu_event<
+    F: Fn(WindowMenuEvent<Args<E, L, MID, TID, A, R>>) + Send + Sync + 'static,
+  >(
     mut self,
     handler: F,
   ) -> Self {
@@ -329,6 +372,17 @@ where
     self
   }
 
+  /// Registers a system tray event handler.
+  pub fn on_system_tray_event<
+    F: Fn(&AppHandle<Args<E, L, MID, TID, A, R>>, SystemTrayEvent<TID>) + Send + Sync + 'static,
+  >(
+    mut self,
+    handler: F,
+  ) -> Self {
+    self.system_tray_event_listeners.push(Box::new(handler));
+    self
+  }
+
   /// Registers a URI scheme protocol available to all webviews.
   /// Leverages [setURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler) on macOS,
   /// [AddWebResourceRequestedFilter](https://docs.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.addwebresourcerequestedfilter?view=webview2-dotnet-1.0.774.44) on Windows
@@ -357,6 +411,7 @@ where
 
   /// Runs the configured Tauri application.
   pub fn run(mut self, context: Context<A>) -> crate::Result<()> {
+    let system_tray_icon = context.system_tray_icon.clone();
     let manager = WindowManager::with_handlers(
       context,
       self.plugins,
@@ -414,13 +469,48 @@ where
 
     (self.setup)(&mut app).map_err(|e| crate::Error::Setup(e))?;
 
+    if !self.system_tray.is_empty() {
+      let ids = get_menu_ids(&self.system_tray);
+      app
+        .runtime
+        .system_tray(
+          system_tray_icon.expect("tray icon not found; please configure it on tauri.conf.json"),
+          self.system_tray,
+        )
+        .expect("failed to run tray");
+      for listener in self.system_tray_event_listeners {
+        let app_handle = AppHandle {
+          manager: app.manager.clone(),
+        };
+        let ids = ids.clone();
+        app.runtime.on_system_tray_event(move |event| {
+          listener(
+            &app_handle,
+            SystemTrayEvent {
+              menu_item_id: ids.get(&event.menu_item_id).unwrap().clone(),
+            },
+          );
+        });
+      }
+    }
+
     app.runtime.run();
     Ok(())
   }
 }
 
+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`
-impl<A: Assets> Default for Builder<String, String, A, Wry> {
+impl<A: Assets> Default for Builder<String, String, String, String, A, Wry> {
   fn default() -> Self {
     Self::new()
   }

+ 80 - 16
core/tauri/src/runtime/flavors/wry.rs

@@ -7,15 +7,16 @@
 use crate::{
   api::config::WindowConfig,
   runtime::{
+    menu::{CustomMenuItem, Menu, MenuId, MenuItem, SystemTrayMenuItem},
     webview::{
-      CustomMenuItem, FileDropEvent, FileDropHandler, Menu, MenuItem, MenuItemId, RpcRequest,
-      WebviewRpcHandler, WindowBuilder, WindowBuilderBase,
+      FileDropEvent, FileDropHandler, RpcRequest, WebviewRpcHandler, WindowBuilder,
+      WindowBuilderBase,
     },
     window::{
       dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size},
       DetachedWindow, MenuEvent, PendingWindow, WindowEvent,
     },
-    Dispatch, Monitor, Params, Runtime,
+    Dispatch, Monitor, Params, Runtime, SystemTrayEvent,
   },
   Icon,
 };
@@ -36,6 +37,7 @@ use wry::{
       MenuType,
     },
     monitor::MonitorHandle,
+    platform::system_tray::SystemTrayBuilder,
     window::{Fullscreen, Icon as WindowIcon, Window, WindowBuilder as WryWindowBuilder, WindowId},
   },
   webview::{
@@ -60,6 +62,8 @@ type WindowEventHandler = Box<dyn Fn(&WindowEvent) + Send>;
 type WindowEventListeners = Arc<Mutex<HashMap<Uuid, WindowEventHandler>>>;
 type MenuEventHandler = Box<dyn Fn(&MenuEvent) + Send>;
 type MenuEventListeners = Arc<Mutex<HashMap<Uuid, MenuEventHandler>>>;
+type SystemTrayEventHandler = Box<dyn Fn(&SystemTrayEvent) + Send>;
+type SystemTrayEventListeners = HashMap<Uuid, SystemTrayEventHandler>;
 
 #[repr(C)]
 #[derive(Debug)]
@@ -201,18 +205,18 @@ impl From<Position> for WryPosition {
   }
 }
 
-impl From<CustomMenuItem> for WryCustomMenu {
-  fn from(item: CustomMenuItem) -> Self {
+impl<I: MenuId> From<CustomMenuItem<I>> for WryCustomMenu {
+  fn from(item: CustomMenuItem<I>) -> Self {
     Self {
-      id: WryMenuId(item.id.0),
+      id: WryMenuId(item.id_value()),
       name: item.name,
       keyboard_accelerators: None,
     }
   }
 }
 
-impl From<MenuItem> for WryMenuItem {
-  fn from(item: MenuItem) -> Self {
+impl<I: MenuId> From<MenuItem<I>> for WryMenuItem {
+  fn from(item: MenuItem<I>) -> Self {
     match item {
       MenuItem::Custom(custom) => Self::Custom(custom.into()),
       MenuItem::About(v) => Self::About(v),
@@ -236,8 +240,8 @@ impl From<MenuItem> for WryMenuItem {
   }
 }
 
-impl From<Menu> for WryMenu {
-  fn from(menu: Menu) -> Self {
+impl<I: MenuId> From<Menu<I>> for WryMenu {
+  fn from(menu: Menu<I>) -> Self {
     Self {
       title: menu.title,
       items: menu.items.into_iter().map(Into::into).collect(),
@@ -245,6 +249,15 @@ impl From<Menu> for WryMenu {
   }
 }
 
+impl<I: MenuId> From<SystemTrayMenuItem<I>> for WryMenuItem {
+  fn from(item: SystemTrayMenuItem<I>) -> Self {
+    match item {
+      SystemTrayMenuItem::Custom(custom) => Self::Custom(custom.into()),
+      SystemTrayMenuItem::Separator => Self::Separator,
+    }
+  }
+}
+
 impl WindowBuilderBase for WryWindowBuilder {}
 impl WindowBuilder for WryWindowBuilder {
   fn new() -> Self {
@@ -276,7 +289,7 @@ impl WindowBuilder for WryWindowBuilder {
     window
   }
 
-  fn menu(self, menu: Vec<Menu>) -> Self {
+  fn menu<I: MenuId>(self, menu: Vec<Menu<I>>) -> Self {
     self.with_menu(menu.into_iter().map(Into::into).collect::<Vec<WryMenu>>())
   }
 
@@ -762,10 +775,11 @@ impl Dispatch for WryDispatcher {
 /// A Tauri [`Runtime`] wrapper around wry.
 pub struct Wry {
   event_loop: EventLoop<Message>,
-  webviews: HashMap<WindowId, WebView>,
+  webviews: Mutex<HashMap<WindowId, WebView>>,
   task_tx: Sender<MainThreadTask>,
   window_event_listeners: WindowEventListeners,
   menu_event_listeners: MenuEventListeners,
+  system_tray_event_listeners: SystemTrayEventListeners,
   task_rx: Receiver<MainThreadTask>,
 }
 
@@ -782,11 +796,12 @@ impl Runtime for Wry {
       task_rx,
       window_event_listeners: Default::default(),
       menu_event_listeners: Default::default(),
+      system_tray_event_listeners: HashMap::default(),
     })
   }
 
   fn create_window<M: Params<Runtime = Self>>(
-    &mut self,
+    &self,
     pending: PendingWindow<M>,
   ) -> crate::Result<DetachedWindow<M>> {
     let label = pending.label.clone();
@@ -812,16 +827,54 @@ impl Runtime for Wry {
       },
     };
 
-    self.webviews.insert(webview.window().id(), webview);
+    self
+      .webviews
+      .lock()
+      .unwrap()
+      .insert(webview.window().id(), webview);
 
     Ok(DetachedWindow { label, dispatcher })
   }
 
+  #[cfg(target_os = "linux")]
+  fn system_tray<I: MenuId>(
+    &self,
+    icon: std::path::PathBuf,
+    menu: Vec<SystemTrayMenuItem<I>>,
+  ) -> crate::Result<()> {
+    SystemTrayBuilder::new(icon, menu.into_iter().map(Into::into).collect())
+      .build(&self.event_loop)
+      .map_err(|e| crate::Error::SystemTray(Box::new(e)))?;
+    Ok(())
+  }
+
+  #[cfg(not(target_os = "linux"))]
+  fn system_tray<I: MenuId>(
+    &self,
+    icon: Vec<u8>,
+    menu: Vec<SystemTrayMenuItem<I>>,
+  ) -> crate::Result<()> {
+    SystemTrayBuilder::new(icon, menu.into_iter().map(Into::into).collect())
+      .build(&self.event_loop)
+      .map_err(|e| crate::Error::SystemTray(Box::new(e)))?;
+    Ok(())
+  }
+
+  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.insert(id, Box::new(f));
+    id
+  }
+
   fn run(self) {
-    let mut webviews = self.webviews;
+    let mut webviews = {
+      let mut lock = self.webviews.lock().expect("poisoned webview collection");
+      std::mem::take(&mut *lock)
+    };
     let task_rx = self.task_rx;
     let window_event_listeners = self.window_event_listeners.clone();
     let menu_event_listeners = self.menu_event_listeners.clone();
+    let system_tray_event_listeners = self.system_tray_event_listeners;
     self.event_loop.run(move |event, event_loop, control_flow| {
       *control_flow = ControlFlow::Wait;
 
@@ -841,12 +894,23 @@ impl Runtime for Wry {
           origin: MenuType::Menubar,
         } => {
           let event = MenuEvent {
-            menu_item_id: MenuItemId(menu_id.0),
+            menu_item_id: menu_id.0,
           };
           for handler in menu_event_listeners.lock().unwrap().values() {
             handler(&event);
           }
         }
+        Event::MenuEvent {
+          menu_id,
+          origin: MenuType::SystemTray,
+        } => {
+          let event = SystemTrayEvent {
+            menu_item_id: menu_id.0,
+          };
+          for handler in system_tray_event_listeners.values() {
+            handler(&event);
+          }
+        }
         Event::WindowEvent { event, window_id } => {
           if let Some(event) = WindowEventWrapper::from(&event).0 {
             for handler in window_event_listeners.lock().unwrap().values() {

+ 51 - 19
core/tauri/src/runtime/manager.rs

@@ -14,9 +14,10 @@ use crate::{
   plugin::PluginStore,
   runtime::{
     app::{GlobalMenuEventListener, WindowMenuEvent},
+    menu::{Menu, MenuId, MenuItem},
     tag::{tags_to_javascript_array, Tag, TagRef, ToJsString},
     webview::{
-      CustomProtocol, FileDropEvent, FileDropHandler, InvokePayload, Menu, WebviewRpcHandler,
+      CustomProtocol, FileDropEvent, FileDropHandler, InvokePayload, WebviewRpcHandler,
       WindowBuilder,
     },
     window::{dpi::PhysicalSize, DetachedWindow, MenuEvent, PendingWindow, WindowEvent},
@@ -82,43 +83,58 @@ pub struct InnerWindowManager<P: Params> {
   /// The webview protocols protocols available to all windows.
   uri_scheme_protocols: HashMap<String, Arc<CustomProtocol>>,
   /// The menu set to all windows.
-  menu: Vec<Menu>,
+  menu: Vec<Menu<P::MenuId>>,
   /// Menu event listeners to all windows.
   menu_event_listeners: Arc<Vec<GlobalMenuEventListener<P>>>,
+  menu_ids: HashMap<u32, P::MenuId>,
 }
 
 /// A [Zero Sized Type] marker representing a full [`Params`].
 ///
 /// [Zero Sized Type]: https://doc.rust-lang.org/nomicon/exotic-sizes.html#zero-sized-types-zsts
-pub struct Args<E: Tag, L: Tag, A: Assets, R: Runtime> {
+pub struct Args<E: Tag, L: Tag, MID: MenuId, TID: MenuId, A: Assets, R: Runtime> {
   _event: PhantomData<fn() -> E>,
   _label: PhantomData<fn() -> L>,
+  _menu_id: PhantomData<fn() -> MID>,
+  _tray_menu_id: PhantomData<fn() -> TID>,
   _assets: PhantomData<fn() -> A>,
   _runtime: PhantomData<fn() -> R>,
 }
 
-impl<E: Tag, L: Tag, A: Assets, R: Runtime> Default for Args<E, L, A, R> {
+impl<E: Tag, L: Tag, MID: MenuId, TID: MenuId, A: Assets, R: Runtime> Default
+  for Args<E, L, MID, TID, A, R>
+{
   fn default() -> Self {
     Self {
       _event: PhantomData,
       _label: PhantomData,
+      _menu_id: PhantomData,
+      _tray_menu_id: PhantomData,
       _assets: PhantomData,
       _runtime: PhantomData,
     }
   }
 }
 
-impl<E: Tag, L: Tag, A: Assets, R: Runtime> ParamsBase for Args<E, L, A, R> {}
-impl<E: Tag, L: Tag, A: Assets, R: Runtime> Params for Args<E, L, A, R> {
+impl<E: Tag, L: Tag, MID: MenuId, TID: MenuId, A: Assets, R: Runtime> ParamsBase
+  for Args<E, L, MID, TID, A, R>
+{
+}
+impl<E: Tag, L: Tag, MID: MenuId, TID: MenuId, A: Assets, R: Runtime> Params
+  for Args<E, L, MID, TID, A, R>
+{
   type Event = E;
   type Label = L;
+  type MenuId = MID;
+  type SystemTrayMenuId = TID;
   type Assets = A;
   type Runtime = R;
 }
 
 pub struct WindowManager<P: Params> {
   pub inner: Arc<InnerWindowManager<P>>,
-  _marker: Args<P::Event, P::Label, P::Assets, P::Runtime>,
+  #[allow(clippy::type_complexity)]
+  _marker: Args<P::Event, P::Label, P::MenuId, P::SystemTrayMenuId, P::Assets, P::Runtime>,
 }
 
 impl<P: Params> Clone for WindowManager<P> {
@@ -130,6 +146,18 @@ impl<P: Params> Clone for WindowManager<P> {
   }
 }
 
+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());
+      }
+    }
+  }
+  map
+}
+
 impl<P: Params> WindowManager<P> {
   #[allow(clippy::too_many_arguments)]
   pub(crate) fn with_handlers(
@@ -139,9 +167,10 @@ impl<P: Params> WindowManager<P> {
     on_page_load: Box<OnPageLoad<P>>,
     uri_scheme_protocols: HashMap<String, Arc<CustomProtocol>>,
     state: StateManager,
-    menu: Vec<Menu>,
+    menu: Vec<Menu<P::MenuId>>,
     menu_event_listeners: Vec<GlobalMenuEventListener<P>>,
   ) -> Self {
+    let menu_ids = get_menu_ids(&menu);
     Self {
       inner: Arc::new(InnerWindowManager {
         windows: Mutex::default(),
@@ -158,6 +187,7 @@ impl<P: Params> WindowManager<P> {
         uri_scheme_protocols,
         menu,
         menu_event_listeners: Arc::new(menu_event_listeners),
+        menu_ids,
       }),
       _marker: Args::default(),
     }
@@ -422,16 +452,17 @@ mod test {
   #[test]
   fn check_get_url() {
     let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate);
-    let manager: WindowManager<Args<String, String, _, Wry>> = WindowManager::with_handlers(
-      context,
-      PluginStore::default(),
-      Box::new(|_| ()),
-      Box::new(|_, _| ()),
-      Default::default(),
-      StateManager::new(),
-      Vec::new(),
-      Default::default(),
-    );
+    let manager: WindowManager<Args<String, String, String, String, _, Wry>> =
+      WindowManager::with_handlers(
+        context,
+        PluginStore::default(),
+        Box::new(|_| ()),
+        Box::new(|_, _| ()),
+        Default::default(),
+        StateManager::new(),
+        Vec::new(),
+        Default::default(),
+      );
 
     #[cfg(custom_protocol)]
     assert_eq!(manager.get_url(), "tauri://localhost");
@@ -517,12 +548,13 @@ impl<P: Params> WindowManager<P> {
     });
     let window_ = window.clone();
     let menu_event_listeners = self.inner.menu_event_listeners.clone();
+    let menu_ids = self.inner.menu_ids.clone();
     window.on_menu_event(move |event| {
       let _ = on_menu_event(&window_, event);
       for handler in menu_event_listeners.iter() {
         handler(WindowMenuEvent {
           window: window_.clone(),
-          menu_item_id: event.menu_item_id,
+          menu_item_id: menu_ids.get(&event.menu_item_id).unwrap().clone(),
         });
       }
     });

+ 209 - 0
core/tauri/src/runtime/menu.rs

@@ -0,0 +1,209 @@
+// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::{
+  collections::hash_map::DefaultHasher,
+  fmt::Debug,
+  hash::{Hash, Hasher},
+};
+
+/// A type that can be derived into a menu id.
+pub trait MenuId: Hash + Eq + Debug + Clone + Send + Sync + 'static {}
+
+impl<T> MenuId for T where T: Hash + Eq + Debug + Clone + Send + Sync + 'static {}
+
+/// A window menu.
+#[derive(Debug, Clone)]
+pub struct Menu<I: MenuId> {
+  pub(crate) title: String,
+  pub(crate) items: Vec<MenuItem<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 {
+    Self {
+      title: title.into(),
+      items,
+    }
+  }
+}
+/// A custom menu item.
+#[derive(Debug, Clone)]
+pub struct CustomMenuItem<I: MenuId> {
+  pub(crate) id: I,
+  pub(crate) name: String,
+}
+
+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 }
+  }
+
+  pub(crate) fn id_value(&self) -> u32 {
+    let mut s = DefaultHasher::new();
+    self.id.hash(&mut s);
+    s.finish() as u32
+  }
+}
+
+/// System tray menu item.
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum SystemTrayMenuItem<I: MenuId> {
+  /// A custom menu item.
+  Custom(CustomMenuItem<I>),
+  /// A separator.
+  Separator,
+}
+
+/// 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>),
+
+  /// Shows a standard "About" item
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  About(String),
+
+  /// A standard "hide the app" menu item.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  Hide,
+
+  /// A standard "Services" menu item.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Linux / Android / iOS:** Unsupported
+  ///
+  Services,
+
+  /// A "hide all other windows" menu item.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Linux / Android / iOS:** Unsupported
+  ///
+  HideOthers,
+
+  /// A menu item to show all the windows for this app.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Linux / Android / iOS:** Unsupported
+  ///
+  ShowAll,
+
+  /// Close the current window.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  CloseWindow,
+
+  /// A "quit this app" menu icon.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  Quit,
+
+  /// A menu item for enabling copying (often text) from responders.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  Copy,
+
+  /// A menu item for enabling cutting (often text) from responders.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  Cut,
+
+  /// An "undo" menu item; particularly useful for supporting the cut/copy/paste/undo lifecycle
+  /// of events.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Linux / Android / iOS:** Unsupported
+  ///
+  Undo,
+
+  /// An "redo" menu item; particularly useful for supporting the cut/copy/paste/undo lifecycle
+  /// of events.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Linux / Android / iOS:** Unsupported
+  ///
+  Redo,
+
+  /// A menu item for selecting all (often text) from responders.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  SelectAll,
+
+  /// A menu item for pasting (often text) into responders.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  Paste,
+
+  /// A standard "enter full screen" item.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Linux / Android / iOS:** Unsupported
+  ///
+  EnterFullScreen,
+
+  /// An item for minimizing the window with the standard system controls.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  Minimize,
+
+  /// An item for instructing the app to zoom
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Linux / Android / iOS:** Unsupported
+  ///
+  Zoom,
+
+  /// Represents a Separator
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Windows / Android / iOS:** Unsupported
+  ///
+  Separator,
+}

+ 28 - 1
core/tauri/src/runtime/mod.rs

@@ -13,18 +13,26 @@ use uuid::Uuid;
 pub(crate) mod app;
 pub mod flavors;
 pub(crate) mod manager;
+/// Create window and system tray menus.
+pub mod menu;
 /// Types useful for interacting with a user's monitors.
 pub mod monitor;
 pub mod tag;
 pub mod webview;
 pub mod window;
 
+use menu::{MenuId, SystemTrayMenuItem};
 use monitor::Monitor;
 use window::{
   dpi::{PhysicalPosition, PhysicalSize, Position, Size},
   MenuEvent, WindowEvent,
 };
 
+/// A system tray event.
+pub struct SystemTrayEvent {
+  pub(crate) menu_item_id: u32,
+}
+
 /// The webview runtime interface.
 pub trait Runtime: Sized + 'static {
   /// The message dispatcher.
@@ -35,10 +43,29 @@ pub trait Runtime: Sized + 'static {
 
   /// Create a new webview window.
   fn create_window<P: Params<Runtime = Self>>(
-    &mut self,
+    &self,
     pending: PendingWindow<P>,
   ) -> crate::Result<DetachedWindow<P>>;
 
+  /// Adds the icon to the system tray with the specified menu items.
+  #[cfg(target_os = "linux")]
+  fn system_tray<I: MenuId>(
+    &self,
+    icon: std::path::PathBuf,
+    menu: Vec<SystemTrayMenuItem<I>>,
+  ) -> crate::Result<()>;
+
+  /// Adds the icon to the system tray with the specified menu items.
+  #[cfg(not(target_os = "linux"))]
+  fn system_tray<I: MenuId>(
+    &self,
+    icon: Vec<u8>,
+    menu: Vec<SystemTrayMenuItem<I>>,
+  ) -> crate::Result<()>;
+
+  /// Registers a system tray event handler.
+  fn on_system_tray_event<F: Fn(&SystemTrayEvent) + Send + 'static>(&mut self, f: F) -> Uuid;
+
   /// Run the webview runtime.
   fn run(self);
 }

+ 7 - 215
core/tauri/src/runtime/webview.rs

@@ -7,15 +7,14 @@
 use crate::runtime::Icon;
 use crate::{
   api::config::{WindowConfig, WindowUrl},
-  runtime::window::DetachedWindow,
+  runtime::{
+    menu::{Menu, MenuId},
+    window::DetachedWindow,
+  },
 };
-use serde::{Deserialize, Serialize};
+use serde::Deserialize;
 use serde_json::Value as JsonValue;
-use std::{
-  collections::{hash_map::DefaultHasher, HashMap},
-  hash::{Hash, Hasher},
-  path::PathBuf,
-};
+use std::{collections::HashMap, path::PathBuf};
 
 type UriSchemeProtocol = dyn Fn(&str) -> crate::Result<Vec<u8>> + Send + Sync + 'static;
 
@@ -97,7 +96,7 @@ pub trait WindowBuilder: WindowBuilderBase {
   fn with_config(config: WindowConfig) -> Self;
 
   /// Sets the menu for the window.
-  fn menu(self, menu: Vec<Menu>) -> Self;
+  fn menu<I: MenuId>(self, menu: Vec<Menu<I>>) -> Self;
 
   /// The initial position of the window's.
   fn position(self, x: f64, y: f64) -> Self;
@@ -189,210 +188,3 @@ pub(crate) struct InvokePayload {
   #[serde(flatten)]
   pub(crate) inner: JsonValue,
 }
-
-/// A window or system tray menu.
-#[derive(Debug, Clone)]
-pub struct Menu {
-  pub(crate) title: String,
-  pub(crate) items: Vec<MenuItem>,
-}
-
-impl Menu {
-  /// Creates a new menu with the given title and items.
-  pub fn new<T: Into<String>>(title: T, items: Vec<MenuItem>) -> Self {
-    Self {
-      title: title.into(),
-      items,
-    }
-  }
-}
-
-/// Identifier of a custom menu item.
-///
-/// Whenever you receive an event arising from a particular menu, this event contains a `MenuId` which
-/// identifies its origin.
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize)]
-pub struct MenuItemId(pub(crate) u32);
-
-impl MenuItemId {
-  fn new<T: Into<String>>(menu_title: T) -> Self {
-    Self(hash_string_to_u32(menu_title.into()))
-  }
-}
-
-fn hash_string_to_u32(title: String) -> u32 {
-  let mut s = DefaultHasher::new();
-  title.hash(&mut s);
-  s.finish() as u32
-}
-
-/// A custom menu item.
-#[derive(Debug, Clone)]
-pub struct CustomMenuItem {
-  pub(crate) id: MenuItemId,
-  pub(crate) name: String,
-}
-
-impl CustomMenuItem {
-  /// Create new custom menu item.
-  pub fn new<T: Into<String>>(title: T) -> Self {
-    let title = title.into();
-    Self {
-      id: MenuItemId::new(&title),
-      name: title,
-    }
-  }
-
-  /// Return unique menu ID. Works only with `MenuItem::Custom`.
-  pub fn id(self) -> MenuItemId {
-    self.id
-  }
-}
-
-/// 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 {
-  /// A custom menu item emits an event inside the EventLoop.
-  Custom(CustomMenuItem),
-
-  /// Shows a standard "About" item
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Android / iOS:** Unsupported
-  ///
-  About(String),
-
-  /// A standard "hide the app" menu item.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Android / iOS:** Unsupported
-  ///
-  Hide,
-
-  /// A standard "Services" menu item.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Linux / Android / iOS:** Unsupported
-  ///
-  Services,
-
-  /// A "hide all other windows" menu item.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Linux / Android / iOS:** Unsupported
-  ///
-  HideOthers,
-
-  /// A menu item to show all the windows for this app.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Linux / Android / iOS:** Unsupported
-  ///
-  ShowAll,
-
-  /// Close the current window.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Android / iOS:** Unsupported
-  ///
-  CloseWindow,
-
-  /// A "quit this app" menu icon.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Android / iOS:** Unsupported
-  ///
-  Quit,
-
-  /// A menu item for enabling copying (often text) from responders.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Android / iOS:** Unsupported
-  ///
-  Copy,
-
-  /// A menu item for enabling cutting (often text) from responders.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Android / iOS:** Unsupported
-  ///
-  Cut,
-
-  /// An "undo" menu item; particularly useful for supporting the cut/copy/paste/undo lifecycle
-  /// of events.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Linux / Android / iOS:** Unsupported
-  ///
-  Undo,
-
-  /// An "redo" menu item; particularly useful for supporting the cut/copy/paste/undo lifecycle
-  /// of events.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Linux / Android / iOS:** Unsupported
-  ///
-  Redo,
-
-  /// A menu item for selecting all (often text) from responders.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Android / iOS:** Unsupported
-  ///
-  SelectAll,
-
-  /// A menu item for pasting (often text) into responders.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Android / iOS:** Unsupported
-  ///
-  Paste,
-
-  /// A standard "enter full screen" item.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Linux / Android / iOS:** Unsupported
-  ///
-  EnterFullScreen,
-
-  /// An item for minimizing the window with the standard system controls.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Android / iOS:** Unsupported
-  ///
-  Minimize,
-
-  /// An item for instructing the app to zoom
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Linux / Android / iOS:** Unsupported
-  ///
-  Zoom,
-
-  /// Represents a Separator
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Android / iOS:** Unsupported
-  ///
-  Separator,
-}

+ 7 - 20
core/tauri/src/runtime/window.rs

@@ -10,7 +10,7 @@ use crate::{
   hooks::{InvokeMessage, InvokeResolver, PageLoadPayload},
   runtime::{
     tag::ToJsString,
-    webview::{FileDropHandler, InvokePayload, MenuItemId, WebviewAttributes, WebviewRpcHandler},
+    webview::{FileDropHandler, InvokePayload, WebviewAttributes, WebviewRpcHandler},
     Dispatch, Monitor, Runtime,
   },
   sealed::{ManagerBase, RuntimeOrDispatch},
@@ -59,14 +59,7 @@ pub enum WindowEvent {
 #[derive(Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct MenuEvent {
-  pub(crate) menu_item_id: MenuItemId,
-}
-
-impl MenuEvent {
-  /// Returns the id of the menu item that triggered the event.
-  pub fn item_id(&self) -> MenuItemId {
-    self.menu_item_id
-  }
+  pub(crate) menu_item_id: u32,
 }
 
 /// A webview window that has yet to be built.
@@ -213,10 +206,6 @@ pub(crate) mod export {
     fn manager(&self) -> &WindowManager<P> {
       &self.manager
     }
-
-    fn runtime(&mut self) -> RuntimeOrDispatch<'_, P> {
-      RuntimeOrDispatch::Dispatch(self.dispatcher())
-    }
   }
 
   impl<'de, P: Params> CommandArg<'de, P> for Window<P> {
@@ -238,7 +227,7 @@ pub(crate) mod export {
       label: P::Label,
       url: WindowUrl,
       setup: F,
-    ) -> crate::Result<()>
+    ) -> crate::Result<Window<P>>
     where
       F: FnOnce(
         <<P::Runtime as Runtime>::Dispatcher as Dispatch>::WindowBuilder,
@@ -252,12 +241,10 @@ pub(crate) mod export {
         <<P::Runtime as Runtime>::Dispatcher as Dispatch>::WindowBuilder::new(),
         WebviewAttributes::new(url),
       );
-      self.create_new_window(PendingWindow::new(
-        window_attributes,
-        webview_attributes,
-        label,
-      ))?;
-      Ok(())
+      self.create_new_window(
+        RuntimeOrDispatch::Dispatch(self.dispatcher()),
+        PendingWindow::new(window_attributes, webview_attributes, label),
+      )
     }
 
     /// The current window's dispatcher.

+ 12 - 0
examples/api/src-tauri/src/main.rs

@@ -11,6 +11,7 @@ mod cmd;
 mod menu;
 
 use serde::Serialize;
+use tauri::{CustomMenuItem, Manager, SystemTrayMenuItem};
 
 #[derive(Serialize)]
 struct Reply {
@@ -36,6 +37,17 @@ fn main() {
     .on_menu_event(|event| {
       println!("{:?}", event.menu_item_id());
     })
+    .system_tray(vec![SystemTrayMenuItem::Custom(CustomMenuItem::new(
+      "toggle".into(),
+      "Toggle",
+    ))])
+    .on_system_tray_event(|app, event| {
+      if event.menu_item_id() == "toggle" {
+        let window = app.get_window("main").unwrap();
+        // TODO: window.is_visible API
+        window.hide().unwrap();
+      }
+    })
     .invoke_handler(tauri::generate_handler![
       cmd::log_operation,
       cmd::perform_request

+ 55 - 50
examples/api/src-tauri/src/menu.rs

@@ -4,60 +4,65 @@
 
 use tauri::{CustomMenuItem, Menu, MenuItem};
 
-pub fn get_menu() -> Vec<Menu> {
-  let custom_print_menu = MenuItem::Custom(CustomMenuItem::new("Print"));
-  let other_test_menu = MenuItem::Custom(CustomMenuItem::new("Custom"));
-  let quit_menu = MenuItem::Custom(CustomMenuItem::new("Quit"));
+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"));
 
   // 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 = 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("Custom help"))],
-    ),
-  ];
+  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",
+        ))],
+      ),
+    ]
+  };
 
   // Attention, Windows only support custom menu for now.
   // If we add any `MenuItem::*` they'll not render

+ 3 - 0
examples/api/src-tauri/tauri.conf.json

@@ -79,6 +79,9 @@
     ],
     "security": {
       "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline' img-src: 'self'"
+    },
+    "systemTray": {
+      "iconPath": "../../.icons/icon.png"
     }
   }
 }

+ 12 - 0
tooling/cli.rs/config_definition.rs

@@ -545,6 +545,8 @@ pub struct TauriConfig {
   /// The updater configuration.
   #[serde(default = "default_updater")]
   pub updater: UpdaterConfig,
+  /// Configuration for app system tray.
+  pub system_tray: Option<SystemTrayConfig>,
 }
 
 impl TauriConfig {
@@ -570,6 +572,16 @@ pub struct UpdaterConfig {
   pub pubkey: Option<String>,
 }
 
+#[skip_serializing_none]
+#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize, JsonSchema)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+pub struct SystemTrayConfig {
+  /// Path to the icon to use on the system tray.
+  ///
+  /// It is forced to be a `.png` file on Linux and macOS, and a `.ico` file on Windows.
+  pub icon_path: PathBuf,
+}
+
 // We enable the unnecessary_wraps because we need
 // to use an Option for dialog otherwise the CLI schema will mark
 // the dialog as a required field which is not as we default it to true.

+ 24 - 0
tooling/cli.rs/schema.json

@@ -862,6 +862,19 @@
       },
       "additionalProperties": false
     },
+    "SystemTrayConfig": {
+      "type": "object",
+      "required": [
+        "iconPath"
+      ],
+      "properties": {
+        "iconPath": {
+          "description": "Path to the icon to use on the system tray.\n\nIt is forced to be a `.png` file on Linux and macOS, and a `.ico` file on Windows.",
+          "type": "string"
+        }
+      },
+      "additionalProperties": false
+    },
     "TauriConfig": {
       "description": "The Tauri configuration object.",
       "type": "object",
@@ -959,6 +972,17 @@
             }
           ]
         },
+        "systemTray": {
+          "description": "Configuration for app system tray.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/SystemTrayConfig"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
         "updater": {
           "description": "The updater configuration.",
           "default": {

+ 21 - 2
tooling/cli.rs/src/build/rust.rs

@@ -164,7 +164,11 @@ impl AppSettings {
   }
 
   pub fn get_bundle_settings(&self, config: &Config) -> crate::Result<BundleSettings> {
-    tauri_config_to_bundle_settings(config.tauri.bundle.clone(), config.tauri.updater.clone())
+    tauri_config_to_bundle_settings(
+      config.tauri.bundle.clone(),
+      config.tauri.system_tray.clone(),
+      config.tauri.updater.clone(),
+    )
   }
 
   pub fn get_out_dir(&self, debug: bool) -> crate::Result<PathBuf> {
@@ -333,6 +337,7 @@ pub fn get_workspace_dir(current_dir: &Path) -> PathBuf {
 
 fn tauri_config_to_bundle_settings(
   config: crate::helpers::config::BundleConfig,
+  system_tray_config: Option<crate::helpers::config::SystemTrayConfig>,
   updater_config: crate::helpers::config::UpdaterConfig,
 ) -> crate::Result<BundleSettings> {
   #[cfg(windows)]
@@ -345,10 +350,24 @@ fn tauri_config_to_bundle_settings(
   );
   #[cfg(not(windows))]
   let windows_icon_path = PathBuf::from("");
+
+  #[allow(unused_mut)]
+  let mut resources = config.resources.unwrap_or_default();
+  #[cfg(target_os = "linux")]
+  {
+    if let Some(system_tray_config) = &system_tray_config {
+      resources.push(system_tray_config.icon_path.to_string_lossy().to_string());
+    }
+  }
+
   Ok(BundleSettings {
     identifier: config.identifier,
     icon: config.icon,
-    resources: config.resources,
+    resources: if resources.is_empty() {
+      None
+    } else {
+      Some(resources)
+    },
     copyright: config.copyright,
     category: match config.category {
       Some(category) => Some(AppCategory::from_str(&category).map_err(|e| match e {