Ver código fonte

refactor: move to `muda` and `tray_icon` crates (#7535)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.studio>
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
Amr Bashir 2 anos atrás
pai
commit
7fb419c326
79 arquivos alterados com 5509 adições e 3888 exclusões
  1. 5 0
      .changes/config-tray-icon-tooltip.md
  2. 5 0
      .changes/config-tray-icon.md
  3. 6 0
      .changes/runtime-create-window-handler.md
  4. 5 0
      .changes/runtime-defaultvbox.md
  5. 6 0
      .changes/runtime-menu-system-tray.md
  6. 6 0
      .changes/runtime-new-args.md
  7. 6 0
      .changes/system-tray-feat.md
  8. 5 0
      .changes/tauri-app-handle-ref.md
  9. 5 0
      .changes/tauri-cleanup-before-exit.md
  10. 5 0
      .changes/tauri-defaultvbox.md
  11. 5 0
      .changes/tauri-libxdo-feat.md
  12. 17 0
      .changes/tauri-menu-tray-refactor.md
  13. 5 0
      .changes/tauri-nsview.md
  14. 5 0
      .changes/tauri-run_on_main_thread.md
  15. 5 0
      .changes/tauri-tray-icon-feat-flag.md
  16. 1 1
      .github/workflows/lint-core.yml
  17. 1 1
      .github/workflows/test-core.yml
  18. 0 1
      Cargo.toml
  19. 6 1
      core/tauri-build/src/allowlist.rs
  20. 1 1
      core/tauri-build/src/codegen/context.rs
  21. 4 6
      core/tauri-build/src/mobile.rs
  22. 1 1
      core/tauri-codegen/README.md
  23. 11 13
      core/tauri-codegen/src/context.rs
  24. 1 1
      core/tauri-codegen/src/lib.rs
  25. 14 7
      core/tauri-config-schema/schema.json
  26. 5 6
      core/tauri-runtime-wry/Cargo.toml
  27. 361 622
      core/tauri-runtime-wry/src/lib.rs
  28. 0 238
      core/tauri-runtime-wry/src/system_tray.rs
  29. 2 2
      core/tauri-runtime/Cargo.toml
  30. 32 223
      core/tauri-runtime/src/lib.rs
  31. 0 747
      core/tauri-runtime/src/menu.rs
  32. 1 8
      core/tauri-runtime/src/webview.rs
  33. 36 54
      core/tauri-runtime/src/window.rs
  34. 21 17
      core/tauri-utils/src/config.rs
  35. 12 7
      core/tauri/Cargo.toml
  36. 485 283
      core/tauri/src/app.rs
  37. 0 704
      core/tauri/src/app/tray.rs
  38. 21 0
      core/tauri/src/error.rs
  39. 29 18
      core/tauri/src/jni_helpers.rs
  40. 73 39
      core/tauri/src/lib.rs
  41. 146 75
      core/tauri/src/manager.rs
  42. 90 0
      core/tauri/src/menu/builders/check.rs
  43. 128 0
      core/tauri/src/menu/builders/icon.rs
  44. 321 0
      core/tauri/src/menu/builders/menu.rs
  45. 20 0
      core/tauri/src/menu/builders/mod.rs
  46. 68 0
      core/tauri/src/menu/builders/normal.rs
  47. 342 0
      core/tauri/src/menu/builders/submenu.rs
  48. 148 0
      core/tauri/src/menu/check.rs
  49. 214 0
      core/tauri/src/menu/icon.rs
  50. 397 0
      core/tauri/src/menu/menu.rs
  51. 251 0
      core/tauri/src/menu/mod.rs
  52. 134 0
      core/tauri/src/menu/normal.rs
  53. 287 0
      core/tauri/src/menu/predefined.rs
  54. 328 0
      core/tauri/src/menu/submenu.rs
  55. 7 7
      core/tauri/src/path/desktop.rs
  56. 1 1
      core/tauri/src/path/mod.rs
  57. 37 31
      core/tauri/src/plugin/mobile.rs
  58. 28 114
      core/tauri/src/test/mock_runtime.rs
  59. 4 4
      core/tauri/src/test/mod.rs
  60. 351 0
      core/tauri/src/tray.rs
  61. 417 39
      core/tauri/src/window.rs
  62. 0 155
      core/tauri/src/window/menu.rs
  63. 0 0
      examples/api/dist/assets/index.css
  64. 0 0
      examples/api/dist/assets/index.js
  65. 247 211
      examples/api/src-tauri/Cargo.lock
  66. 1 1
      examples/api/src-tauri/Cargo.toml
  67. 34 0
      examples/api/src-tauri/src/cmd.rs
  68. 33 12
      examples/api/src-tauri/src/lib.rs
  69. 98 106
      examples/api/src-tauri/src/tray.rs
  70. 0 5
      examples/api/src-tauri/tauri.conf.json
  71. 6 2
      examples/api/src/App.svelte
  72. 19 7
      examples/api/src/views/Welcome.svelte
  73. 1 5
      examples/splashscreen/main.rs
  74. 39 33
      tooling/cli/Cargo.lock
  75. 14 7
      tooling/cli/schema.json
  76. 0 59
      tooling/cli/src/build.rs
  77. 82 13
      tooling/cli/src/interface/rust.rs
  78. 4 0
      tooling/cli/src/migrate/config.rs
  79. 3 0
      tooling/cli/src/migrate/manifest.rs

+ 5 - 0
.changes/config-tray-icon-tooltip.md

@@ -0,0 +1,5 @@
+---
+'tauri-utils': 'minor:feat'
+---
+
+Add option to specify a tooltip text for the tray icon in the config.

+ 5 - 0
.changes/config-tray-icon.md

@@ -0,0 +1,5 @@
+---
+'tauri-utils': 'major:breaking'
+---
+
+`systemTray` config option has been renamed to `trayIcon`.

+ 6 - 0
.changes/runtime-create-window-handler.md

@@ -0,0 +1,6 @@
+---
+'tauri-runtime': 'minor:breaking'
+'tauri-runtime-wry': 'minor:breaking'
+---
+
+`Dispatch::create_window`, `Runtime::create_window` and `RuntimeHandle::create_window` has been changed to accept a 3rd parameter which is a closure that takes `RawWindow` and to be executed right after the window is created and before the webview is added to the window.

+ 5 - 0
.changes/runtime-defaultvbox.md

@@ -0,0 +1,5 @@
+---
+'tauri-runtime-wry': 'minor:feat'
+---
+
+Add `Dispatch::default_vbox`

+ 6 - 0
.changes/runtime-menu-system-tray.md

@@ -0,0 +1,6 @@
+---
+'tauri-runtime': 'major:breaking'
+'tauri-runtime-wry': 'major:breaking'
+---
+
+System tray and menu related APIs and structs have all been removed and are now implemented in tauri outside of the runtime-space.

+ 6 - 0
.changes/runtime-new-args.md

@@ -0,0 +1,6 @@
+---
+'tauri-runtime': 'minor:breaking'
+'tauri-runtime-wry': 'minor:breaking'
+---
+
+`Runtime::new` and `Runtime::new_any_thread` now accept a `RuntimeInitArgs`.

+ 6 - 0
.changes/system-tray-feat.md

@@ -0,0 +1,6 @@
+---
+'tauri-runtime': 'major:breaking'
+'tauri-runtime-wry': 'major:breaking'
+---
+
+Removed `system-tray` feature flag

+ 5 - 0
.changes/tauri-app-handle-ref.md

@@ -0,0 +1,5 @@
+---
+'tauri': 'major:breaking'
+---
+
+Changed `App::handle` and `Manager::app_handle` to return a reference to an `AppHandle` instead of an owned value.

+ 5 - 0
.changes/tauri-cleanup-before-exit.md

@@ -0,0 +1,5 @@
+---
+'tauri': 'minor:feat'
+---
+
+Add `App::cleanup_before_exit` and `AppHandle::cleanup_before_exit` to manually call the cleanup logic. **You should always exit the tauri app immediately after this function returns and not use any tauri-related APIs.**

+ 5 - 0
.changes/tauri-defaultvbox.md

@@ -0,0 +1,5 @@
+---
+'tauri': 'minor:feat'
+---
+
+On Linux, add `Window::default_vbox` to get a reference to the `gtk::Box` that contains the menu bar and the webview.

+ 5 - 0
.changes/tauri-libxdo-feat.md

@@ -0,0 +1,5 @@
+---
+'tauri': 'minor:feat'
+---
+
+Add `linux-libxdo` feature flag (disabled by default) to enable linking to `libxdo` which is used to make `Cut`, `Copy`, `Paste` and `SelectAll` native menu items work on Linux.

+ 17 - 0
.changes/tauri-menu-tray-refactor.md

@@ -0,0 +1,17 @@
+---
+'tauri': 'major:breaking'
+---
+
+The tray icon and menu have received a huge refactor with a lot of breaking changes in order to add new functionalities and improve the DX around using them and here is an overview of the changes:
+
+- All menu and tray types are now exported from `tauri::menu` and `tauri::tray` modules with new names so make sure to check the new types.
+- Removed `tauri::Builder::system_tray`, instead you should use `tauri::tray::TrayIconBuilder` inside `tauri::Builder::setup` hook to create your tray icons.
+- Changed `tauri::Builder::menu` to be a function to accomodate for new menu changes, you can passe `tauri::menu::Menu::default` to it to create a default menu.
+- Renamed `tauri::Context` methods `system_tray_icon`, `tauri::Context::system_tray_icon_mut` and `tauri::Context::set_system_tray_icon` to `tauri::Context::tray_icon`, `tauri::Context::tray_icon_mut` and `tauri::Context::set_tray_icon` to be consistent with new type names.
+- Added `RunEvent::MenuEvent` and `RunEvent::TrayIconEvent`.
+- Added `App/AppHandle::set_menu`, `App/AppHandle::remove_menu`, `App/AppHandle::show_menu`, `App/AppHandle::hide_menu` and `App/AppHandle::menu` to access, remove, hide or show the app-wide menu that is used as the global menu on macOS and on all windows that don't have a specific menu set for it on Windows and Linux.
+- Added `Window::set_menu`, `Window::remove_menu`, `Window::show_menu`, `Window::hide_menu`, `Window::is_menu_visible` and `Window::menu` to access, remove, hide or show the menu on this window.
+- Added `Window::popup_menu` and `Window::popup_menu_at` to show a context menu on the window at the cursor position or at a specific position. You can also popup a context menu using `popup` and `popup_at` methods from `ContextMenu` trait which is implemented for `Menu` and `Submenu` types.
+- Added `App/AppHandle::tray`, `App/AppHandle::tray_by_id`, `App/AppHandle::remove_tray` and `App/AppHandle::remove_tray_by_id` to access or remove a registered tray.
+- Added `WindowBuilder/App/AppHandle::on_menu_event` to register a new menu event handler.
+- Added `App/AppHandle::on_tray_icon_event` to register a new tray event handler.

+ 5 - 0
.changes/tauri-nsview.md

@@ -0,0 +1,5 @@
+---
+'tauri': 'minor:feat'
+---
+
+On macOS, add `Window::ns_view` to get a pointer to the NSWindow content view.

+ 5 - 0
.changes/tauri-run_on_main_thread.md

@@ -0,0 +1,5 @@
+---
+'tauri': 'minor:feat'
+---
+
+Expose `run_on_main_thread` method on `App` that is similar to `AppHandle::run_on_main_thread`.

+ 5 - 0
.changes/tauri-tray-icon-feat-flag.md

@@ -0,0 +1,5 @@
+---
+'tauri': 'major:breaking'
+---
+
+Renamed `system-tray` feature flag to `tray-icon`.

+ 1 - 1
.github/workflows/lint-core.yml

@@ -50,7 +50,7 @@ jobs:
         clippy:
           - { args: '', key: 'empty' }
           - {
-              args: '--features compression,wry,isolation,custom-protocol,system-tray,test',
+              args: '--features compression,wry,linux-ipc-protocol,isolation,custom-protocol,tray-icon,test',
               key: 'all'
             }
           - { args: '--features custom-protocol', key: 'custom-protocol' }

+ 1 - 1
.github/workflows/test-core.yml

@@ -72,7 +72,7 @@ jobs:
               key: no-default
             }
           - {
-              args: --features compression,wry,isolation,custom-protocol,system-tray,test,
+              args: --features compression,wry,linux-ipc-protocol,isolation,custom-protocol,tray-icon,test,
               key: all
             }
 

+ 0 - 1
Cargo.toml

@@ -18,7 +18,6 @@ exclude = [
   # examples that can be compiled with the tauri CLI
   "examples/api/src-tauri",
   "examples/resources/src-tauri",
-  "examples/sidecar/src-tauri",
   "examples/web/core",
   "examples/file-associations/src-tauri",
   "examples/workspace",

+ 6 - 1
core/tauri-build/src/allowlist.rs

@@ -43,7 +43,12 @@ pub fn check(config: &Config, manifest: &mut Manifest) -> Result<()> {
       name: "tauri".into(),
       alias: None,
       kind: DependencyKind::Normal,
-      all_cli_managed_features: Some(TauriConfig::all_features()),
+      all_cli_managed_features: Some(
+        TauriConfig::all_features()
+          .into_iter()
+          .filter(|f| f != &"tray-icon")
+          .collect(),
+      ),
       expected_features: config
         .tauri
         .features()

+ 1 - 1
core/tauri-build/src/codegen/context.rs

@@ -120,7 +120,7 @@ impl CodegenContext {
         config_parent.join(icon).display()
       );
     }
-    if let Some(tray_icon) = config.tauri.system_tray.as_ref().map(|t| &t.icon_path) {
+    if let Some(tray_icon) = config.tauri.tray_icon.as_ref().map(|t| &t.icon_path) {
       println!(
         "cargo:rerun-if-changed={}",
         config_parent.join(tray_icon).display()

+ 4 - 6
core/tauri-build/src/mobile.rs

@@ -222,7 +222,7 @@ fn insert_into_xml(xml: &str, block_identifier: &str, parent_tag: &str, contents
     rewritten.push(line.to_string());
   }
 
-  rewritten.join("\n").to_string()
+  rewritten.join("\n")
 }
 
 pub fn update_android_manifest(block_identifier: &str, parent: &str, insert: String) -> Result<()> {
@@ -294,16 +294,14 @@ dependencies {"
 mod tests {
   #[test]
   fn insert_into_xml() {
-    let manifest = format!(
-      r#"<manifest>
+    let manifest = r#"<manifest>
     <application>
         <intent-filter>
         </intent-filter>
     </application>
-</manifest>"#
-    );
+</manifest>"#;
     let id = "tauritest";
-    let new = super::insert_into_xml(&manifest, id, "application", "<something></something>");
+    let new = super::insert_into_xml(manifest, id, "application", "<something></something>");
 
     let block_id_comment = super::xml_block_comment(id);
     let expected = format!(

+ 1 - 1
core/tauri-codegen/README.md

@@ -24,7 +24,7 @@ Tauri apps can have custom menus and have tray-type interfaces. They can be upda
 
 ## This module
 
-- Embed, hash, and compress assets, including icons for the app as well as the system-tray.
+- Embed, hash, and compress assets, including icons for the app as well as the tray icon.
 - Parse `tauri.conf.json` at compile time and generate the Config struct.
 
 To learn more about the details of how all of these pieces fit together, please consult this [ARCHITECTURE.md](https://github.com/tauri-apps/tauri/blob/dev/ARCHITECTURE.md) document.

+ 11 - 13
core/tauri-codegen/src/context.rs

@@ -264,8 +264,8 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
         );
         png_icon(&root, &out_dir, icon_path).map(|i| quote!(::std::option::Option::Some(#i)))?
       }
-    } else if target == Target::Linux {
-      // handle default window icons for Linux targets
+    } else {
+      // handle default window icons for Unix targets
       let icon_path = find_icon(
         &config,
         &config_parent,
@@ -273,8 +273,6 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
         "icons/icon.png",
       );
       png_icon(&root, &out_dir, icon_path).map(|i| quote!(::std::option::Option::Some(#i)))?
-    } else {
-      quote!(::std::option::Option::None)
     }
   };
 
@@ -319,16 +317,16 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
     }
   );
 
-  let with_system_tray_icon_code = if target.is_desktop() {
-    if let Some(tray) = &config.tauri.system_tray {
-      let system_tray_icon_path = config_parent.join(&tray.icon_path);
-      let ext = system_tray_icon_path.extension();
+  let with_tray_icon_code = if target.is_desktop() {
+    if let Some(tray) = &config.tauri.tray_icon {
+      let tray_icon_icon_path = config_parent.join(&tray.icon_path);
+      let ext = tray_icon_icon_path.extension();
       if ext.map_or(false, |e| e == "ico") {
-        ico_icon(&root, &out_dir, system_tray_icon_path)
-          .map(|i| quote!(context.set_system_tray_icon(#i);))?
+        ico_icon(&root, &out_dir, tray_icon_icon_path)
+          .map(|i| quote!(context.set_tray_icon(#i);))?
       } else if ext.map_or(false, |e| e == "png") {
-        png_icon(&root, &out_dir, system_tray_icon_path)
-          .map(|i| quote!(context.set_system_tray_icon(#i);))?
+        png_icon(&root, &out_dir, tray_icon_icon_path)
+          .map(|i| quote!(context.set_tray_icon(#i);))?
       } else {
         quote!(compile_error!(
           "The tray icon extension must be either `.ico` or `.png`."
@@ -432,7 +430,7 @@ pub fn context_codegen(data: ContextData) -> Result<TokenStream, EmbeddedAssetsE
       #info_plist,
       #pattern,
     );
-    #with_system_tray_icon_code
+    #with_tray_icon_code
     context
   }))
 }

+ 1 - 1
core/tauri-codegen/src/lib.rs

@@ -4,7 +4,7 @@
 
 //! [![](https://github.com/tauri-apps/tauri/raw/dev/.github/splash.png)](https://tauri.app)
 //!
-//! - Embed, hash, and compress assets, including icons for the app as well as the system-tray.
+//! - Embed, hash, and compress assets, including icons for the app as well as the tray icon.
 //! - Parse `tauri.conf.json` at compile time and generate the Config struct.
 
 #![doc(

+ 14 - 7
core/tauri-config-schema/schema.json

@@ -1,7 +1,7 @@
 {
   "$schema": "http://json-schema.org/draft-07/schema#",
   "title": "Config",
-  "description": "The Tauri configuration object. It is read from a file where you can define your frontend assets, configure the bundler and define a system tray.\n\nThe configuration file is generated by the [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in your Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively. The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`. The TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can read a platform-specific configuration from `tauri.linux.conf.json`, `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json` (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used), which gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`package`](#packageconfig): Package settings - [`tauri`](#tauriconfig): The Tauri config - [`build`](#buildconfig): The build configuration - [`plugins`](#pluginconfig): The plugins config\n\n```json title=\"Example tauri.config.json file\" { \"build\": { \"beforeBuildCommand\": \"\", \"beforeDevCommand\": \"\", \"devPath\": \"../dist\", \"distDir\": \"../dist\" }, \"package\": { \"productName\": \"tauri-app\", \"version\": \"0.1.0\" }, \"tauri\": { \"bundle\": {}, \"security\": { \"csp\": null }, \"windows\": [ { \"fullscreen\": false, \"height\": 600, \"resizable\": true, \"title\": \"Tauri App\", \"width\": 800 } ] } } ```",
+  "description": "The Tauri configuration object. It is read from a file where you can define your frontend assets, configure the bundler and define a tray icon.\n\nThe configuration file is generated by the [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in your Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively. The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`. The TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can read a platform-specific configuration from `tauri.linux.conf.json`, `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json` (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used), which gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`package`](#packageconfig): Package settings - [`tauri`](#tauriconfig): The Tauri config - [`build`](#buildconfig): The build configuration - [`plugins`](#pluginconfig): The plugins config\n\n```json title=\"Example tauri.config.json file\" { \"build\": { \"beforeBuildCommand\": \"\", \"beforeDevCommand\": \"\", \"devPath\": \"../dist\", \"distDir\": \"../dist\" }, \"package\": { \"productName\": \"tauri-app\", \"version\": \"0.1.0\" }, \"tauri\": { \"bundle\": {}, \"security\": { \"csp\": null }, \"windows\": [ { \"fullscreen\": false, \"height\": 600, \"resizable\": true, \"title\": \"Tauri App\", \"width\": 800 } ] } } ```",
   "type": "object",
   "properties": {
     "$schema": {
@@ -223,11 +223,11 @@
             }
           ]
         },
-        "systemTray": {
-          "description": "Configuration for app system tray.",
+        "trayIcon": {
+          "description": "Configuration for app tray icon.",
           "anyOf": [
             {
-              "$ref": "#/definitions/SystemTrayConfig"
+              "$ref": "#/definitions/TrayIconConfig"
             },
             {
               "type": "null"
@@ -2065,15 +2065,15 @@
         }
       ]
     },
-    "SystemTrayConfig": {
-      "description": "Configuration for application system tray icon.\n\nSee more: https://tauri.app/v1/api/config#systemtrayconfig",
+    "TrayIconConfig": {
+      "description": "Configuration for application tray icon.\n\nSee more: https://tauri.app/v1/api/config#trayiconconfig",
       "type": "object",
       "required": [
         "iconPath"
       ],
       "properties": {
         "iconPath": {
-          "description": "Path to the default icon to use on the system tray.",
+          "description": "Path to the default icon to use for the tray icon.",
           "type": "string"
         },
         "iconAsTemplate": {
@@ -2092,6 +2092,13 @@
             "string",
             "null"
           ]
+        },
+        "tooltip": {
+          "description": "Tray icon tooltip on Windows and macOS",
+          "type": [
+            "string",
+            "null"
+          ]
         }
       },
       "additionalProperties": false

+ 5 - 6
core/tauri-runtime-wry/Cargo.toml

@@ -16,7 +16,7 @@ rust-version = { workspace = true }
 features = [ "dox" ]
 
 [dependencies]
-wry = { version = "0.30", default-features = false, features = [ "file-drop", "protocol" ] }
+wry = { version = "0.31", default-features = false, features = [ "file-drop", "protocol" ] }
 tauri-runtime = { version = "0.13.0-alpha.6", path = "../tauri-runtime" }
 tauri-utils = { version = "2.0.0-alpha.6", path = "../tauri-utils" }
 uuid = { version = "1", features = [ "v4" ] }
@@ -26,9 +26,9 @@ raw-window-handle = "0.5"
 [target."cfg(windows)".dependencies]
 webview2-com = "0.25"
 
-  [target."cfg(windows)".dependencies.windows]
-  version = "0.48"
-  features = [ "Win32_Foundation" ]
+[target."cfg(windows)".dependencies.windows]
+version = "0.48"
+features = [ "Win32_Foundation" ]
 
 [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
 gtk = { version = "0.16", features = [ "v3_24" ] }
@@ -39,12 +39,11 @@ percent-encoding = "2.1"
 cocoa = "0.24"
 
 [target."cfg(target_os = \"android\")".dependencies]
-jni = "0.20"
+jni = "0.21"
 
 [features]
 dox = [ "wry/dox" ]
 devtools = [ "wry/devtools", "tauri-runtime/devtools" ]
-system-tray = [ "tauri-runtime/system-tray", "wry/tray" ]
 macos-private-api = [
   "wry/fullscreen",
   "wry/transparent",

Diferenças do arquivo suprimidas por serem muito extensas
+ 361 - 622
core/tauri-runtime-wry/src/lib.rs


+ 0 - 238
core/tauri-runtime-wry/src/system_tray.rs

@@ -1,238 +0,0 @@
-// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-License-Identifier: MIT
-
-pub use tauri_runtime::{
-  menu::{
-    Menu, MenuEntry, MenuItem, MenuUpdate, Submenu, SystemTrayMenu, SystemTrayMenuEntry,
-    SystemTrayMenuItem, TrayHandle,
-  },
-  Icon, SystemTrayEvent,
-};
-use wry::application::event_loop::EventLoopWindowTarget;
-pub use wry::application::{
-  event::TrayEvent,
-  event_loop::EventLoopProxy,
-  menu::{
-    ContextMenu as WryContextMenu, CustomMenuItem as WryCustomMenuItem, MenuItem as WryMenuItem,
-  },
-  system_tray::Icon as WryTrayIcon,
-  TrayId as WryTrayId,
-};
-
-#[cfg(target_os = "macos")]
-pub use wry::application::platform::macos::{
-  CustomMenuItemExtMacOS, SystemTrayBuilderExtMacOS, SystemTrayExtMacOS,
-};
-
-use wry::application::system_tray::{SystemTray as WrySystemTray, SystemTrayBuilder};
-
-use crate::{send_user_message, Context, Error, Message, Result, TrayId, TrayMessage};
-
-use tauri_runtime::{menu::MenuHash, SystemTray, UserEvent};
-
-use std::{
-  collections::HashMap,
-  fmt,
-  sync::{Arc, Mutex},
-};
-
-pub type GlobalSystemTrayEventHandler = Box<dyn Fn(TrayId, &SystemTrayEvent) + Send>;
-pub type GlobalSystemTrayEventListeners = Arc<Mutex<Vec<Arc<GlobalSystemTrayEventHandler>>>>;
-
-pub type SystemTrayEventHandler = Box<dyn Fn(&SystemTrayEvent) + Send>;
-pub type SystemTrayEventListeners = Arc<Mutex<Vec<Arc<SystemTrayEventHandler>>>>;
-pub type SystemTrayItems = Arc<Mutex<HashMap<u16, WryCustomMenuItem>>>;
-
-#[derive(Clone, Default)]
-pub struct TrayContext {
-  pub tray: Arc<Mutex<Option<WrySystemTray>>>,
-  pub listeners: SystemTrayEventListeners,
-  pub items: SystemTrayItems,
-}
-
-impl fmt::Debug for TrayContext {
-  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-    f.debug_struct("TrayContext")
-      .field("items", &self.items)
-      .finish()
-  }
-}
-
-#[derive(Clone, Default)]
-pub struct SystemTrayManager {
-  pub trays: Arc<Mutex<HashMap<TrayId, TrayContext>>>,
-  pub global_listeners: GlobalSystemTrayEventListeners,
-}
-
-impl fmt::Debug for SystemTrayManager {
-  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-    f.debug_struct("SystemTrayManager")
-      .field("trays", &self.trays)
-      .finish()
-  }
-}
-
-/// Wrapper around a [`wry::application::system_tray::Icon`] that can be created from an [`WindowIcon`].
-pub struct TrayIcon(pub(crate) WryTrayIcon);
-
-impl TryFrom<Icon> for TrayIcon {
-  type Error = Error;
-  fn try_from(icon: Icon) -> std::result::Result<Self, Self::Error> {
-    WryTrayIcon::from_rgba(icon.rgba, icon.width, icon.height)
-      .map(Self)
-      .map_err(crate::icon_err)
-  }
-}
-
-pub fn create_tray<T>(
-  id: WryTrayId,
-  system_tray: SystemTray,
-  event_loop: &EventLoopWindowTarget<T>,
-) -> crate::Result<(WrySystemTray, HashMap<u16, WryCustomMenuItem>)> {
-  let icon = TrayIcon::try_from(system_tray.icon.expect("tray icon not set"))?;
-
-  let mut items = HashMap::new();
-
-  #[allow(unused_mut)]
-  let mut builder = SystemTrayBuilder::new(
-    icon.0,
-    system_tray
-      .menu
-      .map(|menu| to_wry_context_menu(&mut items, menu)),
-  )
-  .with_id(id);
-
-  #[cfg(target_os = "macos")]
-  {
-    builder = builder
-      .with_icon_as_template(system_tray.icon_as_template)
-      .with_menu_on_left_click(system_tray.menu_on_left_click);
-
-    if let Some(title) = system_tray.title {
-      builder = builder.with_title(&title);
-    }
-  }
-
-  if let Some(tooltip) = system_tray.tooltip {
-    builder = builder.with_tooltip(&tooltip);
-  }
-
-  let tray = builder
-    .build(event_loop)
-    .map_err(|e| Error::SystemTray(Box::new(e)))?;
-
-  Ok((tray, items))
-}
-
-#[derive(Debug, Clone)]
-pub struct SystemTrayHandle<T: UserEvent> {
-  pub(crate) context: Context<T>,
-  pub(crate) id: TrayId,
-  pub(crate) proxy: EventLoopProxy<super::Message<T>>,
-}
-
-impl<T: UserEvent> TrayHandle for SystemTrayHandle<T> {
-  fn set_icon(&self, icon: Icon) -> Result<()> {
-    self
-      .proxy
-      .send_event(Message::Tray(self.id, TrayMessage::UpdateIcon(icon)))
-      .map_err(|_| Error::FailedToSendMessage)
-  }
-
-  fn set_menu(&self, menu: SystemTrayMenu) -> Result<()> {
-    self
-      .proxy
-      .send_event(Message::Tray(self.id, TrayMessage::UpdateMenu(menu)))
-      .map_err(|_| Error::FailedToSendMessage)
-  }
-
-  fn update_item(&self, id: u16, update: MenuUpdate) -> Result<()> {
-    self
-      .proxy
-      .send_event(Message::Tray(self.id, TrayMessage::UpdateItem(id, update)))
-      .map_err(|_| Error::FailedToSendMessage)
-  }
-
-  #[cfg(target_os = "macos")]
-  fn set_icon_as_template(&self, is_template: bool) -> tauri_runtime::Result<()> {
-    self
-      .proxy
-      .send_event(Message::Tray(
-        self.id,
-        TrayMessage::UpdateIconAsTemplate(is_template),
-      ))
-      .map_err(|_| Error::FailedToSendMessage)
-  }
-
-  #[cfg(target_os = "macos")]
-  fn set_title(&self, title: &str) -> tauri_runtime::Result<()> {
-    self
-      .proxy
-      .send_event(Message::Tray(
-        self.id,
-        TrayMessage::UpdateTitle(title.to_owned()),
-      ))
-      .map_err(|_| Error::FailedToSendMessage)
-  }
-
-  fn set_tooltip(&self, tooltip: &str) -> Result<()> {
-    self
-      .proxy
-      .send_event(Message::Tray(
-        self.id,
-        TrayMessage::UpdateTooltip(tooltip.to_owned()),
-      ))
-      .map_err(|_| Error::FailedToSendMessage)
-  }
-
-  fn destroy(&self) -> Result<()> {
-    let (tx, rx) = std::sync::mpsc::channel();
-    send_user_message(
-      &self.context,
-      Message::Tray(self.id, TrayMessage::Destroy(tx)),
-    )?;
-    rx.recv().unwrap()?;
-    Ok(())
-  }
-}
-
-impl From<SystemTrayMenuItem> for crate::MenuItemWrapper {
-  fn from(item: SystemTrayMenuItem) -> Self {
-    match item {
-      SystemTrayMenuItem::Separator => Self(WryMenuItem::Separator),
-      _ => unimplemented!(),
-    }
-  }
-}
-
-pub fn to_wry_context_menu(
-  custom_menu_items: &mut HashMap<MenuHash, WryCustomMenuItem>,
-  menu: SystemTrayMenu,
-) -> 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(crate::MenuItemAttributesWrapper::from(&c).0);
-        #[cfg(target_os = "macos")]
-        if let Some(native_image) = c.native_image {
-          item.set_native_image(crate::NativeImageWrapper::from(native_image).0);
-        }
-        custom_menu_items.insert(c.id, item);
-      }
-      SystemTrayMenuEntry::NativeItem(i) => {
-        tray_menu.add_native_item(crate::MenuItemWrapper::from(i).0);
-      }
-      SystemTrayMenuEntry::Submenu(submenu) => {
-        tray_menu.add_submenu(
-          &submenu.title,
-          submenu.enabled,
-          to_wry_context_menu(custom_menu_items, submenu.inner),
-        );
-      }
-    }
-  }
-  tray_menu
-}

+ 2 - 2
core/tauri-runtime/Cargo.toml

@@ -42,12 +42,12 @@ features = [ "Win32_Foundation" ]
 gtk = { version = "0.16", features = [ "v3_24" ] }
 
 [target."cfg(target_os = \"android\")".dependencies]
-jni = "0.20"
+jni = "0.21"
 
 [target."cfg(target_os = \"macos\")".dependencies]
 url = "2"
 
 [features]
 devtools = [ ]
-system-tray = [ ]
 macos-private-api = [ ]
+

+ 32 - 223
core/tauri-runtime/src/lib.rs

@@ -20,8 +20,6 @@ use url::Url;
 use uuid::Uuid;
 
 pub mod http;
-/// Create window and system tray menus.
-pub mod menu;
 /// Types useful for interacting with a user's monitors.
 pub mod monitor;
 pub mod webview;
@@ -31,7 +29,7 @@ use monitor::Monitor;
 use webview::WindowBuilder;
 use window::{
   dpi::{PhysicalPosition, PhysicalSize, Position, Size},
-  CursorIcon, DetachedWindow, PendingWindow, WindowEvent,
+  CursorIcon, DetachedWindow, PendingWindow, RawWindow, WindowEvent,
 };
 
 use crate::http::{
@@ -41,156 +39,6 @@ use crate::http::{
   InvalidUri,
 };
 
-#[cfg(all(desktop, feature = "system-tray"))]
-use std::fmt;
-
-pub type TrayId = u16;
-pub type TrayEventHandler = dyn Fn(&SystemTrayEvent) + Send + 'static;
-
-#[cfg(all(desktop, feature = "system-tray"))]
-#[non_exhaustive]
-pub struct SystemTray {
-  pub id: TrayId,
-  pub icon: Option<Icon>,
-  pub menu: Option<menu::SystemTrayMenu>,
-  #[cfg(target_os = "macos")]
-  pub icon_as_template: bool,
-  #[cfg(target_os = "macos")]
-  pub menu_on_left_click: bool,
-  #[cfg(target_os = "macos")]
-  pub title: Option<String>,
-  pub on_event: Option<Box<TrayEventHandler>>,
-  pub tooltip: Option<String>,
-}
-
-#[cfg(all(desktop, feature = "system-tray"))]
-impl fmt::Debug for SystemTray {
-  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-    let mut d = f.debug_struct("SystemTray");
-    d.field("id", &self.id)
-      .field("icon", &self.icon)
-      .field("menu", &self.menu);
-    #[cfg(target_os = "macos")]
-    {
-      d.field("icon_as_template", &self.icon_as_template)
-        .field("menu_on_left_click", &self.menu_on_left_click)
-        .field("title", &self.title);
-    }
-    d.finish()
-  }
-}
-
-#[cfg(all(desktop, feature = "system-tray"))]
-impl Clone for SystemTray {
-  fn clone(&self) -> Self {
-    Self {
-      id: self.id,
-      icon: self.icon.clone(),
-      menu: self.menu.clone(),
-      on_event: None,
-      #[cfg(target_os = "macos")]
-      icon_as_template: self.icon_as_template,
-      #[cfg(target_os = "macos")]
-      menu_on_left_click: self.menu_on_left_click,
-      #[cfg(target_os = "macos")]
-      title: self.title.clone(),
-      tooltip: self.tooltip.clone(),
-    }
-  }
-}
-
-#[cfg(all(desktop, feature = "system-tray"))]
-impl Default for SystemTray {
-  fn default() -> Self {
-    Self {
-      id: rand::random(),
-      icon: None,
-      menu: None,
-      #[cfg(target_os = "macos")]
-      icon_as_template: false,
-      #[cfg(target_os = "macos")]
-      menu_on_left_click: false,
-      #[cfg(target_os = "macos")]
-      title: None,
-      on_event: None,
-      tooltip: None,
-    }
-  }
-}
-
-#[cfg(all(desktop, feature = "system-tray"))]
-impl SystemTray {
-  /// Creates a new system tray that only renders an icon.
-  pub fn new() -> Self {
-    Default::default()
-  }
-
-  pub fn menu(&self) -> Option<&menu::SystemTrayMenu> {
-    self.menu.as_ref()
-  }
-
-  /// Sets the tray id.
-  #[must_use]
-  pub fn with_id(mut self, id: TrayId) -> Self {
-    self.id = id;
-    self
-  }
-
-  /// Sets the tray icon.
-  #[must_use]
-  pub fn with_icon(mut self, icon: Icon) -> Self {
-    self.icon.replace(icon);
-    self
-  }
-
-  /// Sets the tray icon as template.
-  #[cfg(target_os = "macos")]
-  #[must_use]
-  pub fn with_icon_as_template(mut self, is_template: bool) -> Self {
-    self.icon_as_template = is_template;
-    self
-  }
-
-  /// Sets whether the menu should appear when the tray receives a left click. Defaults to `true`.
-  #[cfg(target_os = "macos")]
-  #[must_use]
-  pub fn with_menu_on_left_click(mut self, menu_on_left_click: bool) -> Self {
-    self.menu_on_left_click = menu_on_left_click;
-    self
-  }
-
-  #[cfg(target_os = "macos")]
-  #[must_use]
-  pub fn with_title(mut self, title: &str) -> Self {
-    self.title = Some(title.to_owned());
-    self
-  }
-
-  /// Sets the tray icon tooltip.
-  ///
-  /// ## Platform-specific:
-  ///
-  /// - **Linux:** Unsupported
-  #[must_use]
-  pub fn with_tooltip(mut self, tooltip: &str) -> Self {
-    self.tooltip = Some(tooltip.to_owned());
-    self
-  }
-
-  /// Sets the menu to show when the system tray is right clicked.
-  #[must_use]
-  pub fn with_menu(mut self, menu: menu::SystemTrayMenu) -> Self {
-    self.menu.replace(menu);
-    self
-  }
-
-  #[must_use]
-  pub fn on_event<F: Fn(&SystemTrayEvent) + Send + 'static>(mut self, f: F) -> Self {
-    self.on_event.replace(Box::new(f));
-    self
-  }
-}
-
 /// Type of user attention requested on a window.
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
 #[serde(tag = "type")]
@@ -243,11 +91,6 @@ pub enum Error {
   /// Failed to serialize/deserialize.
   #[error("JSON error: {0}")]
   Json(#[from] serde_json::Error),
-  /// Encountered an error creating the app system tray.
-  #[cfg(all(desktop, feature = "system-tray"))]
-  #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-  #[error("error encountered during tray setup: {0}")]
-  SystemTray(Box<dyn std::error::Error + Send + Sync>),
   /// Failed to load window icon.
   #[error("invalid icon: {0}")]
   InvalidIcon(Box<dyn std::error::Error + Send + Sync>),
@@ -328,24 +171,6 @@ pub enum ExitRequestedEventAction {
   Prevent,
 }
 
-/// A system tray event.
-#[derive(Debug)]
-pub enum SystemTrayEvent {
-  MenuItemClick(u16),
-  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`.
 #[derive(Debug, Clone, Default)]
 pub struct RunIteration {
@@ -373,22 +198,15 @@ pub trait RuntimeHandle<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'st
   fn create_proxy(&self) -> <Self::Runtime as Runtime<T>>::EventLoopProxy;
 
   /// Create a new webview window.
-  fn create_window(
+  fn create_window<F: Fn(RawWindow) + Send + 'static>(
     &self,
     pending: PendingWindow<T, Self::Runtime>,
+    before_webview_creation: Option<F>,
   ) -> Result<DetachedWindow<T, Self::Runtime>>;
 
   /// Run a task on the main thread.
   fn run_on_main_thread<F: FnOnce() + Send + 'static>(&self, f: F) -> Result<()>;
 
-  /// Adds an icon to the system tray with the specified menu items.
-  #[cfg(all(desktop, feature = "system-tray"))]
-  #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "system-tray"))))]
-  fn system_tray(
-    &self,
-    system_tray: SystemTray,
-  ) -> Result<<Self::Runtime as Runtime<T>>::TrayHandler>;
-
   fn raw_display_handle(&self) -> RawDisplayHandle;
 
   fn primary_monitor(&self) -> Option<Monitor>;
@@ -407,9 +225,9 @@ pub trait RuntimeHandle<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'st
   /// Finds an Android class in the project scope.
   #[cfg(target_os = "android")]
   fn find_class<'a>(
-    &'a self,
-    env: jni::JNIEnv<'a>,
-    activity: jni::objects::JObject<'a>,
+    &self,
+    env: &mut jni::JNIEnv<'a>,
+    activity: &jni::objects::JObject<'_>,
     name: impl Into<String>,
   ) -> std::result::Result<jni::objects::JClass<'a>, jni::errors::Error>;
 
@@ -419,34 +237,35 @@ pub trait RuntimeHandle<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'st
   #[cfg(target_os = "android")]
   fn run_on_android_context<F>(&self, f: F)
   where
-    F: FnOnce(jni::JNIEnv<'_>, jni::objects::JObject<'_>, jni::objects::JObject<'_>)
-      + Send
-      + 'static;
+    F: FnOnce(&mut jni::JNIEnv, &jni::objects::JObject, &jni::objects::JObject) + Send + 'static;
 }
 
 pub trait EventLoopProxy<T: UserEvent>: Debug + Clone + Send + Sync {
   fn send_event(&self, event: T) -> Result<()>;
 }
 
+#[derive(Default)]
+pub struct RuntimeInitArgs {
+  #[cfg(windows)]
+  pub msg_hook: Option<Box<dyn FnMut(*const std::ffi::c_void) -> bool + 'static>>,
+}
+
 /// The webview runtime interface.
 pub trait Runtime<T: UserEvent>: Debug + Sized + 'static {
   /// The message dispatcher.
   type Dispatcher: Dispatch<T, Runtime = Self>;
   /// The runtime handle type.
   type Handle: RuntimeHandle<T, Runtime = Self>;
-  /// The tray handler type.
-  #[cfg(all(desktop, feature = "system-tray"))]
-  type TrayHandler: menu::TrayHandle;
   /// The proxy type.
   type EventLoopProxy: EventLoopProxy<T>;
 
   /// Creates a new webview runtime. Must be used on the main thread.
-  fn new() -> Result<Self>;
+  fn new(args: RuntimeInitArgs) -> Result<Self>;
 
   /// Creates a new webview runtime on any thread.
   #[cfg(any(windows, target_os = "linux"))]
   #[cfg_attr(doc_cfg, doc(cfg(any(windows, target_os = "linux"))))]
-  fn new_any_thread() -> Result<Self>;
+  fn new_any_thread(args: RuntimeInitArgs) -> Result<Self>;
 
   /// Creates an `EventLoopProxy` that can be used to dispatch user events to the main event loop.
   fn create_proxy(&self) -> Self::EventLoopProxy;
@@ -455,17 +274,11 @@ pub trait Runtime<T: UserEvent>: Debug + Sized + 'static {
   fn handle(&self) -> Self::Handle;
 
   /// Create a new webview window.
-  fn create_window(&self, pending: PendingWindow<T, Self>) -> Result<DetachedWindow<T, Self>>;
-
-  /// Adds the icon to the system tray with the specified menu items.
-  #[cfg(all(desktop, feature = "system-tray"))]
-  #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-  fn system_tray(&self, system_tray: SystemTray) -> Result<Self::TrayHandler>;
-
-  /// Registers a system tray event handler.
-  #[cfg(all(desktop, feature = "system-tray"))]
-  #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-  fn on_system_tray_event<F: Fn(TrayId, &SystemTrayEvent) + Send + 'static>(&mut self, f: F);
+  fn create_window<F: Fn(RawWindow) + Send + 'static>(
+    &self,
+    pending: PendingWindow<T, Self>,
+    before_webview_creation: Option<F>,
+  ) -> Result<DetachedWindow<T, Self>>;
 
   fn primary_monitor(&self) -> Option<Monitor>;
   fn available_monitors(&self) -> Vec<Monitor>;
@@ -520,9 +333,6 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
   /// Registers a window event handler.
   fn on_window_event<F: Fn(&WindowEvent) + Send + 'static>(&self, f: F) -> Uuid;
 
-  /// Registers a window event handler.
-  fn on_menu_event<F: Fn(&window::MenuEvent) + Send + 'static>(&self, f: F) -> Uuid;
-
   /// Runs a closure with the platform webview object as argument.
   fn with_webview<F: FnOnce(Box<dyn std::any::Any>) + Send + 'static>(&self, f: F) -> Result<()>;
 
@@ -606,9 +416,6 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
   /// Gets the window's current title.
   fn title(&self) -> Result<String>;
 
-  /// Gets the window menu current visibility state.
-  fn is_menu_visible(&self) -> Result<bool>;
-
   /// Returns the monitor on which the window currently resides.
   ///
   /// Returns None if current monitor can't be detected.
@@ -632,6 +439,16 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
   ))]
   fn gtk_window(&self) -> Result<gtk::ApplicationWindow>;
 
+  /// Returns the vertical [`gtk::Box`] that is added by default as the sole child of this window.
+  #[cfg(any(
+    target_os = "linux",
+    target_os = "dragonfly",
+    target_os = "freebsd",
+    target_os = "netbsd",
+    target_os = "openbsd"
+  ))]
+  fn default_vbox(&self) -> Result<gtk::Box>;
+
   fn raw_window_handle(&self) -> Result<raw_window_handle::RawWindowHandle>;
 
   /// Returns the current window theme.
@@ -651,9 +468,10 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
   fn request_user_attention(&self, request_type: Option<UserAttentionType>) -> Result<()>;
 
   /// Create a new webview window.
-  fn create_window(
+  fn create_window<F: Fn(RawWindow) + Send + 'static>(
     &mut self,
     pending: PendingWindow<T, Self::Runtime>,
+    before_webview_creation: Option<F>,
   ) -> Result<DetachedWindow<T, Self::Runtime>>;
 
   /// Updates the window resizable flag.
@@ -701,12 +519,6 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
   /// Unminimizes the window.
   fn unminimize(&self) -> Result<()>;
 
-  /// Shows the window menu.
-  fn show_menu(&self) -> Result<()>;
-
-  /// Hides the window menu.
-  fn hide_menu(&self) -> Result<()>;
-
   /// Shows the window.
   fn show(&self) -> Result<()>;
 
@@ -780,7 +592,4 @@ pub trait Dispatch<T: UserEvent>: Debug + Clone + Send + Sync + Sized + 'static
 
   /// Executes javascript on the window this [`Dispatch`] represents.
   fn eval_script<S: Into<String>>(&self, script: S) -> Result<()>;
-
-  /// Applies the specified `update` to the menu item associated with the given `id`.
-  fn update_menu_item(&self, id: u16, update: menu::MenuUpdate) -> Result<()>;
 }

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

@@ -1,747 +0,0 @@
-// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-License-Identifier: MIT
-
-use std::{
-  collections::hash_map::DefaultHasher,
-  fmt,
-  hash::{Hash, Hasher},
-};
-
-pub type MenuHash = u16;
-pub type MenuId = String;
-pub type MenuIdRef<'a> = &'a str;
-
-/// 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: fmt::Debug + Clone + Send + Sync {
-  fn set_icon(&self, icon: crate::Icon) -> crate::Result<()>;
-  fn set_menu(&self, menu: crate::menu::SystemTrayMenu) -> crate::Result<()>;
-  fn update_item(&self, id: u16, update: MenuUpdate) -> crate::Result<()>;
-  #[cfg(target_os = "macos")]
-  fn set_icon_as_template(&self, is_template: bool) -> crate::Result<()>;
-  #[cfg(target_os = "macos")]
-  fn set_title(&self, title: &str) -> crate::Result<()>;
-  fn set_tooltip(&self, tooltip: &str) -> crate::Result<()>;
-  fn destroy(&self) -> crate::Result<()>;
-}
-
-/// A window menu.
-#[derive(Debug, Default, Clone)]
-#[non_exhaustive]
-pub struct Menu {
-  pub items: Vec<MenuEntry>,
-}
-
-#[derive(Debug, Clone)]
-#[non_exhaustive]
-pub struct Submenu {
-  pub title: String,
-  pub enabled: bool,
-  pub inner: Menu,
-}
-
-impl Submenu {
-  /// Creates a new submenu with the given title and menu items.
-  pub fn new<S: Into<String>>(title: S, menu: Menu) -> Self {
-    Self {
-      title: title.into(),
-      enabled: true,
-      inner: menu,
-    }
-  }
-}
-
-impl Menu {
-  /// Creates a new window menu.
-  pub fn new() -> Self {
-    Default::default()
-  }
-
-  /// Creates a menu filled with default menu items and submenus.
-  ///
-  /// ## Platform-specific:
-  ///
-  /// - **Windows**:
-  ///   - File
-  ///     - CloseWindow
-  ///     - Quit
-  ///   - Edit
-  ///     - Cut
-  ///     - Copy
-  ///     - Paste
-  ///   - Window
-  ///     - Minimize
-  ///     - CloseWindow
-  ///
-  /// - **Linux**:
-  ///   - File
-  ///     - CloseWindow
-  ///     - Quit
-  ///   - Window
-  ///     - Minimize
-  ///     - CloseWindow
-  ///
-  /// - **macOS**:
-  ///   - App
-  ///     - About
-  ///     - Separator
-  ///     - Services
-  ///     - Separator
-  ///     - Hide
-  ///     - HideOthers
-  ///     - ShowAll
-  ///     - Separator
-  ///     - Quit
-  ///   - File
-  ///     - CloseWindow
-  ///   - Edit
-  ///     - Undo
-  ///     - Redo
-  ///     - Separator
-  ///     - Cut
-  ///     - Copy
-  ///     - Paste
-  ///     - SelectAll
-  ///   - View
-  ///     - EnterFullScreen
-  ///   - Window
-  ///     - Minimize
-  ///     - Zoom
-  ///     - Separator
-  ///     - CloseWindow
-  pub fn os_default(#[allow(unused)] app_name: &str) -> Self {
-    let mut menu = Menu::new();
-    #[cfg(target_os = "macos")]
-    {
-      menu = menu.add_submenu(Submenu::new(
-        app_name,
-        Menu::new()
-          .add_native_item(MenuItem::About(
-            app_name.to_string(),
-            AboutMetadata::default(),
-          ))
-          .add_native_item(MenuItem::Separator)
-          .add_native_item(MenuItem::Services)
-          .add_native_item(MenuItem::Separator)
-          .add_native_item(MenuItem::Hide)
-          .add_native_item(MenuItem::HideOthers)
-          .add_native_item(MenuItem::ShowAll)
-          .add_native_item(MenuItem::Separator)
-          .add_native_item(MenuItem::Quit),
-      ));
-    }
-
-    let mut file_menu = Menu::new();
-    file_menu = file_menu.add_native_item(MenuItem::CloseWindow);
-    #[cfg(not(target_os = "macos"))]
-    {
-      file_menu = file_menu.add_native_item(MenuItem::Quit);
-    }
-    menu = menu.add_submenu(Submenu::new("File", file_menu));
-
-    #[cfg(not(target_os = "linux"))]
-    let mut edit_menu = Menu::new();
-    #[cfg(target_os = "macos")]
-    {
-      edit_menu = edit_menu.add_native_item(MenuItem::Undo);
-      edit_menu = edit_menu.add_native_item(MenuItem::Redo);
-      edit_menu = edit_menu.add_native_item(MenuItem::Separator);
-    }
-    #[cfg(not(target_os = "linux"))]
-    {
-      edit_menu = edit_menu.add_native_item(MenuItem::Cut);
-      edit_menu = edit_menu.add_native_item(MenuItem::Copy);
-      edit_menu = edit_menu.add_native_item(MenuItem::Paste);
-    }
-    #[cfg(target_os = "macos")]
-    {
-      edit_menu = edit_menu.add_native_item(MenuItem::SelectAll);
-    }
-    #[cfg(not(target_os = "linux"))]
-    {
-      menu = menu.add_submenu(Submenu::new("Edit", edit_menu));
-    }
-    #[cfg(target_os = "macos")]
-    {
-      menu = menu.add_submenu(Submenu::new(
-        "View",
-        Menu::new().add_native_item(MenuItem::EnterFullScreen),
-      ));
-    }
-
-    let mut window_menu = Menu::new();
-    window_menu = window_menu.add_native_item(MenuItem::Minimize);
-    #[cfg(target_os = "macos")]
-    {
-      window_menu = window_menu.add_native_item(MenuItem::Zoom);
-      window_menu = window_menu.add_native_item(MenuItem::Separator);
-    }
-    window_menu = window_menu.add_native_item(MenuItem::CloseWindow);
-    menu = menu.add_submenu(Submenu::new("Window", window_menu));
-
-    menu
-  }
-
-  /// Creates a new window menu with the given items.
-  ///
-  /// # Examples
-  /// ```
-  /// # use tauri_runtime::menu::{Menu, MenuItem, CustomMenuItem, Submenu};
-  /// Menu::with_items([
-  ///   MenuItem::SelectAll.into(),
-  ///   #[cfg(target_os = "macos")]
-  ///   MenuItem::Redo.into(),
-  ///   CustomMenuItem::new("toggle", "Toggle visibility").into(),
-  ///   Submenu::new("View", Menu::new()).into(),
-  /// ]);
-  /// ```
-  pub fn with_items<I: IntoIterator<Item = MenuEntry>>(items: I) -> Self {
-    Self {
-      items: items.into_iter().collect(),
-    }
-  }
-
-  /// Adds the custom menu item to the menu.
-  #[must_use]
-  pub fn add_item(mut self, item: CustomMenuItem) -> Self {
-    self.items.push(MenuEntry::CustomItem(item));
-    self
-  }
-
-  /// Adds a native item to the menu.
-  #[must_use]
-  pub fn add_native_item(mut self, item: MenuItem) -> Self {
-    self.items.push(MenuEntry::NativeItem(item));
-    self
-  }
-
-  /// Adds an entry with submenu.
-  #[must_use]
-  pub fn add_submenu(mut self, submenu: Submenu) -> Self {
-    self.items.push(MenuEntry::Submenu(submenu));
-    self
-  }
-}
-
-/// A custom menu item.
-#[derive(Debug, Clone)]
-#[non_exhaustive]
-pub struct CustomMenuItem {
-  pub id: MenuHash,
-  pub id_str: MenuId,
-  pub title: String,
-  pub keyboard_accelerator: Option<String>,
-  pub enabled: bool,
-  pub selected: bool,
-  #[cfg(target_os = "macos")]
-  pub native_image: Option<NativeImage>,
-}
-
-impl CustomMenuItem {
-  /// Create new custom menu item.
-  pub fn new<I: Into<String>, T: Into<String>>(id: I, title: T) -> Self {
-    let id_str = id.into();
-    Self {
-      id: Self::hash(&id_str),
-      id_str,
-      title: title.into(),
-      keyboard_accelerator: None,
-      enabled: true,
-      selected: false,
-      #[cfg(target_os = "macos")]
-      native_image: None,
-    }
-  }
-
-  /// Assign a keyboard shortcut to the menu action.
-  #[must_use]
-  pub fn accelerator<T: Into<String>>(mut self, accelerator: T) -> Self {
-    self.keyboard_accelerator.replace(accelerator.into());
-    self
-  }
-
-  #[cfg(target_os = "macos")]
-  #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
-  #[must_use]
-  /// A native image do render on the menu item.
-  pub fn native_image(mut self, image: NativeImage) -> Self {
-    self.native_image.replace(image);
-    self
-  }
-
-  /// Mark the item as disabled.
-  #[must_use]
-  pub fn disabled(mut self) -> Self {
-    self.enabled = false;
-    self
-  }
-
-  /// Mark the item as selected.
-  #[must_use]
-  pub fn selected(mut self) -> Self {
-    self.selected = true;
-    self
-  }
-
-  fn hash(id: &str) -> MenuHash {
-    let mut hasher = DefaultHasher::new();
-    id.hash(&mut hasher);
-    hasher.finish() as MenuHash
-  }
-}
-
-/// A system tray menu.
-#[derive(Debug, Default, Clone)]
-#[non_exhaustive]
-pub struct SystemTrayMenu {
-  pub items: Vec<SystemTrayMenuEntry>,
-}
-
-#[derive(Debug, Clone)]
-#[non_exhaustive]
-pub struct SystemTraySubmenu {
-  pub title: String,
-  pub enabled: bool,
-  pub inner: SystemTrayMenu,
-}
-
-impl SystemTraySubmenu {
-  /// Creates a new submenu with the given title and menu items.
-  pub fn new<S: Into<String>>(title: S, menu: SystemTrayMenu) -> Self {
-    Self {
-      title: title.into(),
-      enabled: true,
-      inner: menu,
-    }
-  }
-}
-
-impl SystemTrayMenu {
-  /// Creates a new system tray menu.
-  pub fn new() -> Self {
-    Default::default()
-  }
-
-  /// Adds the custom menu item to the system tray menu.
-  #[must_use]
-  pub fn add_item(mut self, item: CustomMenuItem) -> Self {
-    self.items.push(SystemTrayMenuEntry::CustomItem(item));
-    self
-  }
-
-  /// Adds a native item to the system tray menu.
-  #[must_use]
-  pub fn add_native_item(mut self, item: SystemTrayMenuItem) -> Self {
-    self.items.push(SystemTrayMenuEntry::NativeItem(item));
-    self
-  }
-
-  /// Adds an entry with submenu.
-  #[must_use]
-  pub fn add_submenu(mut self, submenu: SystemTraySubmenu) -> Self {
-    self.items.push(SystemTrayMenuEntry::Submenu(submenu));
-    self
-  }
-}
-
-/// An entry on the system tray menu.
-#[derive(Debug, Clone)]
-pub enum SystemTrayMenuEntry {
-  /// A custom item.
-  CustomItem(CustomMenuItem),
-  /// A native item.
-  NativeItem(SystemTrayMenuItem),
-  /// An entry with submenu.
-  Submenu(SystemTraySubmenu),
-}
-
-/// System tray menu item.
-#[derive(Debug, Clone)]
-#[non_exhaustive]
-pub enum SystemTrayMenuItem {
-  /// A separator.
-  Separator,
-}
-
-/// An entry on the system tray menu.
-#[derive(Debug, Clone)]
-pub enum MenuEntry {
-  /// A custom item.
-  CustomItem(CustomMenuItem),
-  /// A native item.
-  NativeItem(MenuItem),
-  /// An entry with submenu.
-  Submenu(Submenu),
-}
-
-impl From<CustomMenuItem> for MenuEntry {
-  fn from(item: CustomMenuItem) -> Self {
-    Self::CustomItem(item)
-  }
-}
-
-impl From<MenuItem> for MenuEntry {
-  fn from(item: MenuItem) -> Self {
-    Self::NativeItem(item)
-  }
-}
-
-impl From<Submenu> for MenuEntry {
-  fn from(submenu: Submenu) -> Self {
-    Self::Submenu(submenu)
-  }
-}
-
-/// Application metadata for the [`MenuItem::About`] action.
-///
-/// ## Platform-specific
-///
-/// - **Windows / macOS / Android / iOS:** The metadata is ignored on these platforms.
-#[derive(Debug, Clone, Default)]
-#[non_exhaustive]
-pub struct AboutMetadata {
-  /// The application name.
-  pub version: Option<String>,
-  /// The authors of the application.
-  pub authors: Option<Vec<String>>,
-  /// Application comments.
-  pub comments: Option<String>,
-  /// The copyright of the application.
-  pub copyright: Option<String>,
-  /// The license of the application.
-  pub license: Option<String>,
-  /// The application website.
-  pub website: Option<String>,
-  /// The website label.
-  pub website_label: Option<String>,
-}
-
-impl AboutMetadata {
-  /// Creates the default metadata for the [`MenuItem::About`] action, which is just empty.
-  pub fn new() -> Self {
-    Default::default()
-  }
-
-  /// Defines the application version.
-  pub fn version(mut self, version: impl Into<String>) -> Self {
-    self.version.replace(version.into());
-    self
-  }
-
-  /// Defines the application authors.
-  pub fn authors(mut self, authors: Vec<String>) -> Self {
-    self.authors.replace(authors);
-    self
-  }
-
-  /// Defines the application comments.
-  pub fn comments(mut self, comments: impl Into<String>) -> Self {
-    self.comments.replace(comments.into());
-    self
-  }
-
-  /// Defines the application copyright.
-  pub fn copyright(mut self, copyright: impl Into<String>) -> Self {
-    self.copyright.replace(copyright.into());
-    self
-  }
-
-  /// Defines the application license.
-  pub fn license(mut self, license: impl Into<String>) -> Self {
-    self.license.replace(license.into());
-    self
-  }
-
-  /// Defines the application's website link.
-  pub fn website(mut self, website: impl Into<String>) -> Self {
-    self.website.replace(website.into());
-    self
-  }
-
-  /// Defines the application's website link name.
-  pub fn website_label(mut self, website_label: impl Into<String>) -> Self {
-    self.website_label.replace(website_label.into());
-    self
-  }
-}
-
-/// 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 {
-  /// Shows a standard "About" item.
-  ///
-  /// The first value is the application name, and the second is its metadata.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Windows / Android / iOS:** Unsupported
-  /// - **Linux:** The metadata is only applied on Linux
-  ///
-  About(String, AboutMetadata),
-
-  /// A standard "hide the app" menu item.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **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
-  ///
-  /// - **Android / iOS:** Unsupported
-  ///
-  CloseWindow,
-
-  /// A "quit this app" menu icon.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Android / iOS:** Unsupported
-  ///
-  Quit,
-
-  /// A menu item for enabling copying (often text) from responders.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Android / iOS / Linux:** Unsupported
-  ///
-  Copy,
-
-  /// A menu item for enabling cutting (often text) from responders.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Android / iOS / Linux:** 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 / Linux:** Unsupported
-  ///
-  SelectAll,
-
-  /// A menu item for pasting (often text) into responders.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Android / iOS / Linux:** 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
-  ///
-  /// - **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
-  ///
-  /// - **Android / iOS:** Unsupported
-  ///
-  Separator,
-}

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

@@ -4,7 +4,7 @@
 
 //! Items specific to the [`Runtime`](crate::Runtime)'s webview.
 
-use crate::{menu::Menu, window::DetachedWindow, Icon};
+use crate::{window::DetachedWindow, Icon};
 
 #[cfg(target_os = "macos")]
 use tauri_utils::TitleBarStyle;
@@ -153,10 +153,6 @@ pub trait WindowBuilder: WindowBuilderBase {
   /// Initializes a new webview builder from a [`WindowConfig`]
   fn with_config(config: WindowConfig) -> Self;
 
-  /// Sets the menu for the window.
-  #[must_use]
-  fn menu(self, menu: Menu) -> Self;
-
   /// Show window in the center of the screen.
   #[must_use]
   fn center(self) -> Self;
@@ -330,9 +326,6 @@ pub trait WindowBuilder: WindowBuilderBase {
 
   /// Whether the icon was set or not.
   fn has_icon(&self) -> bool;
-
-  /// Gets the window menu.
-  fn get_menu(&self) -> Option<&Menu>;
 }
 
 /// IPC handler.

+ 36 - 54
core/tauri-runtime/src/window.rs

@@ -6,19 +6,20 @@
 
 use crate::{
   http::{Request as HttpRequest, Response as HttpResponse},
-  menu::{Menu, MenuEntry, MenuHash, MenuId},
   webview::{WebviewAttributes, WebviewIpcHandler},
   Dispatch, Runtime, UserEvent, WindowBuilder,
 };
-use serde::{Deserialize, Deserializer, Serialize};
+
+use serde::{Deserialize, Deserializer};
 use tauri_utils::{config::WindowConfig, Theme};
 use url::Url;
 
 use std::{
   collections::HashMap,
   hash::{Hash, Hasher},
+  marker::PhantomData,
   path::PathBuf,
-  sync::{mpsc::Sender, Arc, Mutex},
+  sync::mpsc::Sender,
 };
 
 type UriSchemeProtocol =
@@ -82,25 +83,6 @@ pub enum FileDropEvent {
   Cancelled,
 }
 
-/// A menu event.
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct MenuEvent {
-  pub menu_item_id: u16,
-}
-
-fn get_menu_ids(map: &mut HashMap<MenuHash, MenuId>, menu: &Menu) {
-  for item in &menu.items {
-    match item {
-      MenuEntry::CustomItem(c) => {
-        map.insert(c.id, c.id_str.clone());
-      }
-      MenuEntry::Submenu(s) => get_menu_ids(map, &s.inner),
-      _ => {}
-    }
-  }
-}
-
 /// Describes the appearance of the mouse cursor.
 #[non_exhaustive]
 #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
@@ -209,10 +191,10 @@ impl<'de> Deserialize<'de> for CursorIcon {
 }
 
 #[cfg(target_os = "android")]
-pub struct CreationContext<'a> {
-  pub env: jni::JNIEnv<'a>,
-  pub activity: jni::objects::JObject<'a>,
-  pub webview: jni::objects::JObject<'a>,
+pub struct CreationContext<'a, 'b> {
+  pub env: &'a mut jni::JNIEnv<'b>,
+  pub activity: &'a jni::objects::JObject<'b>,
+  pub webview: &'a jni::objects::JObject<'b>,
 }
 
 /// A webview window that has yet to be built.
@@ -231,9 +213,6 @@ pub struct PendingWindow<T: UserEvent, R: Runtime<T>> {
   /// How to handle IPC calls on the webview window.
   pub ipc_handler: Option<WebviewIpcHandler<T, R>>,
 
-  /// Maps runtime id to a string menu id.
-  pub menu_ids: Arc<Mutex<HashMap<MenuHash, MenuId>>>,
-
   /// A handler to decide if incoming url is allowed to navigate.
   pub navigation_handler: Option<Box<NavigationHandler>>,
 
@@ -243,7 +222,7 @@ pub struct PendingWindow<T: UserEvent, R: Runtime<T>> {
   #[cfg(target_os = "android")]
   #[allow(clippy::type_complexity)]
   pub on_webview_created:
-    Option<Box<dyn Fn(CreationContext<'_>) -> Result<(), jni::errors::Error> + Send>>,
+    Option<Box<dyn Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send>>,
 
   pub web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
 }
@@ -268,10 +247,6 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
     webview_attributes: WebviewAttributes,
     label: impl Into<String>,
   ) -> crate::Result<Self> {
-    let mut menu_ids = HashMap::new();
-    if let Some(menu) = window_builder.get_menu() {
-      get_menu_ids(&mut menu_ids, menu);
-    }
     let label = label.into();
     if !is_label_valid(&label) {
       Err(crate::Error::InvalidWindowLabel)
@@ -282,7 +257,6 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
         uri_scheme_protocols: Default::default(),
         label,
         ipc_handler: None,
-        menu_ids: Arc::new(Mutex::new(menu_ids)),
         navigation_handler: Default::default(),
         url: "tauri://localhost".to_string(),
         #[cfg(target_os = "android")]
@@ -300,10 +274,7 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
   ) -> crate::Result<Self> {
     let window_builder =
       <<R::Dispatcher as Dispatch<T>>::WindowBuilder>::with_config(window_config);
-    let mut menu_ids = HashMap::new();
-    if let Some(menu) = window_builder.get_menu() {
-      get_menu_ids(&mut menu_ids, menu);
-    }
+
     let label = label.into();
     if !is_label_valid(&label) {
       Err(crate::Error::InvalidWindowLabel)
@@ -314,7 +285,6 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
         uri_scheme_protocols: Default::default(),
         label,
         ipc_handler: None,
-        menu_ids: Arc::new(Mutex::new(menu_ids)),
         navigation_handler: Default::default(),
         url: "tauri://localhost".to_string(),
         #[cfg(target_os = "android")]
@@ -324,15 +294,6 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
     }
   }
 
-  #[must_use]
-  pub fn set_menu(mut self, menu: Menu) -> Self {
-    let mut menu_ids = HashMap::new();
-    get_menu_ids(&mut menu_ids, &menu);
-    *self.menu_ids.lock().unwrap() = menu_ids;
-    self.window_builder = self.window_builder.menu(menu);
-    self
-  }
-
   pub fn register_uri_scheme_protocol<
     N: Into<String>,
     H: Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync + 'static,
@@ -349,7 +310,7 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
 
   #[cfg(target_os = "android")]
   pub fn on_webview_created<
-    F: Fn(CreationContext<'_>) -> Result<(), jni::errors::Error> + Send + 'static,
+    F: Fn(CreationContext<'_, '_>) -> Result<(), jni::errors::Error> + Send + 'static,
   >(
     mut self,
     f: F,
@@ -367,9 +328,6 @@ pub struct DetachedWindow<T: UserEvent, R: Runtime<T>> {
 
   /// The [`Dispatch`](crate::Dispatch) associated with the window.
   pub dispatcher: R::Dispatcher,
-
-  /// Maps runtime id to a string menu id.
-  pub menu_ids: Arc<Mutex<HashMap<MenuHash, MenuId>>>,
 }
 
 impl<T: UserEvent, R: Runtime<T>> Clone for DetachedWindow<T, R> {
@@ -377,7 +335,6 @@ impl<T: UserEvent, R: Runtime<T>> Clone for DetachedWindow<T, R> {
     Self {
       label: self.label.clone(),
       dispatcher: self.dispatcher.clone(),
-      menu_ids: self.menu_ids.clone(),
     }
   }
 }
@@ -396,3 +353,28 @@ impl<T: UserEvent, R: Runtime<T>> PartialEq for DetachedWindow<T, R> {
     self.label.eq(&other.label)
   }
 }
+
+/// A raw window type that contains fields to access
+/// the HWND on Windows, gtk::ApplicationWindow on Linux and
+/// NSView on macOS.
+pub struct RawWindow<'a> {
+  #[cfg(windows)]
+  pub hwnd: isize,
+  #[cfg(any(
+    target_os = "linux",
+    target_os = "dragonfly",
+    target_os = "freebsd",
+    target_os = "netbsd",
+    target_os = "openbsd"
+  ))]
+  pub gtk_window: &'a gtk::ApplicationWindow,
+  #[cfg(any(
+    target_os = "linux",
+    target_os = "dragonfly",
+    target_os = "freebsd",
+    target_os = "netbsd",
+    target_os = "openbsd"
+  ))]
+  pub default_vbox: Option<&'a gtk::Box>,
+  pub _marker: &'a PhantomData<()>,
+}

+ 21 - 17
core/tauri-utils/src/config.rs

@@ -1416,9 +1416,9 @@ pub struct TauriConfig {
   /// Security configuration.
   #[serde(default)]
   pub security: SecurityConfig,
-  /// Configuration for app system tray.
-  #[serde(alias = "system-tray")]
-  pub system_tray: Option<SystemTrayConfig>,
+  /// Configuration for app tray icon.
+  #[serde(alias = "tray-icon")]
+  pub tray_icon: Option<TrayIconConfig>,
   /// MacOS private API configuration. Enables the transparent background API and sets the `fullScreenEnabled` preference to `true`.
   #[serde(rename = "macOSPrivateApi", alias = "macos-private-api", default)]
   pub macos_private_api: bool,
@@ -1428,7 +1428,7 @@ impl TauriConfig {
   /// Returns all Cargo features.
   pub fn all_features() -> Vec<&'static str> {
     vec![
-      "system-tray",
+      "tray-icon",
       "macos-private-api",
       "isolation",
       "protocol-asset",
@@ -1438,8 +1438,8 @@ impl TauriConfig {
   /// Returns the enabled Cargo features.
   pub fn features(&self) -> Vec<&str> {
     let mut features = Vec::new();
-    if self.system_tray.is_some() {
-      features.push("system-tray");
+    if self.tray_icon.is_some() {
+      features.push("tray-icon");
     }
     if self.macos_private_api {
       features.push("macos-private-api");
@@ -1550,15 +1550,15 @@ pub struct UpdaterWindowsConfig {
   pub install_mode: WindowsUpdateInstallMode,
 }
 
-/// Configuration for application system tray icon.
+/// Configuration for application tray icon.
 ///
-/// See more: https://tauri.app/v1/api/config#systemtrayconfig
+/// See more: https://tauri.app/v1/api/config#trayiconconfig
 #[skip_serializing_none]
 #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
 #[cfg_attr(feature = "schema", derive(JsonSchema))]
 #[serde(rename_all = "camelCase", deny_unknown_fields)]
-pub struct SystemTrayConfig {
-  /// Path to the default icon to use on the system tray.
+pub struct TrayIconConfig {
+  /// Path to the default icon to use for the tray icon.
   #[serde(alias = "icon-path")]
   pub icon_path: PathBuf,
   /// A Boolean value that determines whether the image represents a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) image on macOS.
@@ -1569,6 +1569,8 @@ pub struct SystemTrayConfig {
   pub menu_on_left_click: bool,
   /// Title for MacOS tray
   pub title: Option<String>,
+  /// Tray icon tooltip on Windows and macOS
+  pub tooltip: Option<String>,
 }
 
 /// General configuration for the iOS target.
@@ -1838,7 +1840,7 @@ impl PackageConfig {
 
 /// The Tauri configuration object.
 /// It is read from a file where you can define your frontend assets,
-/// configure the bundler and define a system tray.
+/// configure the bundler and define a tray icon.
 ///
 /// The configuration file is generated by the
 /// [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in
@@ -2566,19 +2568,21 @@ mod build {
     }
   }
 
-  impl ToTokens for SystemTrayConfig {
+  impl ToTokens for TrayIconConfig {
     fn to_tokens(&self, tokens: &mut TokenStream) {
       let icon_as_template = self.icon_as_template;
       let menu_on_left_click = self.menu_on_left_click;
       let icon_path = path_buf_lit(&self.icon_path);
       let title = opt_str_lit(self.title.as_ref());
+      let tooltip = opt_str_lit(self.tooltip.as_ref());
       literal_struct!(
         tokens,
-        SystemTrayConfig,
+        TrayIconConfig,
         icon_path,
         icon_as_template,
         menu_on_left_click,
-        title
+        title,
+        tooltip
       );
     }
   }
@@ -2615,7 +2619,7 @@ mod build {
       let windows = vec_lit(&self.windows, identity);
       let bundle = &self.bundle;
       let security = &self.security;
-      let system_tray = opt_lit(self.system_tray.as_ref());
+      let tray_icon = opt_lit(self.tray_icon.as_ref());
       let macos_private_api = self.macos_private_api;
 
       literal_struct!(
@@ -2625,7 +2629,7 @@ mod build {
         windows,
         bundle,
         security,
-        system_tray,
+        tray_icon,
         macos_private_api
       );
     }
@@ -2718,7 +2722,7 @@ mod test {
         dangerous_remote_domain_ipc_access: Vec::new(),
         asset_protocol: AssetProtocolConfig::default(),
       },
-      system_tray: None,
+      tray_icon: None,
       macos_private_api: false,
     };
 

+ 12 - 7
core/tauri/Cargo.toml

@@ -18,7 +18,6 @@ no-default-features = true
 features = [
   "wry",
   "custom-protocol",
-  "system-tray",
   "devtools",
   "icon-png",
   "protocol-asset",
@@ -68,6 +67,11 @@ infer = { version = "0.9", optional = true }
 png = { version = "0.17", optional = true }
 ico = { version = "0.2.0", optional = true }
 
+
+[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\", target_os = \"windows\", target_os = \"macos\"))".dependencies]
+muda = { version = "0.8", default-features = false }
+tray-icon = { version = "0.8", default-features = false, optional = true }
+
 [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
 gtk = { version = "0.16", features = [ "v3_24" ] }
 glib = "0.16"
@@ -81,16 +85,16 @@ objc = "0.2"
 [target."cfg(windows)".dependencies]
 webview2-com = "0.25"
 
-  [target."cfg(windows)".dependencies.windows]
-  version = "0.48"
-  features = [ "Win32_Foundation" ]
+[target."cfg(windows)".dependencies.windows]
+version = "0.48"
+features = [ "Win32_Foundation" ]
 
 [target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies]
 log = "0.4"
 heck = "0.4"
 
 [target."cfg(target_os = \"android\")".dependencies]
-jni = "0.20"
+jni = "0.21"
 
 [target."cfg(target_os = \"ios\")".dependencies]
 libc = "0.2"
@@ -114,18 +118,19 @@ tokio = { version = "1", features = [ "full" ] }
 cargo_toml = "0.15"
 
 [features]
-default = [ "wry", "compression", "objc-exception" ]
+default = [ "wry", "compression", "objc-exception", "tray-icon?/common-controls-v6", "muda/common-controls-v6" ]
+tray-icon = [ "dep:tray-icon" ]
 test = [ ]
 compression = [ "tauri-macros/compression", "tauri-utils/compression" ]
 wry = [ "tauri-runtime-wry" ]
 objc-exception = [ "tauri-runtime-wry/objc-exception" ]
 linux-ipc-protocol = [ "tauri-runtime-wry/linux-protocol-body", "webkit2gtk/v2_40" ]
+linux-libxdo = [ "tray-icon/libxdo", "muda/libxdo" ]
 isolation = [ "tauri-utils/isolation", "tauri-macros/isolation" ]
 custom-protocol = [ "tauri-macros/custom-protocol" ]
 native-tls = [ "reqwest/native-tls" ]
 native-tls-vendored = [ "reqwest/native-tls-vendored" ]
 rustls-tls = [ "reqwest/rustls-tls" ]
-system-tray = [ "tauri-runtime/system-tray", "tauri-runtime-wry/system-tray" ]
 devtools = [ "tauri-runtime/devtools", "tauri-runtime-wry/devtools" ]
 dox = [ "tauri-runtime-wry/dox" ]
 process-relaunch-dangerous-allow-symlink-macos = [ "tauri-utils/process-relaunch-dangerous-allow-symlink-macos" ]

+ 485 - 283
core/tauri/src/app.rs

@@ -2,9 +2,6 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-#[cfg(all(desktop, feature = "system-tray"))]
-pub(crate) mod tray;
-
 use crate::{
   command::{CommandArg, CommandItem},
   ipc::{
@@ -30,13 +27,24 @@ use crate::{
 #[cfg(feature = "protocol-asset")]
 use crate::scope::FsScope;
 
+#[cfg(desktop)]
+use crate::menu::{Menu, MenuEvent};
+#[cfg(all(desktop, feature = "tray-icon"))]
+use crate::tray::{TrayIcon, TrayIconBuilder, TrayIconEvent, TrayIconId};
+#[cfg(desktop)]
+use crate::window::WindowMenu;
 use raw_window_handle::HasRawDisplayHandle;
 use serde::Deserialize;
 use serialize_to_javascript::{default_template, DefaultTemplate, Template};
 use tauri_macros::default_runtime;
-use tauri_runtime::window::{
-  dpi::{PhysicalPosition, PhysicalSize},
-  FileDropEvent,
+#[cfg(desktop)]
+use tauri_runtime::EventLoopProxy;
+use tauri_runtime::{
+  window::{
+    dpi::{PhysicalPosition, PhysicalSize},
+    FileDropEvent,
+  },
+  RuntimeInitArgs,
 };
 use tauri_utils::PackageInfo;
 
@@ -46,17 +54,17 @@ use std::{
   sync::{mpsc::Sender, Arc, Weak},
 };
 
-use crate::runtime::menu::{Menu, MenuId, MenuIdRef};
-
 use crate::runtime::RuntimeHandle;
 
 #[cfg(target_os = "macos")]
 use crate::ActivationPolicy;
 
-pub(crate) type GlobalMenuEventListener<R> = Box<dyn Fn(WindowMenuEvent<R>) + Send + Sync>;
+#[cfg(desktop)]
+pub(crate) type GlobalMenuEventListener<T> = Box<dyn Fn(&T, crate::menu::MenuEvent) + Send + Sync>;
+#[cfg(all(desktop, feature = "tray-icon"))]
+pub(crate) type GlobalTrayIconEventListener<T> =
+  Box<dyn Fn(&T, crate::tray::TrayIconEvent) + Send + Sync>;
 pub(crate) type GlobalWindowEventListener<R> = Box<dyn Fn(GlobalWindowEvent<R>) + Send + Sync>;
-#[cfg(all(desktop, feature = "system-tray"))]
-type SystemTrayEventListener<R> = Box<dyn Fn(&AppHandle<R>, tray::SystemTrayEvent) + Send + Sync>;
 /// A closure that is run when the Tauri application is setting up.
 pub type SetupHook<R> =
   Box<dyn FnOnce(&mut App<R>) -> Result<(), Box<dyn std::error::Error>> + Send>;
@@ -199,35 +207,29 @@ pub enum RunEvent {
   MainEventsCleared,
   /// Emitted when the user wants to open the specified resource with the app.
   #[cfg(any(target_os = "macos", target_os = "ios"))]
+  #[cfg_attr(doc_cfg, doc(cfg(any(target_os = "macos", feature = "ios"))))]
   Opened {
     /// The URL of the resources that is being open.
     urls: Vec<url::Url>,
   },
+  /// An event from a menu item, could be on the window menu bar, application menu bar (on macOS) or tray icon menu.
+  #[cfg(desktop)]
+  #[cfg_attr(doc_cfg, doc(cfg(desktop)))]
+  MenuEvent(crate::menu::MenuEvent),
+  /// An event from a tray icon.
+  #[cfg(all(desktop, feature = "tray-icon"))]
+  #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
+  TrayIconEvent(crate::tray::TrayIconEvent),
 }
 
 impl From<EventLoopMessage> for RunEvent {
   fn from(event: EventLoopMessage) -> Self {
-    match event {}
-  }
-}
-
-/// A menu event that was triggered on a window.
-#[default_runtime(crate::Wry, wry)]
-#[derive(Debug)]
-pub struct WindowMenuEvent<R: Runtime> {
-  pub(crate) menu_item_id: MenuId,
-  pub(crate) window: Window<R>,
-}
-
-impl<R: Runtime> WindowMenuEvent<R> {
-  /// The menu item id.
-  pub fn menu_item_id(&self) -> MenuIdRef<'_> {
-    &self.menu_item_id
-  }
-
-  /// The window that the menu belongs to.
-  pub fn window(&self) -> &Window<R> {
-    &self.window
+    match event {
+      #[cfg(desktop)]
+      EventLoopMessage::MenuEvent(e) => Self::MenuEvent(e),
+      #[cfg(all(desktop, feature = "tray-icon"))]
+      EventLoopMessage::TrayIconEvent(e) => Self::TrayIconEvent(e),
+    }
   }
 }
 
@@ -345,7 +347,7 @@ impl<R: Runtime> AppHandle<R> {
   ///
   /// tauri::Builder::default()
   ///   .setup(move |app| {
-  ///     let handle = app.handle();
+  ///     let handle = app.handle().clone();
   ///     std::thread::spawn(move || {
   ///       handle.plugin(init_plugin());
   ///     });
@@ -393,7 +395,7 @@ impl<R: Runtime> AppHandle<R> {
   /// tauri::Builder::default()
   ///   .plugin(plugin)
   ///   .setup(move |app| {
-  ///     let handle = app.handle();
+  ///     let handle = app.handle().clone();
   ///     std::thread::spawn(move || {
   ///       handle.remove_plugin(plugin_name);
   ///     });
@@ -422,16 +424,6 @@ impl<R: Runtime> AppHandle<R> {
     self.cleanup_before_exit();
     crate::process::restart(&self.env());
   }
-
-  /// Runs necessary cleanup tasks before exiting the process
-  fn cleanup_before_exit(&self) {
-    #[cfg(all(windows, feature = "system-tray"))]
-    {
-      for tray in self.manager().trays().values() {
-        let _ = tray.destroy();
-      }
-    }
-  }
 }
 
 impl<R: Runtime> Manager<R> for AppHandle<R> {}
@@ -444,8 +436,8 @@ impl<R: Runtime> ManagerBase<R> for AppHandle<R> {
     RuntimeOrDispatch::RuntimeHandle(self.runtime_handle.clone())
   }
 
-  fn managed_app_handle(&self) -> AppHandle<R> {
-    self.clone()
+  fn managed_app_handle(&self) -> &AppHandle<R> {
+    self
   }
 }
 
@@ -485,7 +477,7 @@ impl<R: Runtime> ManagerBase<R> for App<R> {
     }
   }
 
-  fn managed_app_handle(&self) -> AppHandle<R> {
+  fn managed_app_handle(&self) -> &AppHandle<R> {
     self.handle()
   }
 }
@@ -511,72 +503,101 @@ impl App<crate::Wry> {
 macro_rules! shared_app_impl {
   ($app: ty) => {
     impl<R: Runtime> $app {
-      /// Gets a handle to the first system tray.
-      ///
-      /// Prefer [`Self::tray_handle_by_id`] when multiple system trays are created.
-      ///
-      /// # Examples
-      /// ```
-      /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu};
-      ///
-      /// tauri::Builder::default()
-      ///   .setup(|app| {
-      ///     let app_handle = app.handle();
-      ///     SystemTray::new()
-      ///       .with_menu(
-      ///         SystemTrayMenu::new()
-      ///           .add_item(CustomMenuItem::new("quit", "Quit"))
-      ///           .add_item(CustomMenuItem::new("open", "Open"))
-      ///       )
-      ///       .on_event(move |event| {
-      ///         let tray_handle = app_handle.tray_handle();
-      ///       })
-      ///       .build(app)?;
-      ///     Ok(())
-      ///   });
-      /// ```
-      #[cfg(all(desktop, feature = "system-tray"))]
-      #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-      pub fn tray_handle(&self) -> tray::SystemTrayHandle<R> {
+      /// Registers a global menu event listener.
+      #[cfg(desktop)]
+      pub fn on_menu_event<F: Fn(&AppHandle<R>, MenuEvent) + Send + Sync + 'static>(
+        &self,
+        handler: F,
+      ) {
         self
-          .manager()
-          .trays()
-          .values()
-          .next()
-          .cloned()
-          .expect("tray not configured; use the `Builder#system_tray`, `App#system_tray` or `AppHandle#system_tray` APIs first.")
+          .manager
+          .inner
+          .menu_event_listeners
+          .lock()
+          .unwrap()
+          .push(Box::new(handler));
       }
 
+      /// Registers a global tray icon menu event listener.
+      #[cfg(all(desktop, feature = "tray-icon"))]
+      #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
+      pub fn on_tray_icon_event<F: Fn(&AppHandle<R>, TrayIconEvent) + Send + Sync + 'static>(
+        &self,
+        handler: F,
+      ) {
+        self
+          .manager
+          .inner
+          .global_tray_event_listeners
+          .lock()
+          .unwrap()
+          .push(Box::new(handler));
+      }
 
-      /// Gets a handle to a system tray by its id.
-      ///
-      /// ```
-      /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu};
+      /// Gets the first tray icon registerd, usually the one configured in
+      /// tauri config file.
+      #[cfg(all(desktop, feature = "tray-icon"))]
+      #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
+      pub fn tray(&self) -> Option<TrayIcon<R>> {
+        self
+          .manager
+          .inner
+          .tray_icons
+          .lock()
+          .unwrap()
+          .first()
+          .cloned()
+      }
+
+      /// Removes the first tray icon registerd, usually the one configured in
+      /// tauri config file, from tauri's internal state and returns it.
       ///
-      /// tauri::Builder::default()
-      ///   .setup(|app| {
-      ///     let app_handle = app.handle();
-      ///     let tray_id = "my-tray";
-      ///     SystemTray::new()
-      ///       .with_id(tray_id)
-      ///       .with_menu(
-      ///         SystemTrayMenu::new()
-      ///           .add_item(CustomMenuItem::new("quit", "Quit"))
-      ///           .add_item(CustomMenuItem::new("open", "Open"))
-      ///       )
-      ///       .on_event(move |event| {
-      ///         let tray_handle = app_handle.tray_handle_by_id(tray_id).unwrap();
-      ///       })
-      ///       .build(app)?;
-      ///     Ok(())
-      ///   });
-      /// ```
-      #[cfg(all(desktop, feature = "system-tray"))]
-      #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-      pub fn tray_handle_by_id(&self, id: &str) -> Option<tray::SystemTrayHandle<R>> {
+      /// Note that dropping the returned icon, will cause the tray icon to disappear.
+      #[cfg(all(desktop, feature = "tray-icon"))]
+      #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
+      pub fn remove_tray(&self) -> Option<TrayIcon<R>> {
+        let mut tray_icons = self.manager.inner.tray_icons.lock().unwrap();
+        if !tray_icons.is_empty() {
+          return Some(tray_icons.swap_remove(0));
+        }
+        None
+      }
+
+      /// Gets a tray icon using the provided id.
+      #[cfg(all(desktop, feature = "tray-icon"))]
+      #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
+      pub fn tray_by_id<'a, I>(&self, id: &'a I) -> Option<TrayIcon<R>>
+      where
+        I: ?Sized,
+        TrayIconId: PartialEq<&'a I>,
+      {
         self
-          .manager()
-          .get_tray(id)
+          .manager
+          .inner
+          .tray_icons
+          .lock()
+          .unwrap()
+          .iter()
+          .find(|t| t.id() == &id)
+          .cloned()
+      }
+
+      /// Removes a tray icon using the provided id from tauri's internal state and returns it.
+      ///
+      /// Note that dropping the returned icon, will cause the tray icon to disappear.
+      #[cfg(all(desktop, feature = "tray-icon"))]
+      #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
+      pub fn remove_tray_by_id<'a, I>(&self, id: &'a I) -> Option<TrayIcon<R>>
+      where
+        I: ?Sized,
+        TrayIconId: PartialEq<&'a I>,
+      {
+        let mut tray_icons = self.manager.inner.tray_icons.lock().unwrap();
+        let idx = tray_icons.iter().position(|t| t.id() == &id);
+        if let Some(idx) = idx {
+          return Some(tray_icons.swap_remove(idx));
+        }
+        None
       }
 
       /// Gets the app's configuration, defined on the `tauri.conf.json` file.
@@ -600,29 +621,23 @@ macro_rules! shared_app_impl {
       ///
       /// Returns None if it can't identify any monitor as a primary one.
       pub fn primary_monitor(&self) -> crate::Result<Option<Monitor>> {
-       Ok(match self.runtime() {
-          RuntimeOrDispatch::Runtime(h) => h
-            .primary_monitor().map(Into::into),
-          RuntimeOrDispatch::RuntimeHandle(h) =>  h
-            .primary_monitor().map(Into::into),
-          _ => unreachable!()
+        Ok(match self.runtime() {
+          RuntimeOrDispatch::Runtime(h) => h.primary_monitor().map(Into::into),
+          RuntimeOrDispatch::RuntimeHandle(h) => h.primary_monitor().map(Into::into),
+          _ => unreachable!(),
         })
       }
 
       /// Returns the list of all the monitors available on the system.
       pub fn available_monitors(&self) -> crate::Result<Vec<Monitor>> {
         Ok(match self.runtime() {
-          RuntimeOrDispatch::Runtime(h) => h
-            .available_monitors()
-            .into_iter()
-            .map(Into::into)
-            .collect(),
-          RuntimeOrDispatch::RuntimeHandle(h) => h
-            .available_monitors()
-            .into_iter()
-            .map(Into::into)
-            .collect(),
-          _ => unreachable!()
+          RuntimeOrDispatch::Runtime(h) => {
+            h.available_monitors().into_iter().map(Into::into).collect()
+          }
+          RuntimeOrDispatch::RuntimeHandle(h) => {
+            h.available_monitors().into_iter().map(Into::into).collect()
+          }
+          _ => unreachable!(),
         })
       }
       /// Returns the default window icon.
@@ -630,6 +645,132 @@ macro_rules! shared_app_impl {
         self.manager.inner.default_window_icon.as_ref()
       }
 
+      /// Returns the app-wide menu.
+      #[cfg(desktop)]
+      pub fn menu(&self) -> Option<Menu<R>> {
+        self.manager.menu_lock().clone()
+      }
+
+      /// Sets the app-wide menu and returns the previous one.
+      ///
+      /// If a window was not created with an explicit menu or had one set explicitly,
+      /// this menu will be assigned to it.
+      #[cfg(desktop)]
+      pub fn set_menu(&self, menu: Menu<R>) -> crate::Result<Option<Menu<R>>> {
+        let prev_menu = self.remove_menu()?;
+
+        self.manager.insert_menu_into_stash(&menu);
+
+        self.manager.menu_lock().replace(menu.clone());
+
+        // set it on all windows that don't have one or previously had the app-wide menu
+        #[cfg(not(target_os = "macos"))]
+        {
+          for window in self.manager.windows().values() {
+            let has_app_wide_menu = window.has_app_wide_menu() || window.menu().is_none();
+            if has_app_wide_menu {
+              window.set_menu(menu.clone())?;
+              window.menu_lock().replace(WindowMenu {
+                is_app_wide: true,
+                menu: menu.clone(),
+              });
+            }
+          }
+        }
+
+        // set it app-wide for macos
+        #[cfg(target_os = "macos")]
+        {
+          let menu_ = menu.clone();
+          self.run_on_main_thread(move || {
+            let _ = init_app_menu(&menu_);
+          })?;
+        }
+
+        Ok(prev_menu)
+      }
+
+      /// Remove the app-wide menu and returns it.
+      ///
+      /// If a window was not created with an explicit menu or had one set explicitly,
+      /// this will remove the menu from it.
+      #[cfg(desktop)]
+      pub fn remove_menu(&self) -> crate::Result<Option<Menu<R>>> {
+        let menu = self.manager.menu_lock().as_ref().cloned();
+        #[allow(unused_variables)]
+        if let Some(menu) = menu {
+          // remove from windows that have the app-wide menu
+          #[cfg(not(target_os = "macos"))]
+          {
+            for window in self.manager.windows().values() {
+              let has_app_wide_menu = window.has_app_wide_menu();
+              if has_app_wide_menu {
+                window.remove_menu()?;
+                *window.menu_lock() = None;
+              }
+            }
+          }
+
+          // remove app-wide for macos
+          #[cfg(target_os = "macos")]
+          {
+            self.run_on_main_thread(move || {
+              menu.inner().remove_for_nsapp();
+            })?;
+          }
+        }
+
+        let prev_menu = self.manager.menu_lock().take();
+
+        self
+          .manager
+          .remove_menu_from_stash_by_id(prev_menu.as_ref().map(|m| m.id()));
+
+        Ok(prev_menu)
+      }
+
+      /// Hides the app-wide menu from windows that have it.
+      ///
+      /// If a window was not created with an explicit menu or had one set explicitly,
+      /// this will hide the menu from it.
+      #[cfg(desktop)]
+      pub fn hide_menu(&self) -> crate::Result<()> {
+        #[cfg(not(target_os = "macos"))]
+        {
+          let is_app_menu_set = self.manager.menu_lock().is_some();
+          if is_app_menu_set {
+            for window in self.manager.windows().values() {
+              if window.has_app_wide_menu() {
+                window.hide_menu()?;
+              }
+            }
+          }
+        }
+
+        Ok(())
+      }
+
+      /// Shows the app-wide menu for windows that have it.
+      ///
+      /// If a window was not created with an explicit menu or had one set explicitly,
+      /// this will show the menu for it.
+      #[cfg(desktop)]
+      pub fn show_menu(&self) -> crate::Result<()> {
+        #[cfg(not(target_os = "macos"))]
+        {
+          let is_app_menu_set = self.manager.menu_lock().is_some();
+          if is_app_menu_set {
+            for window in self.manager.windows().values() {
+              if window.has_app_wide_menu() {
+                window.show_menu()?;
+              }
+            }
+          }
+        }
+
+        Ok(())
+      }
+
       /// Shows the application, but does not automatically focus it.
       #[cfg(target_os = "macos")]
       pub fn show(&self) -> crate::Result<()> {
@@ -651,6 +792,13 @@ macro_rules! shared_app_impl {
         }
         Ok(())
       }
+
+      /// Runs necessary cleanup tasks before exiting the process.
+      /// **You should always exit the tauri app immediately after this function returns and not use any tauri-related APIs.**
+      pub fn cleanup_before_exit(&self) {
+        #[cfg(all(desktop, feature = "tray-icon"))]
+        self.manager.inner.tray_icons.lock().unwrap().clear()
+      }
     }
   };
 }
@@ -665,9 +813,14 @@ impl<R: Runtime> App<R> {
     Ok(())
   }
 
+  /// Runs the given closure on the main thread.
+  pub fn run_on_main_thread<F: FnOnce() + Send + 'static>(&self, f: F) -> crate::Result<()> {
+    self.app_handle().run_on_main_thread(f)
+  }
+
   /// Gets a handle to the application instance.
-  pub fn handle(&self) -> AppHandle<R> {
-    self.handle.clone()
+  pub fn handle(&self) -> &AppHandle<R> {
+    &self.handle
   }
 
   /// Sets the activation policy for the application. It is set to `NSApplicationActivationPolicyRegular` by default.
@@ -737,7 +890,7 @@ impl<R: Runtime> App<R> {
   /// });
   /// ```
   pub fn run<F: FnMut(&AppHandle<R>, RunEvent) + 'static>(mut self, mut callback: F) {
-    let app_handle = self.handle();
+    let app_handle = self.handle().clone();
     let manager = self.manager.clone();
     self.runtime.take().unwrap().run(move |event| match event {
       RuntimeRunEvent::Ready => {
@@ -769,8 +922,7 @@ impl<R: Runtime> App<R> {
   /// 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::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).
+  /// The cleanup calls [`App::cleanup_before_exit`] so you may want to call that function before exiting the application.
   ///
   /// # Examples
   /// ```no_run
@@ -781,6 +933,7 @@ impl<R: Runtime> App<R> {
   /// loop {
   ///   let iteration = app.run_iteration();
   ///   if iteration.window_count == 0 {
+  ///     app.cleanup_before_exit();
   ///     break;
   ///   }
   /// }
@@ -788,7 +941,7 @@ impl<R: Runtime> App<R> {
   #[cfg(desktop)]
   pub fn run_iteration(&mut self) -> crate::runtime::RunIteration {
     let manager = self.manager.clone();
-    let app_handle = self.handle();
+    let app_handle = self.handle().clone();
     self.runtime.as_mut().unwrap().run_iteration(move |event| {
       on_event_loop_event(
         &app_handle,
@@ -842,27 +995,17 @@ pub struct Builder<R: Runtime> {
   /// App state.
   state: StateManager,
 
-  /// The menu set to all windows.
-  menu: Option<Menu>,
+  /// A closure that returns the menu set to all windows.
+  #[cfg(desktop)]
+  menu: Option<Box<dyn FnOnce(&AppHandle<R>) -> crate::Result<Menu<R>>>>,
 
   /// Enable macOS default menu creation.
   #[allow(unused)]
   enable_macos_default_menu: bool,
 
-  /// Menu event handlers that listens to all windows.
-  menu_event_listeners: Vec<GlobalMenuEventListener<R>>,
-
   /// Window event handlers that listens to all windows.
   window_event_listeners: Vec<GlobalWindowEventListener<R>>,
 
-  /// The app system tray.
-  #[cfg(all(desktop, feature = "system-tray"))]
-  system_tray: Option<tray::SystemTray>,
-
-  /// System tray event handlers.
-  #[cfg(all(desktop, feature = "system-tray"))]
-  system_tray_event_listeners: Vec<SystemTrayEventListener<R>>,
-
   /// The device event filter.
   device_event_filter: DeviceEventFilter,
 }
@@ -901,14 +1044,10 @@ impl<R: Runtime> Builder<R> {
       plugins: PluginStore::default(),
       uri_scheme_protocols: Default::default(),
       state: StateManager::new(),
+      #[cfg(desktop)]
       menu: None,
       enable_macos_default_menu: true,
-      menu_event_listeners: Vec::new(),
       window_event_listeners: Vec::new(),
-      #[cfg(all(desktop, feature = "system-tray"))]
-      system_tray: None,
-      #[cfg(all(desktop, feature = "system-tray"))]
-      system_tray_event_listeners: Vec::new(),
       device_event_filter: Default::default(),
     }
   }
@@ -1134,50 +1273,33 @@ impl<R: Runtime> Builder<R> {
     self
   }
 
-  /// Sets the given system tray to be built before the app runs.
-  ///
-  /// Prefer the [`SystemTray#method.build`](crate::SystemTray#method.build) method to create the tray at runtime instead.
-  ///
-  /// # Examples
-  /// ```
-  /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu};
-  ///
-  /// tauri::Builder::default()
-  ///   .system_tray(SystemTray::new().with_menu(
-  ///     SystemTrayMenu::new()
-  ///       .add_item(CustomMenuItem::new("quit", "Quit"))
-  ///       .add_item(CustomMenuItem::new("open", "Open"))
-  ///   ));
-  /// ```
-  #[cfg(all(desktop, feature = "system-tray"))]
-  #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-  #[must_use]
-  pub fn system_tray(mut self, system_tray: tray::SystemTray) -> Self {
-    self.system_tray.replace(system_tray);
-    self
-  }
-
   /// Sets the menu to use on all windows.
   ///
   /// # Examples
   /// ```
-  /// use tauri::{MenuEntry, Submenu, MenuItem, Menu, CustomMenuItem};
+  /// use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu};
   ///
   /// tauri::Builder::default()
-  ///   .menu(Menu::with_items([
-  ///     MenuEntry::Submenu(Submenu::new(
+  ///   .menu(|handle| Menu::with_items(handle, &[
+  ///     &Submenu::with_items(
+  ///       handle,
   ///       "File",
-  ///       Menu::with_items([
-  ///         MenuItem::CloseWindow.into(),
+  ///       true,
+  ///       &[
+  ///         &PredefinedMenuItem::close_window(handle, None),
   ///         #[cfg(target_os = "macos")]
-  ///         CustomMenuItem::new("hello", "Hello").into(),
-  ///       ]),
-  ///     )),
+  ///         &MenuItem::new(handle, "Hello", true, None),
+  ///       ],
+  ///     )?
   ///   ]));
   /// ```
   #[must_use]
-  pub fn menu(mut self, menu: Menu) -> Self {
-    self.menu.replace(menu);
+  #[cfg(desktop)]
+  pub fn menu<F: FnOnce(&AppHandle<R>) -> crate::Result<Menu<R>> + 'static>(
+    mut self,
+    f: F,
+  ) -> Self {
+    self.menu.replace(Box::new(f));
     self
   }
 
@@ -1185,8 +1307,6 @@ impl<R: Runtime> Builder<R> {
   ///
   /// # Examples
   /// ```
-  /// use tauri::{MenuEntry, Submenu, MenuItem, Menu, CustomMenuItem};
-  ///
   /// tauri::Builder::default()
   ///   .enable_macos_default_menu(false);
   /// ```
@@ -1196,42 +1316,6 @@ impl<R: Runtime> Builder<R> {
     self
   }
 
-  /// Registers a menu event handler for all windows.
-  ///
-  /// # Examples
-  /// ```
-  /// use tauri::{Menu, MenuEntry, Submenu, CustomMenuItem, api, Manager};
-  /// tauri::Builder::default()
-  ///   .menu(Menu::with_items([
-  ///     MenuEntry::Submenu(Submenu::new(
-  ///       "File",
-  ///       Menu::with_items([
-  ///         CustomMenuItem::new("new", "New").into(),
-  ///         CustomMenuItem::new("learn-more", "Learn More").into(),
-  ///       ]),
-  ///     )),
-  ///   ]))
-  ///   .on_menu_event(|event| {
-  ///     match event.menu_item_id() {
-  ///       "learn-more" => {
-  ///         // open a link in the browser using tauri-plugin-shell
-  ///       }
-  ///       id => {
-  ///         // do something with other events
-  ///         println!("got menu event: {}", id);
-  ///       }
-  ///     }
-  ///   });
-  /// ```
-  #[must_use]
-  pub fn on_menu_event<F: Fn(WindowMenuEvent<R>) + Send + Sync + 'static>(
-    mut self,
-    handler: F,
-  ) -> Self {
-    self.menu_event_listeners.push(Box::new(handler));
-    self
-  }
-
   /// Registers a window event handler for all windows.
   ///
   /// # Examples
@@ -1256,37 +1340,6 @@ impl<R: Runtime> Builder<R> {
     self
   }
 
-  /// Registers a system tray event handler.
-  ///
-  /// Prefer the [`SystemTray#method.on_event`](crate::SystemTray#method.on_event) method when creating a tray at runtime instead.
-  ///
-  /// # Examples
-  /// ```
-  /// use tauri::{Manager, SystemTrayEvent};
-  /// tauri::Builder::default()
-  ///   .on_system_tray_event(|app, event| match event {
-  ///     // show window with id "main" when the tray is left clicked
-  ///     SystemTrayEvent::LeftClick { .. } => {
-  ///       let window = app.get_window("main").unwrap();
-  ///       window.show().unwrap();
-  ///       window.set_focus().unwrap();
-  ///     }
-  ///     _ => {}
-  ///   });
-  /// ```
-  #[cfg(all(desktop, feature = "system-tray"))]
-  #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-  #[must_use]
-  pub fn on_system_tray_event<
-    F: Fn(&AppHandle<R>, tray::SystemTrayEvent) + 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
@@ -1344,7 +1397,9 @@ impl<R: Runtime> Builder<R> {
   pub fn build<A: Assets>(mut self, context: Context<A>) -> crate::Result<App<R>> {
     #[cfg(target_os = "macos")]
     if self.menu.is_none() && self.enable_macos_default_menu {
-      self.menu = Some(Menu::os_default(&context.package_info().name));
+      self.menu = Some(Box::new(|app_handle| {
+        crate::menu::Menu::default(app_handle)
+      }));
     }
 
     let manager = WindowManager::with_handlers(
@@ -1355,7 +1410,8 @@ impl<R: Runtime> Builder<R> {
       self.uri_scheme_protocols,
       self.state,
       self.window_event_listeners,
-      (self.menu, self.menu_event_listeners),
+      #[cfg(desktop)]
+      HashMap::new(),
       (self.invoke_responder, self.invoke_initialization_script),
     );
 
@@ -1370,14 +1426,54 @@ impl<R: Runtime> Builder<R> {
       )?);
     }
 
+    let runtime_args = RuntimeInitArgs {
+      #[cfg(windows)]
+      msg_hook: {
+        let menus = manager.inner.menus.clone();
+        Some(Box::new(move |msg| {
+          use windows::Win32::UI::WindowsAndMessaging::{TranslateAcceleratorW, HACCEL, MSG};
+          unsafe {
+            let msg = msg as *const MSG;
+            for menu in menus.lock().unwrap().values() {
+              let translated =
+                TranslateAcceleratorW((*msg).hwnd, HACCEL(menu.inner().haccel()), msg);
+              if translated == 1 {
+                return true;
+              }
+            }
+
+            false
+          }
+        }))
+      },
+    };
+
     #[cfg(any(windows, target_os = "linux"))]
     let mut runtime = if self.runtime_any_thread {
-      R::new_any_thread()?
+      R::new_any_thread(runtime_args)?
     } else {
-      R::new()?
+      R::new(runtime_args)?
     };
     #[cfg(not(any(windows, target_os = "linux")))]
-    let mut runtime = R::new()?;
+    let mut runtime = R::new(runtime_args)?;
+
+    #[cfg(desktop)]
+    {
+      // setup menu event handler
+      let proxy = runtime.create_proxy();
+      crate::menu::MenuEvent::set_event_handler(Some(move |e| {
+        let _ = proxy.send_event(EventLoopMessage::MenuEvent(e));
+      }));
+
+      // setup tray event handler
+      #[cfg(feature = "tray-icon")]
+      {
+        let proxy = runtime.create_proxy();
+        crate::tray::TrayIconEvent::set_event_handler(Some(move |e| {
+          let _ = proxy.send_event(EventLoopMessage::TrayIconEvent(e));
+        }));
+      }
+    }
 
     runtime.set_device_event_filter(self.device_event_filter);
 
@@ -1395,6 +1491,20 @@ impl<R: Runtime> Builder<R> {
       },
     };
 
+    #[cfg(desktop)]
+    if let Some(menu) = self.menu {
+      let menu = menu(&app.handle)?;
+      app
+        .manager
+        .menus_stash_lock()
+        .insert(menu.id().clone(), menu.clone());
+
+      #[cfg(target_os = "macos")]
+      init_app_menu(&menu)?;
+
+      app.manager.menu_lock().replace(menu);
+    }
+
     app.register_core_plugins()?;
 
     let env = Env::default();
@@ -1433,31 +1543,31 @@ impl<R: Runtime> Builder<R> {
       }
     }
 
-    #[cfg(all(desktop, feature = "system-tray"))]
-    {
-      if let Some(tray) = self.system_tray {
-        tray.build(&app)?;
-      }
+    let handle = app.handle();
 
-      for listener in self.system_tray_event_listeners {
-        let app_handle = app.handle();
-        let listener = Arc::new(std::sync::Mutex::new(listener));
-        app
-          .runtime
-          .as_mut()
-          .unwrap()
-          .on_system_tray_event(move |tray_id, event| {
-            if let Some((tray_id, tray)) = app_handle.manager().get_tray_by_runtime_id(tray_id) {
-              let app_handle = app_handle.clone();
-              let event = tray::SystemTrayEvent::from_runtime_event(event, tray_id, &tray.ids);
-              let listener = listener.clone();
-              listener.lock().unwrap()(&app_handle, event);
-            }
-          });
+    // initialize default tray icon if defined
+    #[cfg(all(desktop, feature = "tray-icon"))]
+    {
+      let config = app.config();
+      if let Some(tray_config) = &config.tauri.tray_icon {
+        let mut tray = TrayIconBuilder::new()
+          .icon_as_template(tray_config.icon_as_template)
+          .menu_on_left_click(tray_config.menu_on_left_click);
+        if let Some(icon) = &app.manager.inner.tray_icon {
+          tray = tray.icon(icon.clone());
+        }
+        if let Some(title) = &tray_config.title {
+          tray = tray.title(title);
+        }
+        if let Some(tooltip) = &tray_config.tooltip {
+          tray = tray.tooltip(tooltip);
+        }
+        let tray = tray.build(handle)?;
+        app.manager.inner.tray_icons.lock().unwrap().push(tray);
       }
     }
 
-    app.manager.initialize_plugins(&app.handle())?;
+    app.manager.initialize_plugins(handle)?;
 
     Ok(app)
   }
@@ -1469,6 +1579,24 @@ impl<R: Runtime> Builder<R> {
   }
 }
 
+#[cfg(target_os = "macos")]
+fn init_app_menu<R: Runtime>(menu: &Menu<R>) -> crate::Result<()> {
+  menu.inner().init_for_nsapp();
+
+  if let Some(window_menu) = menu.get(crate::menu::WINDOW_SUBMENU_ID) {
+    if let Some(m) = window_menu.as_submenu() {
+      m.set_as_windows_menu_for_nsapp()?;
+    }
+  }
+  if let Some(help_menu) = menu.get(crate::menu::HELP_SUBMENU_ID) {
+    if let Some(m) = help_menu.as_submenu() {
+      m.set_as_help_menu_for_nsapp()?;
+    }
+  }
+
+  Ok(())
+}
+
 unsafe impl<R: Runtime> HasRawDisplayHandle for AppHandle<R> {
   fn raw_display_handle(&self) -> raw_window_handle::RawDisplayHandle {
     self.runtime_handle.raw_display_handle()
@@ -1489,18 +1617,37 @@ fn setup<R: Runtime>(app: &mut App<R>) -> crate::Result<()> {
       .map(|p| p.label.clone())
       .collect::<Vec<_>>();
 
+    let app_handle = app.handle();
+    let manager = app.manager();
+
     for pending in pending_windows {
-      let pending = app
-        .manager
-        .prepare_window(app.handle.clone(), pending, &window_labels)?;
+      let pending = manager.prepare_window(app_handle.clone(), pending, &window_labels)?;
+
+      #[cfg(desktop)]
+      let window_menu = app.manager.menu_lock().as_ref().map(|m| WindowMenu {
+        is_app_wide: true,
+        menu: m.clone(),
+      });
+
+      #[cfg(desktop)]
+      let handler = manager.prepare_window_menu_creation_handler(window_menu.as_ref());
+      #[cfg(not(desktop))]
+      #[allow(clippy::type_complexity)]
+      let handler: Option<Box<dyn Fn(tauri_runtime::window::RawWindow<'_>) + Send>> = None;
+
       let window_effects = pending.webview_attributes.window_effects.clone();
-      let detached = if let RuntimeOrDispatch::RuntimeHandle(runtime) = app.handle().runtime() {
-        runtime.create_window(pending)?
+      let detached = if let RuntimeOrDispatch::RuntimeHandle(runtime) = app_handle.runtime() {
+        runtime.create_window(pending, handler)?
       } else {
         // the AppHandle's runtime is always RuntimeOrDispatch::RuntimeHandle
         unreachable!()
       };
-      let window = app.manager.attach_window(app.handle(), detached);
+      let window = manager.attach_window(
+        app_handle.clone(),
+        detached,
+        #[cfg(desktop)]
+        None,
+      );
 
       if let Some(effects) = window_effects {
         crate::vibrancy::set_window_effects(&window, Some(effects))?;
@@ -1563,7 +1710,62 @@ fn on_event_loop_event<R: Runtime, F: FnMut(&AppHandle<R>, RunEvent) + 'static>(
     }
     RuntimeRunEvent::Resumed => RunEvent::Resumed,
     RuntimeRunEvent::MainEventsCleared => RunEvent::MainEventsCleared,
-    RuntimeRunEvent::UserEvent(t) => t.into(),
+    RuntimeRunEvent::UserEvent(t) => {
+      match t {
+        #[cfg(desktop)]
+        EventLoopMessage::MenuEvent(ref e) => {
+          for listener in &*app_handle
+            .manager
+            .inner
+            .menu_event_listeners
+            .lock()
+            .unwrap()
+          {
+            listener(app_handle, e.clone());
+          }
+          for (label, listener) in &*app_handle
+            .manager
+            .inner
+            .window_menu_event_listeners
+            .lock()
+            .unwrap()
+          {
+            if let Some(w) = app_handle.get_window(label) {
+              listener(&w, e.clone());
+            }
+          }
+        }
+        #[cfg(all(desktop, feature = "tray-icon"))]
+        EventLoopMessage::TrayIconEvent(ref e) => {
+          for listener in &*app_handle
+            .manager
+            .inner
+            .global_tray_event_listeners
+            .lock()
+            .unwrap()
+          {
+            listener(app_handle, e.clone());
+          }
+
+          for (id, listener) in &*app_handle
+            .manager
+            .inner
+            .tray_event_listeners
+            .lock()
+            .unwrap()
+          {
+            if e.id == id {
+              if let Some(tray) = app_handle.tray_by_id(id) {
+                listener(&tray, e.clone());
+              }
+            }
+          }
+        }
+      }
+
+      #[allow(unreachable_code)]
+      t.into()
+    }
     #[cfg(any(target_os = "macos", target_os = "ios"))]
     RuntimeRunEvent::Opened { urls } => RunEvent::Opened { urls },
     _ => unimplemented!(),

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

@@ -1,704 +0,0 @@
-// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-License-Identifier: MIT
-
-pub use crate::{
-  runtime::{
-    menu::{
-      MenuHash, MenuId, MenuIdRef, MenuUpdate, SystemTrayMenu, SystemTrayMenuEntry, TrayHandle,
-    },
-    window::dpi::{PhysicalPosition, PhysicalSize},
-    RuntimeHandle, SystemTrayEvent as RuntimeSystemTrayEvent,
-  },
-  Icon, Runtime,
-};
-use crate::{sealed::RuntimeOrDispatch, Manager};
-
-use rand::distributions::{Alphanumeric, DistString};
-use tauri_macros::default_runtime;
-use tauri_runtime::TrayId;
-use tauri_utils::debug_eprintln;
-
-use std::{
-  collections::{hash_map::DefaultHasher, HashMap},
-  fmt,
-  hash::{Hash, Hasher},
-  sync::{Arc, Mutex},
-};
-
-type TrayEventHandler = dyn Fn(SystemTrayEvent) + Send + Sync + 'static;
-
-pub(crate) fn get_menu_ids(map: &mut HashMap<MenuHash, MenuId>, menu: &SystemTrayMenu) {
-  for item in &menu.items {
-    match item {
-      SystemTrayMenuEntry::CustomItem(c) => {
-        map.insert(c.id, c.id_str.clone());
-      }
-      SystemTrayMenuEntry::Submenu(s) => get_menu_ids(map, &s.inner),
-      _ => {}
-    }
-  }
-}
-
-/// Represents a System Tray instance.
-#[derive(Clone)]
-#[non_exhaustive]
-pub struct SystemTray {
-  /// The tray identifier. Defaults to a random string.
-  pub id: String,
-  /// The tray icon.
-  pub icon: Option<tauri_runtime::Icon>,
-  /// The tray menu.
-  pub menu: Option<SystemTrayMenu>,
-  /// Whether the icon is a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) icon or not.
-  #[cfg(target_os = "macos")]
-  pub icon_as_template: bool,
-  /// Whether the menu should appear when the tray receives a left click. Defaults to `true`
-  #[cfg(target_os = "macos")]
-  pub menu_on_left_click: bool,
-  on_event: Option<Arc<TrayEventHandler>>,
-  // TODO: icon_as_template and menu_on_left_click should be an Option instead :(
-  #[cfg(target_os = "macos")]
-  menu_on_left_click_set: bool,
-  #[cfg(target_os = "macos")]
-  icon_as_template_set: bool,
-  #[cfg(target_os = "macos")]
-  title: Option<String>,
-  tooltip: Option<String>,
-}
-
-impl fmt::Debug for SystemTray {
-  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-    let mut d = f.debug_struct("SystemTray");
-    d.field("id", &self.id)
-      .field("icon", &self.icon)
-      .field("menu", &self.menu);
-    #[cfg(target_os = "macos")]
-    {
-      d.field("icon_as_template", &self.icon_as_template)
-        .field("menu_on_left_click", &self.menu_on_left_click);
-    }
-    d.finish()
-  }
-}
-
-impl Default for SystemTray {
-  fn default() -> Self {
-    Self {
-      id: Alphanumeric.sample_string(&mut rand::thread_rng(), 16),
-      icon: None,
-      menu: None,
-      on_event: None,
-      #[cfg(target_os = "macos")]
-      icon_as_template: false,
-      #[cfg(target_os = "macos")]
-      menu_on_left_click: false,
-      #[cfg(target_os = "macos")]
-      icon_as_template_set: false,
-      #[cfg(target_os = "macos")]
-      menu_on_left_click_set: false,
-      #[cfg(target_os = "macos")]
-      title: None,
-      tooltip: None,
-    }
-  }
-}
-
-impl SystemTray {
-  /// Creates a new system tray that only renders an icon.
-  ///
-  /// # Examples
-  ///
-  /// ```
-  /// use tauri::SystemTray;
-  ///
-  /// tauri::Builder::default()
-  ///   .setup(|app| {
-  ///     let tray_handle = SystemTray::new().build(app)?;
-  ///     Ok(())
-  ///   });
-  /// ```
-  pub fn new() -> Self {
-    Default::default()
-  }
-
-  pub(crate) fn menu(&self) -> Option<&SystemTrayMenu> {
-    self.menu.as_ref()
-  }
-
-  /// Sets the tray identifier, used to retrieve its handle and to identify a tray event source.
-  ///
-  /// # Examples
-  ///
-  /// ```
-  /// use tauri::SystemTray;
-  ///
-  /// tauri::Builder::default()
-  ///   .setup(|app| {
-  ///     let tray_handle = SystemTray::new()
-  ///       .with_id("tray-id")
-  ///       .build(app)?;
-  ///     Ok(())
-  ///   });
-  /// ```
-  #[must_use]
-  pub fn with_id<I: Into<String>>(mut self, id: I) -> Self {
-    self.id = id.into();
-    self
-  }
-
-  /// Sets the tray [`Icon`].
-  ///
-  /// # Examples
-  ///
-  /// ```
-  /// use tauri::{Icon, SystemTray};
-  ///
-  /// tauri::Builder::default()
-  ///   .setup(|app| {
-  ///     let tray_handle = SystemTray::new()
-  ///       // dummy and invalid Rgba icon; see the Icon documentation for more information
-  ///       .with_icon(Icon::Rgba { rgba: Vec::new(), width: 0, height: 0 })
-  ///       .build(app)?;
-  ///     Ok(())
-  ///   });
-  /// ```
-  #[must_use]
-  pub fn with_icon<I: TryInto<tauri_runtime::Icon>>(mut self, icon: I) -> Self
-  where
-    I::Error: std::error::Error,
-  {
-    match icon.try_into() {
-      Ok(icon) => {
-        self.icon.replace(icon);
-      }
-      Err(e) => {
-        debug_eprintln!("Failed to load tray icon: {}", e);
-      }
-    }
-    self
-  }
-
-  /// Sets the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc).
-  ///
-  /// Images you mark as template images should consist of only black and clear colors.
-  /// You can use the alpha channel in the image to adjust the opacity of black content.
-  ///
-  /// # Examples
-  ///
-  /// ```
-  /// use tauri::SystemTray;
-  ///
-  /// tauri::Builder::default()
-  ///   .setup(|app| {
-  ///     let mut tray_builder = SystemTray::new();
-  ///     #[cfg(target_os = "macos")]
-  ///     {
-  ///       tray_builder = tray_builder.with_icon_as_template(true);
-  ///     }
-  ///     let tray_handle = tray_builder.build(app)?;
-  ///     Ok(())
-  ///   });
-  /// ```
-  #[cfg(target_os = "macos")]
-  #[must_use]
-  pub fn with_icon_as_template(mut self, is_template: bool) -> Self {
-    self.icon_as_template_set = true;
-    self.icon_as_template = is_template;
-    self
-  }
-
-  /// Sets whether the menu should appear when the tray receives a left click. Defaults to `true`.
-  ///
-  /// # Examples
-  ///
-  /// ```
-  /// use tauri::SystemTray;
-  ///
-  /// tauri::Builder::default()
-  ///   .setup(|app| {
-  ///     let mut tray_builder = SystemTray::new();
-  ///     #[cfg(target_os = "macos")]
-  ///     {
-  ///       tray_builder = tray_builder.with_menu_on_left_click(false);
-  ///     }
-  ///     let tray_handle = tray_builder.build(app)?;
-  ///     Ok(())
-  ///   });
-  /// ```
-  #[cfg(target_os = "macos")]
-  #[must_use]
-  pub fn with_menu_on_left_click(mut self, menu_on_left_click: bool) -> Self {
-    self.menu_on_left_click_set = true;
-    self.menu_on_left_click = menu_on_left_click;
-    self
-  }
-
-  /// Sets the menu title`
-  ///
-  /// # Examples
-  ///
-  /// ```
-  /// use tauri::SystemTray;
-  ///
-  /// tauri::Builder::default()
-  ///   .setup(|app| {
-  ///     let mut tray_builder = SystemTray::new();
-  ///     #[cfg(target_os = "macos")]
-  ///     {
-  ///       tray_builder = tray_builder.with_title("My App");
-  ///     }
-  ///     let tray_handle = tray_builder.build(app)?;
-  ///     Ok(())
-  ///   });
-  /// ```
-  #[cfg(target_os = "macos")]
-  #[must_use]
-  pub fn with_title(mut self, title: &str) -> Self {
-    self.title = Some(title.to_owned());
-    self
-  }
-
-  /// Sets the tray icon tooltip.
-  ///
-  /// ## Platform-specific:
-  ///
-  /// - **Linux:** Unsupported
-  ///
-  /// # Examples
-  ///
-  /// ```
-  /// use tauri::SystemTray;
-  ///
-  /// tauri::Builder::default()
-  ///   .setup(|app| {
-  ///     let tray_handle = SystemTray::new().with_tooltip("My App").build(app)?;
-  ///     Ok(())
-  ///   });
-  /// ```
-  #[must_use]
-  pub fn with_tooltip(mut self, tooltip: &str) -> Self {
-    self.tooltip = Some(tooltip.to_owned());
-    self
-  }
-
-  /// Sets the event listener for this system tray.
-  ///
-  /// # Examples
-  ///
-  /// ```
-  /// use tauri::{Icon, Manager, SystemTray, SystemTrayEvent};
-  ///
-  /// tauri::Builder::default()
-  ///   .setup(|app| {
-  ///     let handle = app.handle();
-  ///     let id = "tray-id";
-  ///     SystemTray::new()
-  ///       .with_id(id)
-  ///       .on_event(move |event| {
-  ///         let tray_handle = handle.tray_handle_by_id(id).unwrap();
-  ///         match event {
-  ///           // show window with id "main" when the tray is left clicked
-  ///           SystemTrayEvent::LeftClick { .. } => {
-  ///             let window = handle.get_window("main").unwrap();
-  ///             window.show().unwrap();
-  ///             window.set_focus().unwrap();
-  ///           }
-  ///           _ => {}
-  ///         }
-  ///       })
-  ///       .build(app)?;
-  ///     Ok(())
-  ///   });
-  /// ```
-  #[must_use]
-  pub fn on_event<F: Fn(SystemTrayEvent) + Send + Sync + 'static>(mut self, f: F) -> Self {
-    self.on_event.replace(Arc::new(f));
-    self
-  }
-
-  /// Sets the menu to show when the system tray is right clicked.
-  ///
-  /// # Examples
-  ///
-  /// ```
-  /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu};
-  ///
-  /// tauri::Builder::default()
-  ///   .setup(|app| {
-  ///     let tray_handle = SystemTray::new()
-  ///       .with_menu(
-  ///         SystemTrayMenu::new()
-  ///           .add_item(CustomMenuItem::new("quit", "Quit"))
-  ///           .add_item(CustomMenuItem::new("open", "Open"))
-  ///       )
-  ///       .build(app)?;
-  ///     Ok(())
-  ///   });
-  /// ```
-  #[must_use]
-  pub fn with_menu(mut self, menu: SystemTrayMenu) -> Self {
-    self.menu.replace(menu);
-    self
-  }
-
-  /// Builds and shows the system tray.
-  ///
-  /// # Examples
-  ///
-  /// ```
-  /// use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu};
-  ///
-  /// tauri::Builder::default()
-  ///   .setup(|app| {
-  ///     let tray_handle = SystemTray::new()
-  ///       .with_menu(
-  ///         SystemTrayMenu::new()
-  ///           .add_item(CustomMenuItem::new("quit", "Quit"))
-  ///           .add_item(CustomMenuItem::new("open", "Open"))
-  ///       )
-  ///       .build(app)?;
-  ///
-  ///       tray_handle.get_item("quit").set_enabled(false);
-  ///     Ok(())
-  ///   });
-  /// ```
-  pub fn build<R: Runtime, M: Manager<R>>(
-    mut self,
-    manager: &M,
-  ) -> crate::Result<SystemTrayHandle<R>> {
-    let mut ids = HashMap::new();
-    if let Some(menu) = self.menu() {
-      get_menu_ids(&mut ids, menu);
-    }
-    let ids = Arc::new(Mutex::new(ids));
-
-    if self.icon.is_none() {
-      if let Some(tray_icon) = &manager.manager().inner.tray_icon {
-        self = self.with_icon(tray_icon.clone());
-      }
-    }
-    #[cfg(target_os = "macos")]
-    {
-      if !self.icon_as_template_set {
-        self.icon_as_template = manager
-          .config()
-          .tauri
-          .system_tray
-          .as_ref()
-          .map_or(false, |t| t.icon_as_template);
-      }
-      if !self.menu_on_left_click_set {
-        self.menu_on_left_click = manager
-          .config()
-          .tauri
-          .system_tray
-          .as_ref()
-          .map_or(false, |t| t.menu_on_left_click);
-      }
-      if self.title.is_none() {
-        self.title = manager
-          .config()
-          .tauri
-          .system_tray
-          .as_ref()
-          .and_then(|t| t.title.clone())
-      }
-    }
-
-    let tray_id = self.id.clone();
-
-    let mut runtime_tray = tauri_runtime::SystemTray::new();
-    runtime_tray = runtime_tray.with_id(hash(&self.id));
-    if let Some(i) = self.icon {
-      runtime_tray = runtime_tray.with_icon(i);
-    }
-
-    if let Some(menu) = self.menu {
-      runtime_tray = runtime_tray.with_menu(menu);
-    }
-
-    if let Some(on_event) = self.on_event {
-      let ids_ = ids.clone();
-      let tray_id_ = tray_id.clone();
-      runtime_tray = runtime_tray.on_event(move |event| {
-        on_event(SystemTrayEvent::from_runtime_event(
-          event,
-          tray_id_.clone(),
-          &ids_,
-        ))
-      });
-    }
-
-    #[cfg(target_os = "macos")]
-    {
-      runtime_tray = runtime_tray.with_icon_as_template(self.icon_as_template);
-      runtime_tray = runtime_tray.with_menu_on_left_click(self.menu_on_left_click);
-      if let Some(title) = self.title {
-        runtime_tray = runtime_tray.with_title(&title);
-      }
-    }
-
-    if let Some(tooltip) = self.tooltip {
-      runtime_tray = runtime_tray.with_tooltip(&tooltip);
-    }
-
-    let id = runtime_tray.id;
-    let tray_handler = match manager.runtime() {
-      RuntimeOrDispatch::Runtime(r) => r.system_tray(runtime_tray),
-      RuntimeOrDispatch::RuntimeHandle(h) => h.system_tray(runtime_tray),
-      RuntimeOrDispatch::Dispatch(_) => manager
-        .app_handle()
-        .runtime_handle
-        .system_tray(runtime_tray),
-    }?;
-
-    let tray_handle = SystemTrayHandle {
-      id,
-      ids,
-      inner: tray_handler,
-    };
-    manager.manager().attach_tray(tray_id, tray_handle.clone());
-
-    Ok(tray_handle)
-  }
-}
-
-fn hash(id: &str) -> MenuHash {
-  let mut hasher = DefaultHasher::new();
-  id.hash(&mut hasher);
-  hasher.finish() as MenuHash
-}
-
-/// System tray event.
-#[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-#[non_exhaustive]
-pub enum SystemTrayEvent {
-  /// Tray context menu item was clicked.
-  #[non_exhaustive]
-  MenuItemClick {
-    /// The tray id.
-    tray_id: String,
-    /// The id of the menu item.
-    id: MenuId,
-  },
-  /// Tray icon received a left click.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Linux:** Unsupported
-  #[non_exhaustive]
-  LeftClick {
-    /// The tray id.
-    tray_id: String,
-    /// The position of the tray icon.
-    position: PhysicalPosition<f64>,
-    /// The size of the tray icon.
-    size: PhysicalSize<f64>,
-  },
-  /// Tray icon received a right click.
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **Linux:** Unsupported
-  /// - **macOS:** `Ctrl` + `Left click` fire this event.
-  #[non_exhaustive]
-  RightClick {
-    /// The tray id.
-    tray_id: String,
-    /// The position of the tray icon.
-    position: PhysicalPosition<f64>,
-    /// The size of the tray icon.
-    size: PhysicalSize<f64>,
-  },
-  /// Fired when a menu item receive a `Double click`
-  ///
-  /// ## Platform-specific
-  ///
-  /// - **macOS / Linux:** Unsupported
-  ///
-  #[non_exhaustive]
-  DoubleClick {
-    /// The tray id.
-    tray_id: String,
-    /// The position of the tray icon.
-    position: PhysicalPosition<f64>,
-    /// The size of the tray icon.
-    size: PhysicalSize<f64>,
-  },
-}
-
-impl SystemTrayEvent {
-  pub(crate) fn from_runtime_event(
-    event: &RuntimeSystemTrayEvent,
-    tray_id: String,
-    menu_ids: &Arc<Mutex<HashMap<u16, String>>>,
-  ) -> Self {
-    match event {
-      RuntimeSystemTrayEvent::MenuItemClick(id) => Self::MenuItemClick {
-        tray_id,
-        id: menu_ids.lock().unwrap().get(id).unwrap().clone(),
-      },
-      RuntimeSystemTrayEvent::LeftClick { position, size } => Self::LeftClick {
-        tray_id,
-        position: *position,
-        size: *size,
-      },
-      RuntimeSystemTrayEvent::RightClick { position, size } => Self::RightClick {
-        tray_id,
-        position: *position,
-        size: *size,
-      },
-      RuntimeSystemTrayEvent::DoubleClick { position, size } => Self::DoubleClick {
-        tray_id,
-        position: *position,
-        size: *size,
-      },
-    }
-  }
-}
-
-/// A handle to a system tray. Allows updating the context menu items.
-#[default_runtime(crate::Wry, wry)]
-#[derive(Debug)]
-pub struct SystemTrayHandle<R: Runtime> {
-  pub(crate) id: TrayId,
-  pub(crate) ids: Arc<Mutex<HashMap<MenuHash, MenuId>>>,
-  pub(crate) inner: R::TrayHandler,
-}
-
-impl<R: Runtime> Clone for SystemTrayHandle<R> {
-  fn clone(&self) -> Self {
-    Self {
-      id: self.id,
-      ids: self.ids.clone(),
-      inner: self.inner.clone(),
-    }
-  }
-}
-
-/// A handle to a system tray menu item.
-#[default_runtime(crate::Wry, wry)]
-#[derive(Debug)]
-pub struct SystemTrayMenuItemHandle<R: Runtime> {
-  id: MenuHash,
-  tray_handler: R::TrayHandler,
-}
-
-impl<R: Runtime> Clone for SystemTrayMenuItemHandle<R> {
-  fn clone(&self) -> Self {
-    Self {
-      id: self.id,
-      tray_handler: self.tray_handler.clone(),
-    }
-  }
-}
-
-impl<R: Runtime> SystemTrayHandle<R> {
-  /// Gets a handle to the menu item that has the specified `id`.
-  pub fn get_item(&self, id: MenuIdRef<'_>) -> SystemTrayMenuItemHandle<R> {
-    let ids = self.ids.lock().unwrap();
-    let iter = ids.iter();
-    for (raw, item_id) in iter {
-      if item_id == id {
-        return SystemTrayMenuItemHandle {
-          id: *raw,
-          tray_handler: self.inner.clone(),
-        };
-      }
-    }
-    panic!("item id not found")
-  }
-
-  /// Attempts to get a handle to the menu item that has the specified `id`, return an error if `id` is not found.
-  pub fn try_get_item(&self, id: MenuIdRef<'_>) -> Option<SystemTrayMenuItemHandle<R>> {
-    self
-      .ids
-      .lock()
-      .unwrap()
-      .iter()
-      .find(|i| i.1 == id)
-      .map(|i| SystemTrayMenuItemHandle {
-        id: *i.0,
-        tray_handler: self.inner.clone(),
-      })
-  }
-  /// Updates the tray icon.
-  pub fn set_icon(&self, icon: Icon) -> crate::Result<()> {
-    self.inner.set_icon(icon.try_into()?).map_err(Into::into)
-  }
-
-  /// Updates the tray menu.
-  pub fn set_menu(&self, menu: SystemTrayMenu) -> crate::Result<()> {
-    let mut ids = HashMap::new();
-    get_menu_ids(&mut ids, &menu);
-    self.inner.set_menu(menu)?;
-    *self.ids.lock().unwrap() = ids;
-    Ok(())
-  }
-
-  /// Support [macOS tray icon template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc) to adjust automatically based on taskbar color.
-  #[cfg(target_os = "macos")]
-  pub fn set_icon_as_template(&self, is_template: bool) -> crate::Result<()> {
-    self
-      .inner
-      .set_icon_as_template(is_template)
-      .map_err(Into::into)
-  }
-
-  /// Adds the title to the tray menu
-  #[cfg(target_os = "macos")]
-  pub fn set_title(&self, title: &str) -> crate::Result<()> {
-    self.inner.set_title(title).map_err(Into::into)
-  }
-
-  /// Set the tooltip for this tray icon.
-  ///
-  /// ## Platform-specific:
-  ///
-  /// - **Linux:** Unsupported
-  pub fn set_tooltip(&self, tooltip: &str) -> crate::Result<()> {
-    self.inner.set_tooltip(tooltip).map_err(Into::into)
-  }
-
-  /// Destroys this system tray.
-  pub fn destroy(&self) -> crate::Result<()> {
-    self.inner.destroy().map_err(Into::into)
-  }
-}
-
-impl<R: Runtime> SystemTrayMenuItemHandle<R> {
-  /// 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)
-  }
-
-  /// Sets the native image for this item.
-  #[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)
-  }
-}

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

@@ -89,4 +89,25 @@ pub enum Error {
   #[cfg(target_os = "android")]
   #[error("jni error: {0}")]
   Jni(#[from] jni::errors::Error),
+  /// Failed to receive message .
+  #[error("failed to receive message")]
+  FailedToReceiveMessage,
+  /// Menu error.
+  #[error("menu error: {0}")]
+  #[cfg(desktop)]
+  Menu(#[from] muda::Error),
+  /// Bad menu icon error.
+  #[error(transparent)]
+  #[cfg(desktop)]
+  BadMenuIcon(#[from] muda::BadIcon),
+  /// Tray icon error.
+  #[error("tray icon error: {0}")]
+  #[cfg(all(desktop, feature = "tray-icon"))]
+  #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
+  Tray(#[from] tray_icon::Error),
+  /// Bad tray icon error.
+  #[error(transparent)]
+  #[cfg(all(desktop, feature = "tray-icon"))]
+  #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
+  BadTrayIcon(#[from] tray_icon::BadIcon),
 }

+ 29 - 18
core/tauri/src/jni_helpers.rs

@@ -5,18 +5,18 @@
 use crate::Runtime;
 use jni::{
   errors::Error as JniError,
-  objects::{JObject, JValue},
+  objects::{JObject, JValueOwned},
   JNIEnv,
 };
 use serde_json::Value as JsonValue;
 use tauri_runtime::RuntimeHandle;
 
 fn json_to_java<'a, R: Runtime>(
-  env: JNIEnv<'a>,
-  activity: JObject<'a>,
+  env: &mut JNIEnv<'a>,
+  activity: &JObject<'_>,
   runtime_handle: &R::Handle,
   json: &JsonValue,
-) -> Result<(&'static str, JValue<'a>), JniError> {
+) -> Result<(&'static str, JValueOwned<'a>), JniError> {
   let (class, v) = match json {
     JsonValue::Null => ("Ljava/lang/Object;", JObject::null().into()),
     JsonValue::Bool(val) => ("Z", (*val).into()),
@@ -40,27 +40,30 @@ fn json_to_java<'a, R: Runtime>(
       for v in val {
         let (signature, val) = json_to_java::<R>(env, activity, runtime_handle, v)?;
         env.call_method(
-          data,
+          &data,
           "put",
           format!("({signature})Lorg/json/JSONArray;"),
-          &[val],
+          &[val.borrow()],
         )?;
       }
 
       ("Ljava/lang/Object;", data.into())
     }
     JsonValue::Object(val) => {
-      let js_object_class =
-        runtime_handle.find_class(env, activity, "app/tauri/plugin/JSObject")?;
-      let data = env.new_object(js_object_class, "()V", &[])?;
+      let data = {
+        let js_object_class =
+          runtime_handle.find_class(env, activity, "app/tauri/plugin/JSObject")?;
+        env.new_object(js_object_class, "()V", &[])?
+      };
 
       for (key, value) in val {
         let (signature, val) = json_to_java::<R>(env, activity, runtime_handle, value)?;
+        let key = env.new_string(key)?;
         env.call_method(
-          data,
+          &data,
           "put",
           format!("(Ljava/lang/String;{signature})Lapp/tauri/plugin/JSObject;"),
-          &[env.new_string(key)?.into(), val],
+          &[(&key).into(), val.borrow()],
         )?;
       }
 
@@ -71,17 +74,25 @@ fn json_to_java<'a, R: Runtime>(
 }
 
 pub fn to_jsobject<'a, R: Runtime>(
-  env: JNIEnv<'a>,
-  activity: JObject<'a>,
+  env: &mut JNIEnv<'a>,
+  activity: &JObject<'_>,
   runtime_handle: &R::Handle,
   json: &JsonValue,
-) -> Result<JValue<'a>, JniError> {
+) -> Result<JValueOwned<'a>, JniError> {
   if let JsonValue::Object(_) = json {
     json_to_java::<R>(env, activity, runtime_handle, json).map(|(_class, data)| data)
   } else {
-    // currently the Kotlin lib cannot handle nulls or raw values, it must be an object
-    let js_object_class = runtime_handle.find_class(env, activity, "app/tauri/plugin/JSObject")?;
-    let data = env.new_object(js_object_class, "()V", &[])?;
-    Ok(data.into())
+    Ok(empty_object::<R>(env, activity, runtime_handle)?.into())
   }
 }
+
+fn empty_object<'a, R: Runtime>(
+  env: &mut JNIEnv<'a>,
+  activity: &JObject<'_>,
+  runtime_handle: &R::Handle,
+) -> Result<JObject<'a>, JniError> {
+  // currently the Kotlin lib cannot handle nulls or raw values, it must be an object
+  let js_object_class = runtime_handle.find_class(env, activity, "app/tauri/plugin/JSObject")?;
+  let data = env.new_object(js_object_class, "()V", &[])?;
+  Ok(data)
+}

+ 73 - 39
core/tauri/src/lib.rs

@@ -17,6 +17,7 @@
 //! - **dox**: Internal feature to generate Rust documentation without linking on Linux.
 //! - **objc-exception**: Wrap each msg_send! in a @try/@catch and panics if an exception is caught, preventing Objective-C from unwinding into Rust.
 //! - **linux-ipc-protocol**: Use custom protocol for faster IPC on Linux. Requires webkit2gtk v2.40 or above.
+//! - **linux-libxdo**: Enables linking to libxdo which enables Cut, Copy, Paste and SelectAll menu items to work on Linux.
 //! - **isolation**: Enables the isolation pattern. Enabled by default if the `tauri > pattern > use` config option is set to `isolation` on the `tauri.conf.json` file.
 //! - **custom-protocol**: Feature managed by the Tauri CLI. When enabled, Tauri assumes a production environment instead of a development one.
 //! - **devtools**: Enables the developer tools (Web inspector) and [`Window::open_devtools`]. Enabled by default on debug builds.
@@ -25,7 +26,7 @@
 //! - **native-tls-vendored**: Compile and statically link to a vendored copy of OpenSSL.
 //! - **rustls-tls**: Provides TLS support to connect over HTTPS using rustls.
 //! - **process-relaunch-dangerous-allow-symlink-macos**: Allows the [`process::current_binary`] function to allow symlinks on macOS (this is dangerous, see the Security section in the documentation website).
-//! - **system-tray**: Enables application system tray API. Enabled by default if the `systemTray` config is defined on the `tauri.conf.json` file.
+//! - **tray-icon**: Enables application tray icon APIs. Enabled by default if the `trayIcon` config is defined on the `tauri.conf.json` file.
 //! - **macos-private-api**: Enables features only available in **macOS**'s private APIs, currently the `transparent` window functionality and the `fullScreenEnabled` preference setting to `true`. Enabled by default if the `tauri > macosPrivateApi` config flag is set to `true` on the `tauri.conf.json` file.
 //! - **window-data-url**: Enables usage of data URLs on the webview.
 //! - **compression** *(enabled by default): Enables asset compression. You should only disable this if you want faster compile times in release builds - it produces larger binaries.
@@ -92,6 +93,8 @@ use tauri_runtime as runtime;
 mod ios;
 #[cfg(target_os = "android")]
 mod jni_helpers;
+#[cfg(desktop)]
+pub mod menu;
 /// Path APIs.
 pub mod path;
 pub mod process;
@@ -99,6 +102,9 @@ pub mod process;
 pub mod scope;
 mod state;
 
+#[cfg(all(desktop, feature = "tray-icon"))]
+#[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
+pub mod tray;
 pub use tauri_utils as utils;
 
 /// A Tauri [`Runtime`] wrapper around wry.
@@ -130,14 +136,20 @@ macro_rules! android_binding {
 
     // this function is a glue between PluginManager.kt > handlePluginResponse and Rust
     #[allow(non_snake_case)]
-    pub fn handlePluginResponse(env: JNIEnv, _: JClass, id: i32, success: JString, error: JString) {
-      ::tauri::handle_android_plugin_response(env, id, success, error);
+    pub fn handlePluginResponse(
+      mut env: JNIEnv,
+      _: JClass,
+      id: i32,
+      success: JString,
+      error: JString,
+    ) {
+      ::tauri::handle_android_plugin_response(&mut env, id, success, error);
     }
 
     // this function is a glue between PluginManager.kt > sendChannelData and Rust
     #[allow(non_snake_case)]
-    pub fn sendChannelData(env: JNIEnv, _: JClass, id: i64, data: JString) {
-      ::tauri::send_channel_data(env, id, data);
+    pub fn sendChannelData(mut env: JNIEnv, _: JClass, id: i64, data: JString) {
+      ::tauri::send_channel_data(&mut env, id, data);
     }
   };
 }
@@ -156,7 +168,11 @@ pub type Result<T> = std::result::Result<T, Error>;
 pub type SyncTask = Box<dyn FnOnce() + Send>;
 
 use serde::Serialize;
-use std::{collections::HashMap, fmt, sync::Arc};
+use std::{
+  collections::HashMap,
+  fmt::{self, Debug},
+  sync::Arc,
+};
 
 // Export types likely to be used by the application.
 pub use runtime::http;
@@ -167,22 +183,12 @@ pub use tauri_runtime_wry::webview_version;
 
 #[cfg(target_os = "macos")]
 #[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
-pub use runtime::{menu::NativeImage, ActivationPolicy};
+pub use runtime::ActivationPolicy;
 
 #[cfg(target_os = "macos")]
 pub use self::utils::TitleBarStyle;
-#[cfg(all(desktop, feature = "system-tray"))]
-#[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-pub use {
-  self::app::tray::{SystemTray, SystemTrayEvent, SystemTrayHandle, SystemTrayMenuItemHandle},
-  self::runtime::menu::{SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu},
-};
-pub use {
-  self::app::WindowMenuEvent,
-  self::event::{Event, EventHandler},
-  self::runtime::menu::{AboutMetadata, CustomMenuItem, Menu, MenuEntry, MenuItem, Submenu},
-  self::window::menu::MenuEvent,
-};
+
+pub use self::event::{Event, EventHandler};
 pub use {
   self::app::{
     App, AppHandle, AssetResolver, Builder, CloseRequestApi, GlobalWindowEvent, RunEvent,
@@ -249,7 +255,15 @@ pub fn log_stdout() {
 
 /// The user event type.
 #[derive(Debug, Clone)]
-pub enum EventLoopMessage {}
+pub enum EventLoopMessage {
+  /// An event from a menu item, could be on the window menu bar, application menu bar (on macOS) or tray icon menu.
+  #[cfg(desktop)]
+  MenuEvent(menu::MenuEvent),
+  /// An event from a menu item, could be on the window menu bar, application menu bar (on macOS) or tray icon menu.
+  #[cfg(all(desktop, feature = "tray-icon"))]
+  #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
+  TrayIconEvent(tray::TrayIconEvent),
+}
 
 /// The webview runtime interface. A wrapper around [`runtime::Runtime`] with the proper user event type associated.
 pub trait Runtime: runtime::Runtime<EventLoopMessage> {}
@@ -388,8 +402,8 @@ pub struct Context<A: Assets> {
   pub(crate) assets: Arc<A>,
   pub(crate) default_window_icon: Option<Icon>,
   pub(crate) app_icon: Option<Vec<u8>>,
-  #[cfg(desktop)]
-  pub(crate) system_tray_icon: Option<Icon>,
+  #[cfg(all(desktop, feature = "tray-icon"))]
+  pub(crate) tray_icon: Option<Icon>,
   pub(crate) package_info: PackageInfo,
   pub(crate) _info_plist: (),
   pub(crate) pattern: Pattern,
@@ -404,8 +418,8 @@ impl<A: Assets> fmt::Debug for Context<A> {
       .field("package_info", &self.package_info)
       .field("pattern", &self.pattern);
 
-    #[cfg(desktop)]
-    d.field("system_tray_icon", &self.system_tray_icon);
+    #[cfg(all(desktop, feature = "tray-icon"))]
+    d.field("tray_icon", &self.tray_icon);
 
     d.finish()
   }
@@ -449,17 +463,19 @@ impl<A: Assets> Context<A> {
   }
 
   /// The icon to use on the system tray UI.
-  #[cfg(desktop)]
+  #[cfg(all(desktop, feature = "tray-icon"))]
+  #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
   #[inline(always)]
-  pub fn system_tray_icon(&self) -> Option<&Icon> {
-    self.system_tray_icon.as_ref()
+  pub fn tray_icon(&self) -> Option<&Icon> {
+    self.tray_icon.as_ref()
   }
 
-  /// A mutable reference to the icon to use on the system tray UI.
-  #[cfg(desktop)]
+  /// A mutable reference to the icon to use on the tray icon.
+  #[cfg(all(desktop, feature = "tray-icon"))]
+  #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
   #[inline(always)]
-  pub fn system_tray_icon_mut(&mut self) -> &mut Option<Icon> {
-    &mut self.system_tray_icon
+  pub fn tray_icon_mut(&mut self) -> &mut Option<Icon> {
+    &mut self.tray_icon
   }
 
   /// Package information.
@@ -497,8 +513,8 @@ impl<A: Assets> Context<A> {
       assets,
       default_window_icon,
       app_icon,
-      #[cfg(desktop)]
-      system_tray_icon: None,
+      #[cfg(all(desktop, feature = "tray-icon"))]
+      tray_icon: None,
       package_info,
       _info_plist: info_plist,
       pattern,
@@ -506,10 +522,11 @@ impl<A: Assets> Context<A> {
   }
 
   /// Sets the app tray icon.
-  #[cfg(desktop)]
+  #[cfg(all(desktop, feature = "tray-icon"))]
+  #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "tray-icon"))))]
   #[inline(always)]
-  pub fn set_system_tray_icon(&mut self, icon: Icon) {
-    self.system_tray_icon.replace(icon);
+  pub fn set_tray_icon(&mut self, icon: Icon) {
+    self.tray_icon.replace(icon);
   }
 
   /// Sets the app shell scope.
@@ -524,7 +541,7 @@ impl<A: Assets> Context<A> {
 /// Manages a running application.
 pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
   /// The application handle associated with this manager.
-  fn app_handle(&self) -> AppHandle<R> {
+  fn app_handle(&self) -> &AppHandle<R> {
     self.managed_app_handle()
   }
 
@@ -648,7 +665,7 @@ pub trait Manager<R: Runtime>: sealed::ManagerBase<R> {
   ///
   /// tauri::Builder::default()
   ///   .setup(|app| {
-  ///     let handle = app.handle();
+  ///     let handle = app.handle().clone();
   ///     let handler = app.listen_global("ready", move |event| {
   ///       println!("app is ready");
   ///
@@ -845,7 +862,7 @@ pub(crate) mod sealed {
     /// The manager behind the [`Managed`] item.
     fn manager(&self) -> &WindowManager<R>;
     fn runtime(&self) -> RuntimeOrDispatch<'_, R>;
-    fn managed_app_handle(&self) -> AppHandle<R>;
+    fn managed_app_handle(&self) -> &AppHandle<R>;
   }
 }
 
@@ -895,6 +912,23 @@ mod tests {
   }
 }
 
+#[allow(unused)]
+macro_rules! run_main_thread {
+  ($self:ident, $ex:expr) => {{
+    use std::sync::mpsc::channel;
+    let (tx, rx) = channel();
+    let self_ = $self.clone();
+    let task = move || {
+      let _ = tx.send($ex(self_));
+    };
+    $self.app_handle.run_on_main_thread(Box::new(task))?;
+    rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage)
+  }};
+}
+
+#[allow(unused)]
+pub(crate) use run_main_thread;
+
 #[cfg(test)]
 mod test_utils {
   use proptest::prelude::*;

+ 146 - 75
core/tauri/src/manager.rs

@@ -10,6 +10,10 @@ use std::{
   sync::{Arc, Mutex, MutexGuard},
 };
 
+#[cfg(desktop)]
+use crate::menu::{Menu, MenuId};
+#[cfg(all(desktop, feature = "tray-icon"))]
+use crate::tray::{TrayIcon, TrayIconId};
 use serde::Serialize;
 use serialize_to_javascript::{default_template, DefaultTemplate, Template};
 use url::Url;
@@ -23,10 +27,7 @@ use tauri_utils::{
 };
 
 use crate::{
-  app::{
-    AppHandle, GlobalMenuEventListener, GlobalWindowEvent, GlobalWindowEventListener, OnPageLoad,
-    PageLoadPayload, WindowMenuEvent,
-  },
+  app::{AppHandle, GlobalWindowEvent, GlobalWindowEventListener, OnPageLoad, PageLoadPayload},
   event::{assert_event_name_is_valid, Event, EventHandler, Listeners},
   ipc::{Invoke, InvokeHandler, InvokeResponder},
   pattern::PatternJavascript,
@@ -49,11 +50,14 @@ use crate::{
   WindowEvent,
 };
 
+#[cfg(desktop)]
+use crate::app::GlobalMenuEventListener;
+#[cfg(all(desktop, feature = "tray-icon"))]
+use crate::app::GlobalTrayIconEventListener;
+
 #[cfg(any(target_os = "linux", target_os = "windows"))]
 use crate::path::BaseDirectory;
 
-use crate::{runtime::menu::Menu, MenuEvent};
-
 const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
 const WINDOW_MOVED_EVENT: &str = "tauri://move";
 const WINDOW_CLOSE_REQUESTED_EVENT: &str = "tauri://close-requested";
@@ -65,7 +69,6 @@ const WINDOW_THEME_CHANGED: &str = "tauri://theme-changed";
 const WINDOW_FILE_DROP_EVENT: &str = "tauri://file-drop";
 const WINDOW_FILE_DROP_HOVER_EVENT: &str = "tauri://file-drop-hover";
 const WINDOW_FILE_DROP_CANCELLED_EVENT: &str = "tauri://file-drop-cancelled";
-const MENU_EVENT: &str = "tauri://menu";
 
 pub(crate) const PROCESS_IPC_MESSAGE_FN: &str =
   include_str!("../scripts/process-ipc-message-fn.js");
@@ -210,9 +213,7 @@ fn replace_csp_nonce(
 
 #[default_runtime(crate::Wry, wry)]
 pub struct InnerWindowManager<R: Runtime> {
-  windows: Mutex<HashMap<String, Window<R>>>,
-  #[cfg(all(desktop, feature = "system-tray"))]
-  pub(crate) trays: Mutex<HashMap<String, crate::SystemTrayHandle<R>>>,
+  pub(crate) windows: Mutex<HashMap<String, Window<R>>>,
   pub(crate) plugins: Mutex<PluginStore<R>>,
   listeners: Listeners,
   pub(crate) state: Arc<StateManager>,
@@ -227,18 +228,42 @@ pub struct InnerWindowManager<R: Runtime> {
   assets: Arc<dyn Assets>,
   pub(crate) default_window_icon: Option<Icon>,
   pub(crate) app_icon: Option<Vec<u8>>,
-  #[cfg(desktop)]
+  #[cfg(all(desktop, feature = "tray-icon"))]
   pub(crate) tray_icon: Option<Icon>,
 
   package_info: PackageInfo,
   /// The webview protocols available to all windows.
   uri_scheme_protocols: HashMap<String, Arc<CustomProtocol<R>>>,
+  /// A set containing a reference to the active menus, including
+  /// the app-wide menu and the window-specific menus
+  ///
+  /// This should be mainly used to acceess [`Menu::haccel`]
+  /// to setup the accelerator handling in the event loop
+  #[cfg(desktop)]
+  pub menus: Arc<Mutex<HashMap<MenuId, Menu<R>>>>,
   /// The menu set to all windows.
-  menu: Option<Menu>,
+  #[cfg(desktop)]
+  pub(crate) menu: Arc<Mutex<Option<Menu<R>>>>,
   /// Menu event listeners to all windows.
-  menu_event_listeners: Arc<Vec<GlobalMenuEventListener<R>>>,
+  #[cfg(desktop)]
+  pub(crate) menu_event_listeners: Arc<Mutex<Vec<GlobalMenuEventListener<AppHandle<R>>>>>,
+  /// Menu event listeners to specific windows.
+  #[cfg(desktop)]
+  pub(crate) window_menu_event_listeners:
+    Arc<Mutex<HashMap<String, GlobalMenuEventListener<Window<R>>>>>,
   /// Window event listeners to all windows.
   window_event_listeners: Arc<Vec<GlobalWindowEventListener<R>>>,
+  /// Tray icons
+  #[cfg(all(desktop, feature = "tray-icon"))]
+  pub(crate) tray_icons: Arc<Mutex<Vec<TrayIcon<R>>>>,
+  /// Global Tray icon event listeners.
+  #[cfg(all(desktop, feature = "tray-icon"))]
+  pub(crate) global_tray_event_listeners:
+    Arc<Mutex<Vec<GlobalTrayIconEventListener<AppHandle<R>>>>>,
+  /// Tray icon event listeners.
+  #[cfg(all(desktop, feature = "tray-icon"))]
+  pub(crate) tray_event_listeners:
+    Arc<Mutex<HashMap<TrayIconId, GlobalTrayIconEventListener<TrayIcon<R>>>>>,
   /// Responder for invoke calls.
   invoke_responder: Option<Arc<InvokeResponder<R>>>,
   /// The script that initializes the invoke system.
@@ -257,10 +282,9 @@ impl<R: Runtime> fmt::Debug for InnerWindowManager<R> {
       .field("default_window_icon", &self.default_window_icon)
       .field("app_icon", &self.app_icon)
       .field("package_info", &self.package_info)
-      .field("menu", &self.menu)
       .field("pattern", &self.pattern);
 
-    #[cfg(desktop)]
+    #[cfg(all(desktop, feature = "tray-icon"))]
     d.field("tray_icon", &self.tray_icon);
 
     d.finish()
@@ -303,7 +327,7 @@ impl<R: Runtime> Clone for WindowManager<R> {
 }
 
 impl<R: Runtime> WindowManager<R> {
-  #[allow(clippy::too_many_arguments)]
+  #[allow(clippy::too_many_arguments, clippy::type_complexity)]
   pub(crate) fn with_handlers(
     #[allow(unused_mut)] mut context: Context<impl Assets>,
     plugins: PluginStore<R>,
@@ -312,7 +336,10 @@ impl<R: Runtime> WindowManager<R> {
     uri_scheme_protocols: HashMap<String, Arc<CustomProtocol<R>>>,
     state: StateManager,
     window_event_listeners: Vec<GlobalWindowEventListener<R>>,
-    (menu, menu_event_listeners): (Option<Menu>, Vec<GlobalMenuEventListener<R>>),
+    #[cfg(desktop)] window_menu_event_listeners: HashMap<
+      String,
+      GlobalMenuEventListener<Window<R>>,
+    >,
     (invoke_responder, invoke_initialization_script): (Option<Arc<InvokeResponder<R>>>, String),
   ) -> Self {
     // generate a random isolation key at runtime
@@ -324,8 +351,6 @@ impl<R: Runtime> WindowManager<R> {
     Self {
       inner: Arc::new(InnerWindowManager {
         windows: Mutex::default(),
-        #[cfg(all(desktop, feature = "system-tray"))]
-        trays: Default::default(),
         plugins: Mutex::new(plugins),
         listeners: Listeners::default(),
         state: Arc::new(state),
@@ -335,14 +360,26 @@ impl<R: Runtime> WindowManager<R> {
         assets: context.assets,
         default_window_icon: context.default_window_icon,
         app_icon: context.app_icon,
-        #[cfg(desktop)]
-        tray_icon: context.system_tray_icon,
+        #[cfg(all(desktop, feature = "tray-icon"))]
+        tray_icon: context.tray_icon,
         package_info: context.package_info,
         pattern: context.pattern,
         uri_scheme_protocols,
-        menu,
-        menu_event_listeners: Arc::new(menu_event_listeners),
+        #[cfg(desktop)]
+        menus: Default::default(),
+        #[cfg(desktop)]
+        menu: Default::default(),
+        #[cfg(desktop)]
+        menu_event_listeners: Default::default(),
+        #[cfg(desktop)]
+        window_menu_event_listeners: Arc::new(Mutex::new(window_menu_event_listeners)),
         window_event_listeners: Arc::new(window_event_listeners),
+        #[cfg(all(desktop, feature = "tray-icon"))]
+        tray_icons: Default::default(),
+        #[cfg(all(desktop, feature = "tray-icon"))]
+        global_tray_event_listeners: Default::default(),
+        #[cfg(all(desktop, feature = "tray-icon"))]
+        tray_event_listeners: Default::default(),
         invoke_responder,
         invoke_initialization_script,
       }),
@@ -363,6 +400,83 @@ impl<R: Runtime> WindowManager<R> {
     self.inner.state.clone()
   }
 
+  #[cfg(desktop)]
+  pub(crate) fn prepare_window_menu_creation_handler(
+    &self,
+    window_menu: Option<&crate::window::WindowMenu<R>>,
+  ) -> Option<impl Fn(tauri_runtime::window::RawWindow<'_>)> {
+    {
+      if let Some(menu) = window_menu {
+        self
+          .menus_stash_lock()
+          .insert(menu.menu.id().clone(), menu.menu.clone());
+      }
+    }
+
+    #[cfg(target_os = "macos")]
+    return None;
+
+    #[cfg_attr(target_os = "macos", allow(unused_variables, unreachable_code))]
+    if let Some(menu) = &window_menu {
+      let menu = menu.menu.clone();
+      Some(move |raw: tauri_runtime::window::RawWindow<'_>| {
+        #[cfg(target_os = "windows")]
+        let _ = menu.inner().init_for_hwnd(raw.hwnd as _);
+        #[cfg(any(
+          target_os = "linux",
+          target_os = "dragonfly",
+          target_os = "freebsd",
+          target_os = "netbsd",
+          target_os = "openbsd"
+        ))]
+        let _ = menu
+          .inner()
+          .init_for_gtk_window(raw.gtk_window, raw.default_vbox);
+      })
+    } else {
+      None
+    }
+  }
+
+  /// App-wide menu.
+  #[cfg(desktop)]
+  pub(crate) fn menu_lock(&self) -> MutexGuard<'_, Option<Menu<R>>> {
+    self.inner.menu.lock().expect("poisoned window manager")
+  }
+
+  /// Menus stash.
+  #[cfg(desktop)]
+  pub(crate) fn menus_stash_lock(&self) -> MutexGuard<'_, HashMap<MenuId, Menu<R>>> {
+    self.inner.menus.lock().expect("poisoned window manager")
+  }
+
+  #[cfg(desktop)]
+  pub(crate) fn is_menu_in_use<I: PartialEq<MenuId>>(&self, id: &I) -> bool {
+    self
+      .menu_lock()
+      .as_ref()
+      .map(|m| id.eq(m.id()))
+      .unwrap_or(false)
+  }
+
+  /// Menus stash.
+  #[cfg(desktop)]
+  pub(crate) fn insert_menu_into_stash(&self, menu: &Menu<R>) {
+    self
+      .menus_stash_lock()
+      .insert(menu.id().clone(), menu.clone());
+  }
+
+  #[cfg(desktop)]
+  pub(crate) fn remove_menu_from_stash_by_id(&self, id: Option<&MenuId>) {
+    if let Some(id) = id {
+      let is_used_by_a_window = self.windows_lock().values().any(|w| w.is_menu_in_use(id));
+      if !(self.is_menu_in_use(id) || is_used_by_a_window) {
+        self.menus_stash_lock().remove(id);
+      }
+    }
+  }
+
   /// The invoke responder.
   pub(crate) fn invoke_responder(&self) -> Option<Arc<InvokeResponder<R>>> {
     self.inner.invoke_responder.clone()
@@ -1037,12 +1151,6 @@ impl<R: Runtime> WindowManager<R> {
       }
     }
 
-    if pending.window_builder.get_menu().is_none() {
-      if let Some(menu) = &self.inner.menu {
-        pending = pending.set_menu(menu.clone());
-      }
-    }
-
     #[cfg(target_os = "android")]
     {
       pending = pending.on_webview_created(move |ctx| {
@@ -1136,12 +1244,19 @@ impl<R: Runtime> WindowManager<R> {
     Ok(pending)
   }
 
-  pub fn attach_window(
+  pub(crate) fn attach_window(
     &self,
     app_handle: AppHandle<R>,
     window: DetachedWindow<EventLoopMessage, R>,
+    #[cfg(desktop)] menu: Option<crate::window::WindowMenu<R>>,
   ) -> Window<R> {
-    let window = Window::new(self.clone(), window, app_handle);
+    let window = Window::new(
+      self.clone(),
+      window,
+      app_handle,
+      #[cfg(desktop)]
+      menu,
+    );
 
     let window_ = window.clone();
     let window_event_listeners = self.inner.window_event_listeners.clone();
@@ -1155,19 +1270,6 @@ impl<R: Runtime> WindowManager<R> {
         });
       }
     });
-    {
-      let window_ = window.clone();
-      let menu_event_listeners = self.inner.menu_event_listeners.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.clone(),
-          });
-        }
-      });
-    }
 
     // insert the window into our manager
     {
@@ -1297,33 +1399,6 @@ impl<R: Runtime> WindowManager<R> {
   }
 }
 
-/// Tray APIs
-#[cfg(all(desktop, feature = "system-tray"))]
-impl<R: Runtime> WindowManager<R> {
-  pub fn get_tray(&self, id: &str) -> Option<crate::SystemTrayHandle<R>> {
-    self.inner.trays.lock().unwrap().get(id).cloned()
-  }
-
-  pub fn trays(&self) -> HashMap<String, crate::SystemTrayHandle<R>> {
-    self.inner.trays.lock().unwrap().clone()
-  }
-
-  pub fn attach_tray(&self, id: String, tray: crate::SystemTrayHandle<R>) {
-    self.inner.trays.lock().unwrap().insert(id, tray);
-  }
-
-  pub fn get_tray_by_runtime_id(&self, id: u16) -> Option<(String, crate::SystemTrayHandle<R>)> {
-    let trays = self.inner.trays.lock().unwrap();
-    let iter = trays.iter();
-    for (tray_id, tray) in iter {
-      if tray.id == id {
-        return Some((tray_id.clone(), tray.clone()));
-      }
-    }
-    None
-  }
-}
-
 fn on_window_event<R: Runtime>(
   window: &Window<R>,
   manager: &WindowManager<R>,
@@ -1396,10 +1471,6 @@ struct ScaleFactorChanged {
   size: PhysicalSize<u32>,
 }
 
-fn on_menu_event<R: Runtime>(window: &Window<R>, event: &MenuEvent) -> crate::Result<()> {
-  window.emit(MENU_EVENT, event.menu_item_id.clone())
-}
-
 #[cfg(feature = "isolation")]
 fn request_to_path(request: &tauri_runtime::http::Request, base_url: &str) -> String {
   let mut path = request

+ 90 - 0
core/tauri/src/menu/builders/check.rs

@@ -0,0 +1,90 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::{menu::CheckMenuItem, menu::MenuId, Manager, Runtime};
+
+/// A builder type for [`CheckMenuItem`]
+pub struct CheckMenuItemBuilder {
+  id: Option<MenuId>,
+  text: String,
+  enabled: bool,
+  checked: bool,
+  accelerator: Option<String>,
+}
+
+impl CheckMenuItemBuilder {
+  /// Create a new menu item builder.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn new<S: AsRef<str>>(text: S) -> Self {
+    Self {
+      id: None,
+      text: text.as_ref().to_string(),
+      enabled: true,
+      checked: true,
+      accelerator: None,
+    }
+  }
+
+  /// Create a new menu item builder with the specified id.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn with_id<I: Into<MenuId>, S: AsRef<str>>(id: I, text: S) -> Self {
+    Self {
+      id: Some(id.into()),
+      text: text.as_ref().to_string(),
+      enabled: true,
+      checked: true,
+      accelerator: None,
+    }
+  }
+
+  /// Set the id for this menu item.
+  pub fn id<I: Into<MenuId>>(mut self, id: I) -> Self {
+    self.id.replace(id.into());
+    self
+  }
+
+  /// Set the enabled state for this menu item.
+  pub fn enabled(mut self, enabled: bool) -> Self {
+    self.enabled = enabled;
+    self
+  }
+
+  /// Set the checked state for this menu item.
+  pub fn checked(mut self, checked: bool) -> Self {
+    self.checked = checked;
+    self
+  }
+
+  /// Set the accelerator for this menu item.
+  pub fn accelerator<S: AsRef<str>>(mut self, accelerator: S) -> Self {
+    self.accelerator.replace(accelerator.as_ref().to_string());
+    self
+  }
+
+  /// Build the menu item
+  pub fn build<R: Runtime, M: Manager<R>>(self, manager: &M) -> CheckMenuItem<R> {
+    if let Some(id) = self.id {
+      CheckMenuItem::with_id(
+        manager,
+        id,
+        self.text,
+        self.enabled,
+        self.checked,
+        self.accelerator,
+      )
+    } else {
+      CheckMenuItem::new(
+        manager,
+        self.text,
+        self.enabled,
+        self.checked,
+        self.accelerator,
+      )
+    }
+  }
+}

+ 128 - 0
core/tauri/src/menu/builders/icon.rs

@@ -0,0 +1,128 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use muda::{MenuId, NativeIcon};
+
+use crate::{menu::IconMenuItem, Icon, Manager, Runtime};
+
+/// A builder type for [`IconMenuItem`]
+pub struct IconMenuItemBuilder {
+  id: Option<MenuId>,
+  text: String,
+  enabled: bool,
+  icon: Option<Icon>,
+  native_icon: Option<NativeIcon>,
+  accelerator: Option<String>,
+}
+
+impl IconMenuItemBuilder {
+  /// Create a new menu item builder.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn new<S: AsRef<str>>(text: S) -> Self {
+    Self {
+      id: None,
+      text: text.as_ref().to_string(),
+      enabled: true,
+      icon: None,
+      native_icon: None,
+      accelerator: None,
+    }
+  }
+
+  /// Create a new menu item builder with the specified id.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn with_id<I: Into<MenuId>, S: AsRef<str>>(id: I, text: S) -> Self {
+    Self {
+      id: Some(id.into()),
+      text: text.as_ref().to_string(),
+      enabled: true,
+      icon: None,
+      native_icon: None,
+      accelerator: None,
+    }
+  }
+
+  /// Set the id for this menu item.
+  pub fn id<I: Into<MenuId>>(mut self, id: I) -> Self {
+    self.id.replace(id.into());
+    self
+  }
+
+  /// Set the enabled state for this menu item.
+  pub fn enabled(mut self, enabled: bool) -> Self {
+    self.enabled = enabled;
+    self
+  }
+
+  /// Set the accelerator for this menu item.
+  pub fn accelerator<S: AsRef<str>>(mut self, accelerator: S) -> Self {
+    self.accelerator.replace(accelerator.as_ref().to_string());
+    self
+  }
+
+  /// Set the icon for this menu item.
+  ///
+  /// **Note:** This method conflicts with [`Self::native_icon`]
+  /// so calling one of them, will reset the other.
+  pub fn icon(mut self, icon: Icon) -> Self {
+    self.icon.replace(icon);
+    self.native_icon = None;
+    self
+  }
+
+  /// Set the icon for this menu item.
+  ///
+  /// **Note:** This method conflicts with [`Self::icon`]
+  /// so calling one of them, will reset the other.
+  pub fn native_icon(mut self, icon: NativeIcon) -> Self {
+    self.native_icon.replace(icon);
+    self.icon = None;
+    self
+  }
+
+  /// Build the menu item
+  pub fn build<R: Runtime, M: Manager<R>>(self, manager: &M) -> IconMenuItem<R> {
+    if self.icon.is_some() {
+      if let Some(id) = self.id {
+        IconMenuItem::with_id(
+          manager,
+          id,
+          self.text,
+          self.enabled,
+          self.icon,
+          self.accelerator,
+        )
+      } else {
+        IconMenuItem::new(
+          manager,
+          self.text,
+          self.enabled,
+          self.icon,
+          self.accelerator,
+        )
+      }
+    } else if let Some(id) = self.id {
+      IconMenuItem::with_id_and_native_icon(
+        manager,
+        id,
+        self.text,
+        self.enabled,
+        self.native_icon,
+        self.accelerator,
+      )
+    } else {
+      IconMenuItem::with_native_icon(
+        manager,
+        self.text,
+        self.enabled,
+        self.native_icon,
+        self.accelerator,
+      )
+    }
+  }
+}

+ 321 - 0
core/tauri/src/menu/builders/menu.rs

@@ -0,0 +1,321 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::{menu::*, Icon, Manager, Runtime};
+
+/// A builder type for [`Menu`]
+///
+/// # Example
+///
+/// ```no_run
+/// use tauri::menu::*;
+/// tauri::Builder::default()
+///   .setup(move |app| {
+///     let handle = app.handle();
+///     # let icon1 = tauri::Icon::Rgba {
+///     #   rgba: Vec::new(),
+///     #   width: 0,
+///     #   height: 0,
+///     # };
+///     let menu = MenuBuilder::new(handle)
+///       .item(&MenuItem::new(handle, "MenuItem 1", true, None))
+///       .items(&[
+///         &CheckMenuItem::new(handle, "CheckMenuItem 1", true, true, None),
+///         &IconMenuItem::new(handle, "IconMenuItem 1", true, Some(icon1), None),
+///       ])
+///       .separator()
+///       .cut()
+///       .copy()
+///       .paste()
+///       .separator()
+///       .text("MenuItem 2")
+///       .check("CheckMenuItem 2")
+///       .icon("IconMenuItem 2", app.default_window_icon().cloned().unwrap())
+///       .build()?;
+///     app.set_menu(menu);
+///     Ok(())
+///   });
+/// ```
+pub struct MenuBuilder<'m, R: Runtime, M: Manager<R>> {
+  id: Option<MenuId>,
+  manager: &'m M,
+  items: Vec<MenuItemKind<R>>,
+}
+
+impl<'m, R: Runtime, M: Manager<R>> MenuBuilder<'m, R, M> {
+  /// Create a new menu builder.
+  pub fn new(manager: &'m M) -> Self {
+    Self {
+      id: None,
+      items: Vec::new(),
+      manager,
+    }
+  }
+
+  /// Create a new menu builder with the specified id.
+  pub fn with_id<I: Into<MenuId>>(manager: &'m M, id: I) -> Self {
+    Self {
+      id: Some(id.into()),
+      items: Vec::new(),
+      manager,
+    }
+  }
+
+  /// Set the id for this menu.
+  pub fn id<I: Into<MenuId>>(mut self, id: I) -> Self {
+    self.id.replace(id.into());
+    self
+  }
+
+  /// Add this item to the menu.
+  pub fn item(mut self, item: &dyn IsMenuItem<R>) -> Self {
+    self.items.push(item.kind());
+    self
+  }
+
+  /// Add these items to the menu.
+  pub fn items(mut self, items: &[&dyn IsMenuItem<R>]) -> Self {
+    for item in items {
+      self = self.item(*item);
+    }
+    self
+  }
+
+  /// Add a [MenuItem] to the menu.
+  pub fn text<S: AsRef<str>>(mut self, text: S) -> Self {
+    self
+      .items
+      .push(MenuItem::new(self.manager, text, true, None).kind());
+    self
+  }
+
+  /// Add a [CheckMenuItem] to the menu.
+  pub fn check<S: AsRef<str>>(mut self, text: S) -> Self {
+    self
+      .items
+      .push(CheckMenuItem::new(self.manager, text, true, true, None).kind());
+    self
+  }
+
+  /// Add an [IconMenuItem] to the menu.
+  pub fn icon<S: AsRef<str>>(mut self, text: S, icon: Icon) -> Self {
+    self
+      .items
+      .push(IconMenuItem::new(self.manager, text, true, Some(icon), None).kind());
+    self
+  }
+
+  /// Add an [IconMenuItem] with a native icon to the menu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux**: Unsupported.
+  pub fn native_icon<S: AsRef<str>>(mut self, text: S, icon: NativeIcon) -> Self {
+    self
+      .items
+      .push(IconMenuItem::with_native_icon(self.manager, text, true, Some(icon), None).kind());
+    self
+  }
+
+  /// Add Separator menu item to the menu.
+  pub fn separator(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::separator(self.manager).kind());
+    self
+  }
+
+  /// Add Copy menu item to the menu.
+  pub fn copy(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::copy(self.manager, None).kind());
+    self
+  }
+
+  /// Add Cut menu item to the menu.
+  pub fn cut(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::cut(self.manager, None).kind());
+    self
+  }
+
+  /// Add Paste menu item to the menu.
+  pub fn paste(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::paste(self.manager, None).kind());
+    self
+  }
+
+  /// Add SelectAll menu item to the menu.
+  pub fn select_all(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::select_all(self.manager, None).kind());
+    self
+  }
+
+  /// Add Undo menu item to the menu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn undo(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::undo(self.manager, None).kind());
+    self
+  }
+  /// Add Redo menu item to the menu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn redo(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::redo(self.manager, None).kind());
+    self
+  }
+
+  /// Add Minimize window menu item to the menu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn minimize(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::minimize(self.manager, None).kind());
+    self
+  }
+
+  /// Add Maximize window menu item to the menu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn maximize(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::maximize(self.manager, None).kind());
+    self
+  }
+
+  /// Add Fullscreen menu item to the menu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn fullscreen(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::fullscreen(self.manager, None).kind());
+    self
+  }
+
+  /// Add Hide window menu item to the menu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn hide(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::hide(self.manager, None).kind());
+    self
+  }
+
+  /// Add Hide other windows menu item to the menu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn hide_others(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::hide_others(self.manager, None).kind());
+    self
+  }
+
+  /// Add Show all app windows menu item to the menu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn show_all(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::show_all(self.manager, None).kind());
+    self
+  }
+
+  /// Add Close window menu item to the menu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn close_window(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::close_window(self.manager, None).kind());
+    self
+  }
+
+  /// Add Quit app menu item to the menu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn quit(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::quit(self.manager, None).kind());
+    self
+  }
+
+  /// Add About app menu item to the menu.
+  pub fn about(mut self, metadata: Option<AboutMetadata>) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::about(self.manager, None, metadata).kind());
+    self
+  }
+
+  /// Add Services menu item to the menu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn services(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::services(self.manager, None).kind());
+    self
+  }
+
+  /// Builds this menu
+  pub fn build(self) -> crate::Result<Menu<R>> {
+    if self.items.is_empty() {
+      Ok(if let Some(id) = self.id {
+        Menu::with_id(self.manager, id)
+      } else {
+        Menu::new(self.manager)
+      })
+    } else {
+      let items = self
+        .items
+        .iter()
+        .map(|i| i as &dyn IsMenuItem<R>)
+        .collect::<Vec<_>>();
+      if let Some(id) = self.id {
+        Menu::with_id_and_items(self.manager, id, &items)
+      } else {
+        Menu::with_items(self.manager, &items)
+      }
+    }
+  }
+}

+ 20 - 0
core/tauri/src/menu/builders/mod.rs

@@ -0,0 +1,20 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#![cfg(desktop)]
+
+//! A module containting menu builder types
+
+pub use muda::AboutMetadataBuilder;
+
+mod menu;
+pub use menu::MenuBuilder;
+mod normal;
+pub use normal::MenuItemBuilder;
+mod submenu;
+pub use submenu::SubmenuBuilder;
+mod check;
+pub use check::CheckMenuItemBuilder;
+mod icon;
+pub use icon::IconMenuItemBuilder;

+ 68 - 0
core/tauri/src/menu/builders/normal.rs

@@ -0,0 +1,68 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::{menu::MenuId, menu::MenuItem, Manager, Runtime};
+
+/// A builder type for [`MenuItem`]
+pub struct MenuItemBuilder {
+  id: Option<MenuId>,
+  text: String,
+  enabled: bool,
+  accelerator: Option<String>,
+}
+
+impl MenuItemBuilder {
+  /// Create a new menu item builder.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn new<S: AsRef<str>>(text: S) -> Self {
+    Self {
+      id: None,
+      text: text.as_ref().to_string(),
+      enabled: true,
+      accelerator: None,
+    }
+  }
+
+  /// Create a new menu item builder with the specified id.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn with_id<I: Into<MenuId>, S: AsRef<str>>(id: I, text: S) -> Self {
+    Self {
+      id: Some(id.into()),
+      text: text.as_ref().to_string(),
+      enabled: true,
+      accelerator: None,
+    }
+  }
+
+  /// Set the id for this menu item.
+  pub fn id<I: Into<MenuId>>(mut self, id: I) -> Self {
+    self.id.replace(id.into());
+    self
+  }
+
+  /// Set the enabled state for this menu item.
+  pub fn enabled(mut self, enabled: bool) -> Self {
+    self.enabled = enabled;
+    self
+  }
+
+  /// Set the accelerator for this menu item.
+  pub fn accelerator<S: AsRef<str>>(mut self, accelerator: S) -> Self {
+    self.accelerator.replace(accelerator.as_ref().to_string());
+    self
+  }
+
+  /// Build the menu item
+  pub fn build<R: Runtime, M: Manager<R>>(self, manager: &M) -> MenuItem<R> {
+    if let Some(id) = self.id {
+      MenuItem::with_id(manager, id, self.text, self.enabled, self.accelerator)
+    } else {
+      MenuItem::new(manager, self.text, self.enabled, self.accelerator)
+    }
+  }
+}

+ 342 - 0
core/tauri/src/menu/builders/submenu.rs

@@ -0,0 +1,342 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::{menu::*, Icon, Manager, Runtime};
+
+/// A builder type for [`Submenu`]
+///
+/// # Example
+///
+/// ```no_run
+/// use tauri::menu::*;
+/// tauri::Builder::default()
+///   .setup(move |app| {
+///     let handle = app.handle();
+///     # let icon1 = tauri::Icon::Rgba {
+///     #   rgba: Vec::new(),
+///     #   width: 0,
+///     #   height: 0,
+///     # };
+///     # let icon2 = icon1.clone();
+///     let menu = Menu::new(handle);
+///     let submenu = SubmenuBuilder::new(handle, "File")
+///       .item(&MenuItem::new(handle, "MenuItem 1", true, None))
+///       .items(&[
+///         &CheckMenuItem::new(handle, "CheckMenuItem 1", true, true, None),
+///         &IconMenuItem::new(handle, "IconMenuItem 1", true, Some(icon1), None),
+///       ])
+///       .separator()
+///       .cut()
+///       .copy()
+///       .paste()
+///       .separator()
+///       .text("MenuItem 2")
+///       .check("CheckMenuItem 2")
+///       .icon("IconMenuItem 2", app.default_window_icon().cloned().unwrap())
+///       .build()?;
+///     menu.append(&submenu)?;
+///     app.set_menu(menu);
+///     Ok(())
+///   });
+/// ```
+pub struct SubmenuBuilder<'m, R: Runtime, M: Manager<R>> {
+  id: Option<MenuId>,
+  manager: &'m M,
+  text: String,
+  enabled: bool,
+  items: Vec<MenuItemKind<R>>,
+}
+
+impl<'m, R: Runtime, M: Manager<R>> SubmenuBuilder<'m, R, M> {
+  /// Create a new submenu builder.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn new<S: AsRef<str>>(manager: &'m M, text: S) -> Self {
+    Self {
+      id: None,
+      items: Vec::new(),
+      text: text.as_ref().to_string(),
+      enabled: true,
+      manager,
+    }
+  }
+
+  /// Create a new submenu builder with the specified id.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn with_id<I: Into<MenuId>, S: AsRef<str>>(manager: &'m M, id: I, text: S) -> Self {
+    Self {
+      id: Some(id.into()),
+      text: text.as_ref().to_string(),
+      enabled: true,
+      items: Vec::new(),
+      manager,
+    }
+  }
+
+  /// Set the id for this submenu.
+  pub fn id<I: Into<MenuId>>(mut self, id: I) -> Self {
+    self.id.replace(id.into());
+    self
+  }
+
+  /// Set the enabled state for the submenu.
+  pub fn enabled(mut self, enabled: bool) -> Self {
+    self.enabled = enabled;
+    self
+  }
+
+  /// Add this item to the submenu.
+  pub fn item(mut self, item: &dyn IsMenuItem<R>) -> Self {
+    self.items.push(item.kind());
+    self
+  }
+
+  /// Add these items to the submenu.
+  pub fn items(mut self, items: &[&dyn IsMenuItem<R>]) -> Self {
+    for item in items {
+      self = self.item(*item);
+    }
+    self
+  }
+
+  /// Add a [MenuItem] to the submenu.
+  pub fn text<S: AsRef<str>>(mut self, text: S) -> Self {
+    self
+      .items
+      .push(MenuItem::new(self.manager, text, true, None).kind());
+    self
+  }
+
+  /// Add a [CheckMenuItem] to the submenu.
+  pub fn check<S: AsRef<str>>(mut self, text: S) -> Self {
+    self
+      .items
+      .push(CheckMenuItem::new(self.manager, text, true, true, None).kind());
+    self
+  }
+
+  /// Add an [IconMenuItem] to the submenu.
+  pub fn icon<S: AsRef<str>>(mut self, text: S, icon: Icon) -> Self {
+    self
+      .items
+      .push(IconMenuItem::new(self.manager, text, true, Some(icon), None).kind());
+    self
+  }
+
+  /// Add an [IconMenuItem] with a native icon to the submenu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux**: Unsupported.
+  pub fn native_icon<S: AsRef<str>>(mut self, text: S, icon: NativeIcon) -> Self {
+    self
+      .items
+      .push(IconMenuItem::with_native_icon(self.manager, text, true, Some(icon), None).kind());
+    self
+  }
+
+  /// Add Separator menu item to the submenu.
+  pub fn separator(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::separator(self.manager).kind());
+    self
+  }
+
+  /// Add Copy menu item to the submenu.
+  pub fn copy(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::copy(self.manager, None).kind());
+    self
+  }
+
+  /// Add Cut menu item to the submenu.
+  pub fn cut(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::cut(self.manager, None).kind());
+    self
+  }
+
+  /// Add Paste menu item to the submenu.
+  pub fn paste(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::paste(self.manager, None).kind());
+    self
+  }
+
+  /// Add SelectAll menu item to the submenu.
+  pub fn select_all(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::select_all(self.manager, None).kind());
+    self
+  }
+
+  /// Add Undo menu item to the submenu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn undo(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::undo(self.manager, None).kind());
+    self
+  }
+  /// Add Redo menu item to the submenu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn redo(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::redo(self.manager, None).kind());
+    self
+  }
+
+  /// Add Minimize window menu item to the submenu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn minimize(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::minimize(self.manager, None).kind());
+    self
+  }
+
+  /// Add Maximize window menu item to the submenu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn maximize(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::maximize(self.manager, None).kind());
+    self
+  }
+
+  /// Add Fullscreen menu item to the submenu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn fullscreen(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::fullscreen(self.manager, None).kind());
+    self
+  }
+
+  /// Add Hide window menu item to the submenu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn hide(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::hide(self.manager, None).kind());
+    self
+  }
+
+  /// Add Hide other windows menu item to the submenu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn hide_others(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::hide_others(self.manager, None).kind());
+    self
+  }
+
+  /// Add Show all app windows menu item to the submenu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn show_all(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::show_all(self.manager, None).kind());
+    self
+  }
+
+  /// Add Close window menu item to the submenu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn close_window(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::close_window(self.manager, None).kind());
+    self
+  }
+
+  /// Add Quit app menu item to the submenu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn quit(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::quit(self.manager, None).kind());
+    self
+  }
+
+  /// Add About app menu item to the submenu.
+  pub fn about(mut self, metadata: Option<AboutMetadata>) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::about(self.manager, None, metadata).kind());
+    self
+  }
+
+  /// Add Services menu item to the submenu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn services(mut self) -> Self {
+    self
+      .items
+      .push(PredefinedMenuItem::services(self.manager, None).kind());
+    self
+  }
+
+  /// Builds this submenu
+  pub fn build(self) -> crate::Result<Submenu<R>> {
+    if self.items.is_empty() {
+      Ok(if let Some(id) = self.id {
+        Submenu::with_id(self.manager, id, self.text, self.enabled)
+      } else {
+        Submenu::new(self.manager, self.text, self.enabled)
+      })
+    } else {
+      let items = self
+        .items
+        .iter()
+        .map(|i| i as &dyn IsMenuItem<R>)
+        .collect::<Vec<_>>();
+      if let Some(id) = self.id {
+        Submenu::with_id_and_items(self.manager, id, self.text, self.enabled, &items)
+      } else {
+        Submenu::with_items(self.manager, self.text, self.enabled, &items)
+      }
+    }
+  }
+}

+ 148 - 0
core/tauri/src/menu/check.rs

@@ -0,0 +1,148 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::{menu::MenuId, run_main_thread, AppHandle, Manager, Runtime};
+
+/// A menu item inside a [`Menu`] or [`Submenu`] and contains only text.
+///
+/// [`Menu`]: super::Menu
+/// [`Submenu`]: super::Submenu
+pub struct CheckMenuItem<R: Runtime> {
+  pub(crate) id: MenuId,
+  pub(crate) inner: muda::CheckMenuItem,
+  pub(crate) app_handle: AppHandle<R>,
+}
+
+impl<R: Runtime> Clone for CheckMenuItem<R> {
+  fn clone(&self) -> Self {
+    Self {
+      id: self.id.clone(),
+      inner: self.inner.clone(),
+      app_handle: self.app_handle.clone(),
+    }
+  }
+}
+
+/// # Safety
+///
+/// We make sure it always runs on the main thread.
+unsafe impl<R: Runtime> Sync for CheckMenuItem<R> {}
+unsafe impl<R: Runtime> Send for CheckMenuItem<R> {}
+
+impl<R: Runtime> super::sealed::IsMenuItemBase for CheckMenuItem<R> {
+  fn inner(&self) -> &dyn muda::IsMenuItem {
+    &self.inner
+  }
+}
+
+impl<R: Runtime> super::IsMenuItem<R> for CheckMenuItem<R> {
+  fn kind(&self) -> super::MenuItemKind<R> {
+    super::MenuItemKind::Check(self.clone())
+  }
+
+  fn id(&self) -> &MenuId {
+    &self.id
+  }
+}
+
+impl<R: Runtime> CheckMenuItem<R> {
+  /// Create a new menu item.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn new<M: Manager<R>, S: AsRef<str>>(
+    manager: &M,
+    text: S,
+    enabled: bool,
+    checked: bool,
+    acccelerator: Option<S>,
+  ) -> Self {
+    let item = muda::CheckMenuItem::new(
+      text,
+      enabled,
+      checked,
+      acccelerator.and_then(|s| s.as_ref().parse().ok()),
+    );
+    Self {
+      id: item.id().clone(),
+      inner: item,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Create a new menu item with the specified id.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn with_id<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
+    manager: &M,
+    id: I,
+    text: S,
+    enabled: bool,
+    checked: bool,
+    acccelerator: Option<S>,
+  ) -> Self {
+    let item = muda::CheckMenuItem::with_id(
+      id,
+      text,
+      enabled,
+      checked,
+      acccelerator.and_then(|s| s.as_ref().parse().ok()),
+    );
+    Self {
+      id: item.id().clone(),
+      inner: item,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// The application handle associated with this type.
+  pub fn app_handle(&self) -> &AppHandle<R> {
+    &self.app_handle
+  }
+
+  /// Returns a unique identifier associated with this menu item.
+  pub fn id(&self) -> &MenuId {
+    &self.id
+  }
+
+  /// Get the text for this menu item.
+  pub fn text(&self) -> crate::Result<String> {
+    run_main_thread!(self, |self_: Self| self_.inner.text())
+  }
+
+  /// Set the text for this menu item. `text` could optionally contain
+  /// an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn set_text<S: AsRef<str>>(&self, text: S) -> crate::Result<()> {
+    let text = text.as_ref().to_string();
+    run_main_thread!(self, |self_: Self| self_.inner.set_text(text))
+  }
+
+  /// Get whether this menu item is enabled or not.
+  pub fn is_enabled(&self) -> crate::Result<bool> {
+    run_main_thread!(self, |self_: Self| self_.inner.is_enabled())
+  }
+
+  /// Enable or disable this menu item.
+  pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
+    run_main_thread!(self, |self_: Self| self_.inner.set_enabled(enabled))
+  }
+
+  /// Set this menu item accelerator.
+  pub fn set_accelerator<S: AsRef<str>>(&self, acccelerator: Option<S>) -> crate::Result<()> {
+    let accel = acccelerator.and_then(|s| s.as_ref().parse().ok());
+    run_main_thread!(self, |self_: Self| self_.inner.set_accelerator(accel))?.map_err(Into::into)
+  }
+
+  /// Get whether this check menu item is checked or not.
+  pub fn is_checked(&self) -> crate::Result<bool> {
+    run_main_thread!(self, |self_: Self| self_.inner.is_checked())
+  }
+
+  /// Check or Uncheck this check menu item.
+  pub fn set_checked(&self, checked: bool) -> crate::Result<()> {
+    run_main_thread!(self, |self_: Self| self_.inner.set_checked(checked))
+  }
+}

+ 214 - 0
core/tauri/src/menu/icon.rs

@@ -0,0 +1,214 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use super::NativeIcon;
+use crate::{menu::MenuId, run_main_thread, AppHandle, Icon, Manager, Runtime};
+
+/// A menu item inside a [`Menu`] or [`Submenu`] and contains only text.
+///
+/// [`Menu`]: super::Menu
+/// [`Submenu`]: super::Submenu
+pub struct IconMenuItem<R: Runtime> {
+  pub(crate) id: MenuId,
+  pub(crate) inner: muda::IconMenuItem,
+  pub(crate) app_handle: AppHandle<R>,
+}
+
+impl<R: Runtime> Clone for IconMenuItem<R> {
+  fn clone(&self) -> Self {
+    Self {
+      id: self.id.clone(),
+      inner: self.inner.clone(),
+      app_handle: self.app_handle.clone(),
+    }
+  }
+}
+
+/// # Safety
+///
+/// We make sure it always runs on the main thread.
+unsafe impl<R: Runtime> Sync for IconMenuItem<R> {}
+unsafe impl<R: Runtime> Send for IconMenuItem<R> {}
+
+impl<R: Runtime> super::sealed::IsMenuItemBase for IconMenuItem<R> {
+  fn inner(&self) -> &dyn muda::IsMenuItem {
+    &self.inner
+  }
+}
+
+impl<R: Runtime> super::IsMenuItem<R> for IconMenuItem<R> {
+  fn kind(&self) -> super::MenuItemKind<R> {
+    super::MenuItemKind::Icon(self.clone())
+  }
+
+  fn id(&self) -> &MenuId {
+    &self.id
+  }
+}
+
+impl<R: Runtime> IconMenuItem<R> {
+  /// Create a new menu item.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn new<M: Manager<R>, S: AsRef<str>>(
+    manager: &M,
+    text: S,
+    enabled: bool,
+    icon: Option<Icon>,
+    acccelerator: Option<S>,
+  ) -> Self {
+    let item = muda::IconMenuItem::new(
+      text,
+      enabled,
+      icon.and_then(|i| i.try_into().ok()),
+      acccelerator.and_then(|s| s.as_ref().parse().ok()),
+    );
+    Self {
+      id: item.id().clone(),
+      inner: item,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Create a new menu item with the specified id.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn with_id<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
+    manager: &M,
+    id: I,
+    text: S,
+    enabled: bool,
+    icon: Option<Icon>,
+    acccelerator: Option<S>,
+  ) -> Self {
+    let item = muda::IconMenuItem::with_id(
+      id,
+      text,
+      enabled,
+      icon.and_then(|i| i.try_into().ok()),
+      acccelerator.and_then(|s| s.as_ref().parse().ok()),
+    );
+    Self {
+      id: item.id().clone(),
+      inner: item,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Create a new icon menu item but with a native icon.
+  ///
+  /// See [`IconMenuItem::new`] for more info.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux**: Unsupported.
+  pub fn with_native_icon<M: Manager<R>, S: AsRef<str>>(
+    manager: &M,
+    text: S,
+    enabled: bool,
+    native_icon: Option<NativeIcon>,
+    acccelerator: Option<S>,
+  ) -> Self {
+    let item = muda::IconMenuItem::with_native_icon(
+      text,
+      enabled,
+      native_icon,
+      acccelerator.and_then(|s| s.as_ref().parse().ok()),
+    );
+    Self {
+      id: item.id().clone(),
+      inner: item,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Create a new icon menu item with the specified id but with a native icon.
+  ///
+  /// See [`IconMenuItem::new`] for more info.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux**: Unsupported.
+  pub fn with_id_and_native_icon<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
+    manager: &M,
+    id: I,
+    text: S,
+    enabled: bool,
+    native_icon: Option<NativeIcon>,
+    acccelerator: Option<S>,
+  ) -> Self {
+    let item = muda::IconMenuItem::with_id_and_native_icon(
+      id,
+      text,
+      enabled,
+      native_icon,
+      acccelerator.and_then(|s| s.as_ref().parse().ok()),
+    );
+    Self {
+      id: item.id().clone(),
+      inner: item,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// The application handle associated with this type.
+  pub fn app_handle(&self) -> &AppHandle<R> {
+    &self.app_handle
+  }
+
+  /// Returns a unique identifier associated with this menu item.
+  pub fn id(&self) -> &MenuId {
+    &self.id
+  }
+
+  /// Get the text for this menu item.
+  pub fn text(&self) -> crate::Result<String> {
+    run_main_thread!(self, |self_: Self| self_.inner.text())
+  }
+
+  /// Set the text for this menu item. `text` could optionally contain
+  /// an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn set_text<S: AsRef<str>>(&self, text: S) -> crate::Result<()> {
+    let text = text.as_ref().to_string();
+    run_main_thread!(self, |self_: Self| self_.inner.set_text(text))
+  }
+
+  /// Get whether this menu item is enabled or not.
+  pub fn is_enabled(&self) -> crate::Result<bool> {
+    run_main_thread!(self, |self_: Self| self_.inner.is_enabled())
+  }
+
+  /// Enable or disable this menu item.
+  pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
+    run_main_thread!(self, |self_: Self| self_.inner.set_enabled(enabled))
+  }
+
+  /// Set this menu item accelerator.
+  pub fn set_accelerator<S: AsRef<str>>(&self, acccelerator: Option<S>) -> crate::Result<()> {
+    let accel = acccelerator.and_then(|s| s.as_ref().parse().ok());
+    run_main_thread!(self, |self_: Self| self_.inner.set_accelerator(accel))?.map_err(Into::into)
+  }
+
+  /// Change this menu item icon or remove it.
+  pub fn set_icon(&self, icon: Option<Icon>) -> crate::Result<()> {
+    run_main_thread!(self, |self_: Self| self_
+      .inner
+      .set_icon(icon.and_then(|i| i.try_into().ok())))
+  }
+
+  /// Change this menu item icon to a native image or remove it.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux**: Unsupported.
+  pub fn set_native_icon(&mut self, _icon: Option<NativeIcon>) -> crate::Result<()> {
+    #[cfg(target_os = "macos")]
+    return run_main_thread!(self, |mut self_: Self| self_.inner.set_native_icon(_icon));
+    #[allow(unreachable_code)]
+    Ok(())
+  }
+}

+ 397 - 0
core/tauri/src/menu/menu.rs

@@ -0,0 +1,397 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use super::sealed::ContextMenuBase;
+use super::{IsMenuItem, MenuItemKind, PredefinedMenuItem, Submenu};
+use crate::Window;
+use crate::{run_main_thread, AppHandle, Manager, Position, Runtime};
+use muda::ContextMenu;
+use muda::{AboutMetadata, MenuId};
+
+/// Expected submenu id of the Window menu for macOS.
+pub const WINDOW_SUBMENU_ID: &str = "__tauri_window_menu__";
+/// Expected submenu id of the Help menu for macOS.
+pub const HELP_SUBMENU_ID: &str = "__tauri_help_menu__";
+
+/// A type that is either a menu bar on the window
+/// on Windows and Linux or as a global menu in the menubar on macOS.
+pub struct Menu<R: Runtime> {
+  pub(crate) id: MenuId,
+  pub(crate) inner: muda::Menu,
+  pub(crate) app_handle: AppHandle<R>,
+}
+
+/// # Safety
+///
+/// We make sure it always runs on the main thread.
+unsafe impl<R: Runtime> Sync for Menu<R> {}
+unsafe impl<R: Runtime> Send for Menu<R> {}
+
+impl<R: Runtime> Clone for Menu<R> {
+  fn clone(&self) -> Self {
+    Self {
+      id: self.id.clone(),
+      inner: self.inner.clone(),
+      app_handle: self.app_handle.clone(),
+    }
+  }
+}
+
+impl<R: Runtime> super::ContextMenu for Menu<R> {
+  fn popup<T: Runtime>(&self, window: Window<T>) -> crate::Result<()> {
+    self.popup_inner(window, None::<Position>)
+  }
+
+  fn popup_at<T: Runtime, P: Into<Position>>(
+    &self,
+    window: Window<T>,
+    position: P,
+  ) -> crate::Result<()> {
+    self.popup_inner(window, Some(position))
+  }
+}
+
+impl<R: Runtime> ContextMenuBase for Menu<R> {
+  fn popup_inner<T: Runtime, P: Into<crate::Position>>(
+    &self,
+    window: crate::Window<T>,
+    position: Option<P>,
+  ) -> crate::Result<()> {
+    let position = position.map(Into::into).map(super::into_position);
+    run_main_thread!(self, move |self_: Self| {
+      #[cfg(target_os = "macos")]
+      if let Ok(view) = window.ns_view() {
+        self_
+          .inner()
+          .show_context_menu_for_nsview(view as _, position);
+      }
+
+      #[cfg(any(
+        target_os = "linux",
+        target_os = "dragonfly",
+        target_os = "freebsd",
+        target_os = "netbsd",
+        target_os = "openbsd"
+      ))]
+      if let Ok(w) = window.gtk_window() {
+        self_.inner().show_context_menu_for_gtk_window(&w, position);
+      }
+
+      #[cfg(windows)]
+      if let Ok(hwnd) = window.hwnd() {
+        self_.inner().show_context_menu_for_hwnd(hwnd.0, position)
+      }
+    })
+  }
+  fn inner(&self) -> &dyn muda::ContextMenu {
+    &self.inner
+  }
+
+  fn inner_owned(&self) -> Box<dyn muda::ContextMenu> {
+    Box::new(self.clone().inner)
+  }
+}
+
+impl<R: Runtime> Menu<R> {
+  /// Creates a new menu.
+  pub fn new<M: Manager<R>>(manager: &M) -> Self {
+    let menu = muda::Menu::new();
+    Self {
+      id: menu.id().clone(),
+      inner: menu,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Creates a new menu with the specified id.
+  pub fn with_id<M: Manager<R>, I: Into<MenuId>>(manager: &M, id: I) -> Self {
+    let menu = muda::Menu::with_id(id);
+    Self {
+      id: menu.id().clone(),
+      inner: menu,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Creates a new menu with given `items`. It calls [`Menu::new`] and [`Menu::append_items`] internally.
+  pub fn with_items<M: Manager<R>>(
+    manager: &M,
+    items: &[&dyn IsMenuItem<R>],
+  ) -> crate::Result<Self> {
+    let menu = Self::new(manager);
+    menu.append_items(items)?;
+    Ok(menu)
+  }
+
+  /// Creates a new menu with the specified id and given `items`.
+  /// It calls [`Menu::new`] and [`Menu::append_items`] internally.
+  pub fn with_id_and_items<M: Manager<R>, I: Into<MenuId>>(
+    manager: &M,
+    id: I,
+    items: &[&dyn IsMenuItem<R>],
+  ) -> crate::Result<Self> {
+    let menu = Self::with_id(manager, id);
+    menu.append_items(items)?;
+    Ok(menu)
+  }
+
+  /// Creates a menu filled with default menu items and submenus.
+  pub fn default(app_handle: &AppHandle<R>) -> crate::Result<Self> {
+    let pkg_info = app_handle.package_info();
+    let config = app_handle.config();
+    let about_metadata = AboutMetadata {
+      name: Some(pkg_info.name.clone()),
+      version: Some(pkg_info.version.to_string()),
+      copyright: config.tauri.bundle.copyright.clone(),
+      authors: config.tauri.bundle.publisher.clone().map(|p| vec![p]),
+      ..Default::default()
+    };
+
+    let window_menu = Submenu::with_id_and_items(
+      app_handle,
+      WINDOW_SUBMENU_ID,
+      "Window",
+      true,
+      &[
+        &PredefinedMenuItem::minimize(app_handle, None),
+        &PredefinedMenuItem::maximize(app_handle, None),
+        #[cfg(target_os = "macos")]
+        &PredefinedMenuItem::separator(app_handle),
+        &PredefinedMenuItem::close_window(app_handle, None),
+      ],
+    )?;
+
+    let help_menu = Submenu::with_id_and_items(
+      app_handle,
+      HELP_SUBMENU_ID,
+      "Help",
+      true,
+      &[
+        #[cfg(not(target_os = "macos"))]
+        &PredefinedMenuItem::about(app_handle, None, Some(about_metadata)),
+      ],
+    )?;
+
+    let menu = Menu::with_items(
+      app_handle,
+      &[
+        #[cfg(target_os = "macos")]
+        &Submenu::with_items(
+          app_handle,
+          pkg_info.name.clone(),
+          true,
+          &[
+            &PredefinedMenuItem::about(app_handle, None, Some(about_metadata)),
+            &PredefinedMenuItem::separator(app_handle),
+            &PredefinedMenuItem::services(app_handle, None),
+            &PredefinedMenuItem::separator(app_handle),
+            &PredefinedMenuItem::hide(app_handle, None),
+            &PredefinedMenuItem::hide_others(app_handle, None),
+            &PredefinedMenuItem::separator(app_handle),
+            &PredefinedMenuItem::quit(app_handle, None),
+          ],
+        )?,
+        #[cfg(not(any(
+          target_os = "linux",
+          target_os = "dragonfly",
+          target_os = "freebsd",
+          target_os = "netbsd",
+          target_os = "openbsd"
+        )))]
+        &Submenu::with_items(
+          app_handle,
+          "File",
+          true,
+          &[
+            &PredefinedMenuItem::close_window(app_handle, None),
+            #[cfg(not(target_os = "macos"))]
+            &PredefinedMenuItem::quit(app_handle, None),
+          ],
+        )?,
+        &Submenu::with_items(
+          app_handle,
+          "Edit",
+          true,
+          &[
+            &PredefinedMenuItem::undo(app_handle, None),
+            &PredefinedMenuItem::redo(app_handle, None),
+            &PredefinedMenuItem::separator(app_handle),
+            &PredefinedMenuItem::cut(app_handle, None),
+            &PredefinedMenuItem::copy(app_handle, None),
+            &PredefinedMenuItem::paste(app_handle, None),
+            &PredefinedMenuItem::select_all(app_handle, None),
+          ],
+        )?,
+        #[cfg(target_os = "macos")]
+        &Submenu::with_items(
+          app_handle,
+          "View",
+          true,
+          &[&PredefinedMenuItem::fullscreen(app_handle, None)],
+        )?,
+        &window_menu,
+        &help_menu,
+      ],
+    )?;
+
+    Ok(menu)
+  }
+
+  pub(crate) fn inner(&self) -> &muda::Menu {
+    &self.inner
+  }
+
+  /// The application handle associated with this type.
+  pub fn app_handle(&self) -> &AppHandle<R> {
+    &self.app_handle
+  }
+
+  /// Returns a unique identifier associated with this menu.
+  pub fn id(&self) -> &MenuId {
+    &self.id
+  }
+
+  /// Add a menu item to the end of this menu.
+  ///
+  /// ## Platform-spcific:
+  ///
+  /// - **macOS:** Only [`Submenu`] can be added to the menu.
+  ///
+  /// [`Submenu`]: super::Submenu
+  pub fn append(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
+    let kind = item.kind();
+    run_main_thread!(self, |self_: Self| self_.inner.append(kind.inner().inner()))?
+      .map_err(Into::into)
+  }
+
+  /// Add menu items to the end of this menu. It calls [`Menu::append`] in a loop internally.
+  ///
+  /// ## Platform-spcific:
+  ///
+  /// - **macOS:** Only [`Submenu`] can be added to the menu
+  ///
+  /// [`Submenu`]: super::Submenu
+  pub fn append_items(&self, items: &[&dyn IsMenuItem<R>]) -> crate::Result<()> {
+    for item in items {
+      self.append(*item)?
+    }
+
+    Ok(())
+  }
+
+  /// Add a menu item to the beginning of this menu.
+  ///
+  /// ## Platform-spcific:
+  ///
+  /// - **macOS:** Only [`Submenu`] can be added to the menu
+  ///
+  /// [`Submenu`]: super::Submenu
+  pub fn prepend(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
+    let kind = item.kind();
+    run_main_thread!(self, |self_: Self| self_
+      .inner
+      .prepend(kind.inner().inner()))?
+    .map_err(Into::into)
+  }
+
+  /// Add menu items to the beginning of this menu. It calls [`Menu::insert_items`] with position of `0` internally.
+  ///
+  /// ## Platform-spcific:
+  ///
+  /// - **macOS:** Only [`Submenu`] can be added to the menu
+  ///
+  /// [`Submenu`]: super::Submenu
+  pub fn prepend_items(&self, items: &[&dyn IsMenuItem<R>]) -> crate::Result<()> {
+    self.insert_items(items, 0)
+  }
+
+  /// Insert a menu item at the specified `postion` in the menu.
+  ///
+  /// ## Platform-spcific:
+  ///
+  /// - **macOS:** Only [`Submenu`] can be added to the menu
+  ///
+  /// [`Submenu`]: super::Submenu
+  pub fn insert(&self, item: &dyn IsMenuItem<R>, position: usize) -> crate::Result<()> {
+    let kind = item.kind();
+    run_main_thread!(self, |self_: Self| self_
+      .inner
+      .insert(kind.inner().inner(), position))?
+    .map_err(Into::into)
+  }
+
+  /// Insert menu items at the specified `postion` in the menu.
+  ///
+  /// ## Platform-spcific:
+  ///
+  /// - **macOS:** Only [`Submenu`] can be added to the menu
+  ///
+  /// [`Submenu`]: super::Submenu
+  pub fn insert_items(&self, items: &[&dyn IsMenuItem<R>], position: usize) -> crate::Result<()> {
+    for (i, item) in items.iter().enumerate() {
+      self.insert(*item, position + i)?
+    }
+
+    Ok(())
+  }
+
+  /// Remove a menu item from this menu.
+  pub fn remove(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
+    let kind = item.kind();
+    run_main_thread!(self, |self_: Self| self_.inner.remove(kind.inner().inner()))?
+      .map_err(Into::into)
+  }
+
+  /// Retrieves the menu item matching the given identifier.
+  pub fn get<'a, I>(&self, id: &'a I) -> Option<MenuItemKind<R>>
+  where
+    I: ?Sized,
+    MenuId: PartialEq<&'a I>,
+  {
+    self
+      .items()
+      .unwrap_or_default()
+      .into_iter()
+      .find(|i| i.id() == &id)
+  }
+
+  /// Returns a list of menu items that has been added to this menu.
+  pub fn items(&self) -> crate::Result<Vec<MenuItemKind<R>>> {
+    let handle = self.app_handle.clone();
+    run_main_thread!(self, |self_: Self| self_
+      .inner
+      .items()
+      .into_iter()
+      .map(|i| match i {
+        muda::MenuItemKind::MenuItem(i) => super::MenuItemKind::MenuItem(super::MenuItem {
+          id: i.id().clone(),
+          inner: i,
+          app_handle: handle.clone(),
+        }),
+        muda::MenuItemKind::Submenu(i) => super::MenuItemKind::Submenu(super::Submenu {
+          id: i.id().clone(),
+          inner: i,
+          app_handle: handle.clone(),
+        }),
+        muda::MenuItemKind::Predefined(i) => {
+          super::MenuItemKind::Predefined(super::PredefinedMenuItem {
+            id: i.id().clone(),
+            inner: i,
+            app_handle: handle.clone(),
+          })
+        }
+        muda::MenuItemKind::Check(i) => super::MenuItemKind::Check(super::CheckMenuItem {
+          id: i.id().clone(),
+          inner: i,
+          app_handle: handle.clone(),
+        }),
+        muda::MenuItemKind::Icon(i) => super::MenuItemKind::Icon(super::IconMenuItem {
+          id: i.id().clone(),
+          inner: i,
+          app_handle: handle.clone(),
+        }),
+      })
+      .collect::<Vec<_>>())
+  }
+}

+ 251 - 0
core/tauri/src/menu/mod.rs

@@ -0,0 +1,251 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#![cfg(desktop)]
+
+//! Menu types and utility functions
+
+// TODO(muda-migration): figure out js events
+
+mod builders;
+mod check;
+mod icon;
+#[allow(clippy::module_inception)]
+mod menu;
+mod normal;
+mod predefined;
+mod submenu;
+pub use builders::*;
+pub use check::CheckMenuItem;
+pub use icon::IconMenuItem;
+pub use menu::{Menu, HELP_SUBMENU_ID, WINDOW_SUBMENU_ID};
+pub use normal::MenuItem;
+pub use predefined::PredefinedMenuItem;
+pub use submenu::Submenu;
+
+use crate::Runtime;
+pub use muda::{AboutMetadata, MenuEvent, MenuId, NativeIcon};
+
+/// An enumeration of all menu item kinds that could be added to
+/// a [`Menu`] or [`Submenu`]
+pub enum MenuItemKind<R: Runtime> {
+  /// Normal menu item
+  MenuItem(MenuItem<R>),
+  /// Submenu menu item
+  Submenu(Submenu<R>),
+  /// Predefined menu item
+  Predefined(PredefinedMenuItem<R>),
+  /// Check menu item
+  Check(CheckMenuItem<R>),
+  /// Icon menu item
+  Icon(IconMenuItem<R>),
+}
+
+impl<R: Runtime> MenuItemKind<R> {
+  /// Returns a unique identifier associated with this menu item.
+  pub fn id(&self) -> &MenuId {
+    match self {
+      MenuItemKind::MenuItem(i) => i.id(),
+      MenuItemKind::Submenu(i) => i.id(),
+      MenuItemKind::Predefined(i) => i.id(),
+      MenuItemKind::Check(i) => i.id(),
+      MenuItemKind::Icon(i) => i.id(),
+    }
+  }
+
+  pub(crate) fn inner(&self) -> &dyn IsMenuItem<R> {
+    match self {
+      MenuItemKind::MenuItem(i) => i,
+      MenuItemKind::Submenu(i) => i,
+      MenuItemKind::Predefined(i) => i,
+      MenuItemKind::Check(i) => i,
+      MenuItemKind::Icon(i) => i,
+    }
+  }
+
+  /// Casts this item to a [`MenuItem`], and returns `None` if it wasn't.
+  pub fn as_menuitem(&self) -> Option<&MenuItem<R>> {
+    match self {
+      MenuItemKind::MenuItem(i) => Some(i),
+      _ => None,
+    }
+  }
+
+  /// Casts this item to a [`MenuItem`], and panics if it wasn't.
+  pub fn as_menuitem_unchecked(&self) -> &MenuItem<R> {
+    match self {
+      MenuItemKind::MenuItem(i) => i,
+      _ => panic!("Not a MenuItem"),
+    }
+  }
+
+  /// Casts this item to a [`Submenu`], and returns `None` if it wasn't.
+  pub fn as_submenu(&self) -> Option<&Submenu<R>> {
+    match self {
+      MenuItemKind::Submenu(i) => Some(i),
+      _ => None,
+    }
+  }
+
+  /// Casts this item to a [`Submenu`], and panics if it wasn't.
+  pub fn as_submenu_unchecked(&self) -> &Submenu<R> {
+    match self {
+      MenuItemKind::Submenu(i) => i,
+      _ => panic!("Not a Submenu"),
+    }
+  }
+
+  /// Casts this item to a [`PredefinedMenuItem`], and returns `None` if it wasn't.
+  pub fn as_predefined_menuitem(&self) -> Option<&PredefinedMenuItem<R>> {
+    match self {
+      MenuItemKind::Predefined(i) => Some(i),
+      _ => None,
+    }
+  }
+
+  /// Casts this item to a [`PredefinedMenuItem`], and panics if it wasn't.
+  pub fn as_predefined_menuitem_unchecked(&self) -> &PredefinedMenuItem<R> {
+    match self {
+      MenuItemKind::Predefined(i) => i,
+      _ => panic!("Not a PredefinedMenuItem"),
+    }
+  }
+
+  /// Casts this item to a [`CheckMenuItem`], and returns `None` if it wasn't.
+  pub fn as_check_menuitem(&self) -> Option<&CheckMenuItem<R>> {
+    match self {
+      MenuItemKind::Check(i) => Some(i),
+      _ => None,
+    }
+  }
+
+  /// Casts this item to a [`CheckMenuItem`], and panics if it wasn't.
+  pub fn as_check_menuitem_unchecked(&self) -> &CheckMenuItem<R> {
+    match self {
+      MenuItemKind::Check(i) => i,
+      _ => panic!("Not a CheckMenuItem"),
+    }
+  }
+
+  /// Casts this item to a [`IconMenuItem`], and returns `None` if it wasn't.
+  pub fn as_icon_menuitem(&self) -> Option<&IconMenuItem<R>> {
+    match self {
+      MenuItemKind::Icon(i) => Some(i),
+      _ => None,
+    }
+  }
+
+  /// Casts this item to a [`IconMenuItem`], and panics if it wasn't.
+  pub fn as_icon_menuitem_unchecked(&self) -> &IconMenuItem<R> {
+    match self {
+      MenuItemKind::Icon(i) => i,
+      _ => panic!("Not an IconMenuItem"),
+    }
+  }
+}
+
+impl<R: Runtime> Clone for MenuItemKind<R> {
+  fn clone(&self) -> Self {
+    match self {
+      Self::MenuItem(i) => Self::MenuItem(i.clone()),
+      Self::Submenu(i) => Self::Submenu(i.clone()),
+      Self::Predefined(i) => Self::Predefined(i.clone()),
+      Self::Check(i) => Self::Check(i.clone()),
+      Self::Icon(i) => Self::Icon(i.clone()),
+    }
+  }
+}
+
+impl<R: Runtime> sealed::IsMenuItemBase for MenuItemKind<R> {
+  fn inner(&self) -> &dyn muda::IsMenuItem {
+    self.inner().inner()
+  }
+}
+
+impl<R: Runtime> IsMenuItem<R> for MenuItemKind<R> {
+  fn kind(&self) -> MenuItemKind<R> {
+    self.clone()
+  }
+
+  fn id(&self) -> &MenuId {
+    self.id()
+  }
+}
+
+/// A trait that defines a generic item in a menu, which may be one of [`MenuItemKind`]
+///
+/// # Safety
+///
+/// This trait is ONLY meant to be implemented internally by the crate.
+pub trait IsMenuItem<R: Runtime>: sealed::IsMenuItemBase {
+  /// Returns the kind of this menu item.
+  fn kind(&self) -> MenuItemKind<R>;
+
+  /// Returns a unique identifier associated with this menu.
+  fn id(&self) -> &MenuId;
+}
+
+/// A helper trait with methods to help creating a context menu.
+///
+/// # Safety
+///
+/// This trait is ONLY meant to be implemented internally by the crate.
+pub trait ContextMenu: sealed::ContextMenuBase + Send + Sync {
+  /// Popup this menu as a context menu on the specified window at the cursor position.
+  fn popup<R: crate::Runtime>(&self, window: crate::Window<R>) -> crate::Result<()>;
+
+  /// Popup this menu as a context menu on the specified window at the specified position.
+  ///
+  /// The position is relative to the window's top-left corner.
+  fn popup_at<R: crate::Runtime, P: Into<crate::Position>>(
+    &self,
+    window: crate::Window<R>,
+    position: P,
+  ) -> crate::Result<()>;
+}
+
+pub(crate) mod sealed {
+
+  pub trait IsMenuItemBase {
+    fn inner(&self) -> &dyn muda::IsMenuItem;
+  }
+
+  pub trait ContextMenuBase {
+    fn inner(&self) -> &dyn muda::ContextMenu;
+    fn inner_owned(&self) -> Box<dyn muda::ContextMenu>;
+    fn popup_inner<R: crate::Runtime, P: Into<crate::Position>>(
+      &self,
+      window: crate::Window<R>,
+      position: Option<P>,
+    ) -> crate::Result<()>;
+  }
+}
+
+impl TryFrom<crate::Icon> for muda::Icon {
+  type Error = crate::Error;
+
+  fn try_from(value: crate::Icon) -> Result<Self, Self::Error> {
+    let value: crate::runtime::Icon = value.try_into()?;
+    muda::Icon::from_rgba(value.rgba, value.width, value.height).map_err(Into::into)
+  }
+}
+
+pub(crate) fn into_logical_position<P: crate::Pixel>(
+  p: crate::LogicalPosition<P>,
+) -> muda::LogicalPosition<P> {
+  muda::LogicalPosition { x: p.x, y: p.y }
+}
+
+pub(crate) fn into_physical_position<P: crate::Pixel>(
+  p: crate::PhysicalPosition<P>,
+) -> muda::PhysicalPosition<P> {
+  muda::PhysicalPosition { x: p.x, y: p.y }
+}
+
+pub(crate) fn into_position(p: crate::Position) -> muda::Position {
+  match p {
+    crate::Position::Physical(p) => muda::Position::Physical(into_physical_position(p)),
+    crate::Position::Logical(p) => muda::Position::Logical(into_logical_position(p)),
+  }
+}

+ 134 - 0
core/tauri/src/menu/normal.rs

@@ -0,0 +1,134 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::{menu::MenuId, run_main_thread, AppHandle, Manager, Runtime};
+
+/// A menu item inside a [`Menu`] or [`Submenu`] and contains only text.
+///
+/// [`Menu`]: super::Menu
+/// [`Submenu`]: super::Submenu
+pub struct MenuItem<R: Runtime> {
+  pub(crate) id: MenuId,
+  pub(crate) inner: muda::MenuItem,
+  pub(crate) app_handle: AppHandle<R>,
+}
+
+impl<R: Runtime> Clone for MenuItem<R> {
+  fn clone(&self) -> Self {
+    Self {
+      id: self.id.clone(),
+      inner: self.inner.clone(),
+      app_handle: self.app_handle.clone(),
+    }
+  }
+}
+
+/// # Safety
+///
+/// We make sure it always runs on the main thread.
+unsafe impl<R: Runtime> Sync for MenuItem<R> {}
+unsafe impl<R: Runtime> Send for MenuItem<R> {}
+
+impl<R: Runtime> super::sealed::IsMenuItemBase for MenuItem<R> {
+  fn inner(&self) -> &dyn muda::IsMenuItem {
+    &self.inner
+  }
+}
+
+impl<R: Runtime> super::IsMenuItem<R> for MenuItem<R> {
+  fn kind(&self) -> super::MenuItemKind<R> {
+    super::MenuItemKind::MenuItem(self.clone())
+  }
+
+  fn id(&self) -> &MenuId {
+    &self.id
+  }
+}
+
+impl<R: Runtime> MenuItem<R> {
+  /// Create a new menu item.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn new<M: Manager<R>, S: AsRef<str>>(
+    manager: &M,
+    text: S,
+    enabled: bool,
+    acccelerator: Option<S>,
+  ) -> Self {
+    let item = muda::MenuItem::new(
+      text,
+      enabled,
+      acccelerator.and_then(|s| s.as_ref().parse().ok()),
+    );
+    Self {
+      id: item.id().clone(),
+      inner: item,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Create a new menu item with the specified id.
+  ///
+  /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn with_id<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
+    manager: &M,
+    id: I,
+    text: S,
+    enabled: bool,
+    acccelerator: Option<S>,
+  ) -> Self {
+    let item = muda::MenuItem::with_id(
+      id,
+      text,
+      enabled,
+      acccelerator.and_then(|s| s.as_ref().parse().ok()),
+    );
+    Self {
+      id: item.id().clone(),
+      inner: item,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// The application handle associated with this type.
+  pub fn app_handle(&self) -> &AppHandle<R> {
+    &self.app_handle
+  }
+
+  /// Returns a unique identifier associated with this menu item.
+  pub fn id(&self) -> &MenuId {
+    &self.id
+  }
+
+  /// Get the text for this menu item.
+  pub fn text(&self) -> crate::Result<String> {
+    run_main_thread!(self, |self_: Self| self_.inner.text())
+  }
+
+  /// Set the text for this menu item. `text` could optionally contain
+  /// an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn set_text<S: AsRef<str>>(&self, text: S) -> crate::Result<()> {
+    let text = text.as_ref().to_string();
+    run_main_thread!(self, |self_: Self| self_.inner.set_text(text))
+  }
+
+  /// Get whether this menu item is enabled or not.
+  pub fn is_enabled(&self) -> crate::Result<bool> {
+    run_main_thread!(self, |self_: Self| self_.inner.is_enabled())
+  }
+
+  /// Enable or disable this menu item.
+  pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
+    run_main_thread!(self, |self_: Self| self_.inner.set_enabled(enabled))
+  }
+
+  /// Set this menu item accelerator.
+  pub fn set_accelerator<S: AsRef<str>>(&self, acccelerator: Option<S>) -> crate::Result<()> {
+    let accel = acccelerator.and_then(|s| s.as_ref().parse().ok());
+    run_main_thread!(self, |self_: Self| self_.inner.set_accelerator(accel))?.map_err(Into::into)
+  }
+}

+ 287 - 0
core/tauri/src/menu/predefined.rs

@@ -0,0 +1,287 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use super::AboutMetadata;
+use crate::{menu::MenuId, run_main_thread, AppHandle, Manager, Runtime};
+
+/// A predefined (native) menu item which has a predfined behavior by the OS or by this crate.
+pub struct PredefinedMenuItem<R: Runtime> {
+  pub(crate) id: MenuId,
+  pub(crate) inner: muda::PredefinedMenuItem,
+  pub(crate) app_handle: AppHandle<R>,
+}
+
+impl<R: Runtime> Clone for PredefinedMenuItem<R> {
+  fn clone(&self) -> Self {
+    Self {
+      id: self.id.clone(),
+      inner: self.inner.clone(),
+      app_handle: self.app_handle.clone(),
+    }
+  }
+}
+
+/// # Safety
+///
+/// We make sure it always runs on the main thread.
+unsafe impl<R: Runtime> Sync for PredefinedMenuItem<R> {}
+unsafe impl<R: Runtime> Send for PredefinedMenuItem<R> {}
+
+impl<R: Runtime> super::sealed::IsMenuItemBase for PredefinedMenuItem<R> {
+  fn inner(&self) -> &dyn muda::IsMenuItem {
+    &self.inner
+  }
+}
+
+impl<R: Runtime> super::IsMenuItem<R> for PredefinedMenuItem<R> {
+  fn kind(&self) -> super::MenuItemKind<R> {
+    super::MenuItemKind::Predefined(self.clone())
+  }
+
+  fn id(&self) -> &MenuId {
+    self.id()
+  }
+}
+
+impl<R: Runtime> PredefinedMenuItem<R> {
+  /// Separator menu item
+  pub fn separator<M: Manager<R>>(manager: &M) -> Self {
+    let inner = muda::PredefinedMenuItem::separator();
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Copy menu item
+  pub fn copy<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::copy(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Cut menu item
+  pub fn cut<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::cut(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Paste menu item
+  pub fn paste<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::paste(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// SelectAll menu item
+  pub fn select_all<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::select_all(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Undo menu item
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn undo<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::undo(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+  /// Redo menu item
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn redo<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::redo(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Minimize window menu item
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn minimize<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::minimize(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Maximize window menu item
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn maximize<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::maximize(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Fullscreen menu item
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn fullscreen<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::fullscreen(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Hide window menu item
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn hide<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::hide(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Hide other windows menu item
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn hide_others<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::hide_others(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Show all app windows menu item
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn show_all<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::show_all(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Close window menu item
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn close_window<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::show_all(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Quit app menu item
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn quit<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::quit(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// About app menu item
+  pub fn about<M: Manager<R>>(
+    manager: &M,
+    text: Option<&str>,
+    metadata: Option<AboutMetadata>,
+  ) -> Self {
+    let inner = muda::PredefinedMenuItem::about(text, metadata);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Services menu item
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Windows / Linux:** Unsupported.
+  pub fn services<M: Manager<R>>(manager: &M, text: Option<&str>) -> Self {
+    let inner = muda::PredefinedMenuItem::services(text);
+    Self {
+      id: inner.id().clone(),
+      inner,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Returns a unique identifier associated with this menu item.
+  pub fn id(&self) -> &MenuId {
+    &self.id
+  }
+
+  /// Get the text for this menu item.
+  pub fn text(&self) -> crate::Result<String> {
+    run_main_thread!(self, |self_: Self| self_.inner.text())
+  }
+
+  /// Set the text for this menu item. `text` could optionally contain
+  /// an `&` before a character to assign this character as the mnemonic
+  /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn set_text<S: AsRef<str>>(&self, text: S) -> crate::Result<()> {
+    let text = text.as_ref().to_string();
+    run_main_thread!(self, |self_: Self| self_.inner.set_text(text))
+  }
+
+  /// The application handle associated with this type.
+  pub fn app_handle(&self) -> &AppHandle<R> {
+    &self.app_handle
+  }
+}

+ 328 - 0
core/tauri/src/menu/submenu.rs

@@ -0,0 +1,328 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use super::{sealed::ContextMenuBase, IsMenuItem, MenuItemKind};
+use crate::{run_main_thread, AppHandle, Manager, Position, Runtime, Window};
+use muda::{ContextMenu, MenuId};
+
+/// A type that is a submenu inside a [`Menu`] or [`Submenu`]
+///
+/// [`Menu`]: super::Menu
+/// [`Submenu`]: super::Submenu
+pub struct Submenu<R: Runtime> {
+  pub(crate) id: MenuId,
+  pub(crate) inner: muda::Submenu,
+  pub(crate) app_handle: AppHandle<R>,
+}
+
+/// # Safety
+///
+/// We make sure it always runs on the main thread.
+unsafe impl<R: Runtime> Sync for Submenu<R> {}
+unsafe impl<R: Runtime> Send for Submenu<R> {}
+
+impl<R: Runtime> Clone for Submenu<R> {
+  fn clone(&self) -> Self {
+    Self {
+      id: self.id.clone(),
+      inner: self.inner.clone(),
+      app_handle: self.app_handle.clone(),
+    }
+  }
+}
+
+impl<R: Runtime> super::sealed::IsMenuItemBase for Submenu<R> {
+  fn inner(&self) -> &dyn muda::IsMenuItem {
+    &self.inner
+  }
+}
+
+impl<R: Runtime> super::IsMenuItem<R> for Submenu<R> {
+  fn kind(&self) -> super::MenuItemKind<R> {
+    super::MenuItemKind::Submenu(self.clone())
+  }
+
+  fn id(&self) -> &MenuId {
+    &self.id
+  }
+}
+
+impl<R: Runtime> super::ContextMenu for Submenu<R> {
+  fn popup<T: Runtime>(&self, window: Window<T>) -> crate::Result<()> {
+    self.popup_inner(window, None::<Position>)
+  }
+
+  fn popup_at<T: Runtime, P: Into<Position>>(
+    &self,
+    window: Window<T>,
+    position: P,
+  ) -> crate::Result<()> {
+    self.popup_inner(window, Some(position))
+  }
+}
+
+impl<R: Runtime> ContextMenuBase for Submenu<R> {
+  fn popup_inner<T: Runtime, P: Into<crate::Position>>(
+    &self,
+    window: crate::Window<T>,
+    position: Option<P>,
+  ) -> crate::Result<()> {
+    let position = position.map(Into::into).map(super::into_position);
+    run_main_thread!(self, move |self_: Self| {
+      #[cfg(target_os = "macos")]
+      if let Ok(view) = window.ns_view() {
+        self_
+          .inner()
+          .show_context_menu_for_nsview(view as _, position);
+      }
+
+      #[cfg(any(
+        target_os = "linux",
+        target_os = "dragonfly",
+        target_os = "freebsd",
+        target_os = "netbsd",
+        target_os = "openbsd"
+      ))]
+      if let Ok(w) = window.gtk_window() {
+        self_.inner().show_context_menu_for_gtk_window(&w, position);
+      }
+
+      #[cfg(windows)]
+      if let Ok(hwnd) = window.hwnd() {
+        self_.inner().show_context_menu_for_hwnd(hwnd.0, position)
+      }
+    })
+  }
+
+  fn inner(&self) -> &dyn muda::ContextMenu {
+    &self.inner
+  }
+
+  fn inner_owned(&self) -> Box<dyn muda::ContextMenu> {
+    Box::new(self.clone().inner)
+  }
+}
+
+impl<R: Runtime> Submenu<R> {
+  /// Creates a new submenu.
+  pub fn new<M: Manager<R>, S: AsRef<str>>(manager: &M, text: S, enabled: bool) -> Self {
+    let submenu = muda::Submenu::new(text, enabled);
+    Self {
+      id: submenu.id().clone(),
+      inner: submenu,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Creates a new submenu with the specified id.
+  pub fn with_id<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
+    manager: &M,
+    id: I,
+    text: S,
+    enabled: bool,
+  ) -> Self {
+    let menu = muda::Submenu::with_id(id, text, enabled);
+    Self {
+      id: menu.id().clone(),
+      inner: menu,
+      app_handle: manager.app_handle().clone(),
+    }
+  }
+
+  /// Creates a new menu with given `items`. It calls [`Submenu::new`] and [`Submenu::append_items`] internally.
+  pub fn with_items<M: Manager<R>, S: AsRef<str>>(
+    manager: &M,
+    text: S,
+    enabled: bool,
+    items: &[&dyn IsMenuItem<R>],
+  ) -> crate::Result<Self> {
+    let menu = Self::new(manager, text, enabled);
+    menu.append_items(items)?;
+    Ok(menu)
+  }
+
+  /// Creates a new menu with the specified id and given `items`.
+  /// It calls [`Submenu::new`] and [`Submenu::append_items`] internally.
+  pub fn with_id_and_items<M: Manager<R>, I: Into<MenuId>, S: AsRef<str>>(
+    manager: &M,
+    id: I,
+    text: S,
+    enabled: bool,
+    items: &[&dyn IsMenuItem<R>],
+  ) -> crate::Result<Self> {
+    let menu = Self::with_id(manager, id, text, enabled);
+    menu.append_items(items)?;
+    Ok(menu)
+  }
+
+  pub(crate) fn inner(&self) -> &muda::Submenu {
+    &self.inner
+  }
+
+  /// The application handle associated with this type.
+  pub fn app_handle(&self) -> &AppHandle<R> {
+    &self.app_handle
+  }
+
+  /// Returns a unique identifier associated with this submenu.
+  pub fn id(&self) -> &MenuId {
+    &self.id
+  }
+
+  /// Add a menu item to the end of this submenu.
+  pub fn append(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
+    let kind = item.kind();
+    run_main_thread!(self, |self_: Self| self_.inner.append(kind.inner().inner()))?
+      .map_err(Into::into)
+  }
+
+  /// Add menu items to the end of this submenu. It calls [`Submenu::append`] in a loop internally.
+  pub fn append_items(&self, items: &[&dyn IsMenuItem<R>]) -> crate::Result<()> {
+    for item in items {
+      self.append(*item)?
+    }
+
+    Ok(())
+  }
+
+  /// Add a menu item to the beginning of this submenu.
+  pub fn prepend(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
+    let kind = item.kind();
+    run_main_thread!(self, |self_: Self| {
+      self_.inner.prepend(kind.inner().inner())
+    })?
+    .map_err(Into::into)
+  }
+
+  /// Add menu items to the beginning of this submenu. It calls [`Submenu::insert_items`] with position of `0` internally.
+  pub fn prepend_items(&self, items: &[&dyn IsMenuItem<R>]) -> crate::Result<()> {
+    self.insert_items(items, 0)
+  }
+
+  /// Insert a menu item at the specified `postion` in this submenu.
+  pub fn insert(&self, item: &dyn IsMenuItem<R>, position: usize) -> crate::Result<()> {
+    let kind = item.kind();
+    run_main_thread!(self, |self_: Self| {
+      self_.inner.insert(kind.inner().inner(), position)
+    })?
+    .map_err(Into::into)
+  }
+
+  /// Insert menu items at the specified `postion` in this submenu.
+  pub fn insert_items(&self, items: &[&dyn IsMenuItem<R>], position: usize) -> crate::Result<()> {
+    for (i, item) in items.iter().enumerate() {
+      self.insert(*item, position + i)?
+    }
+
+    Ok(())
+  }
+
+  /// Remove a menu item from this submenu.
+  pub fn remove(&self, item: &dyn IsMenuItem<R>) -> crate::Result<()> {
+    let kind = item.kind();
+    run_main_thread!(self, |self_: Self| self_.inner.remove(kind.inner().inner()))?
+      .map_err(Into::into)
+  }
+
+  /// Returns a list of menu items that has been added to this submenu.
+  pub fn items(&self) -> crate::Result<Vec<MenuItemKind<R>>> {
+    let handle = self.app_handle.clone();
+    run_main_thread!(self, |self_: Self| {
+      self_
+        .inner
+        .items()
+        .into_iter()
+        .map(|i| match i {
+          muda::MenuItemKind::MenuItem(i) => super::MenuItemKind::MenuItem(super::MenuItem {
+            id: i.id().clone(),
+            inner: i,
+            app_handle: handle.clone(),
+          }),
+          muda::MenuItemKind::Submenu(i) => super::MenuItemKind::Submenu(super::Submenu {
+            id: i.id().clone(),
+            inner: i,
+            app_handle: handle.clone(),
+          }),
+          muda::MenuItemKind::Predefined(i) => {
+            super::MenuItemKind::Predefined(super::PredefinedMenuItem {
+              id: i.id().clone(),
+              inner: i,
+              app_handle: handle.clone(),
+            })
+          }
+          muda::MenuItemKind::Check(i) => super::MenuItemKind::Check(super::CheckMenuItem {
+            id: i.id().clone(),
+            inner: i,
+            app_handle: handle.clone(),
+          }),
+          muda::MenuItemKind::Icon(i) => super::MenuItemKind::Icon(super::IconMenuItem {
+            id: i.id().clone(),
+            inner: i,
+            app_handle: handle.clone(),
+          }),
+        })
+        .collect::<Vec<_>>()
+    })
+  }
+
+  /// Get the text for this submenu.
+  pub fn text(&self) -> crate::Result<String> {
+    run_main_thread!(self, |self_: Self| self_.inner.text())
+  }
+
+  /// Set the text for this submenu. `text` could optionally contain
+  /// an `&` before a character to assign this character as the mnemonic
+  /// for this submenu. To display a `&` without assigning a mnemenonic, use `&&`.
+  pub fn set_text<S: AsRef<str>>(&self, text: S) -> crate::Result<()> {
+    let text = text.as_ref().to_string();
+    run_main_thread!(self, |self_: Self| self_.inner.set_text(text))
+  }
+
+  /// Get whether this submenu is enabled or not.
+  pub fn is_enabled(&self) -> crate::Result<bool> {
+    run_main_thread!(self, |self_: Self| self_.inner.is_enabled())
+  }
+
+  /// Enable or disable this submenu.
+  pub fn set_enabled(&self, enabled: bool) -> crate::Result<()> {
+    run_main_thread!(self, |self_: Self| self_.inner.set_enabled(enabled))
+  }
+
+  /// Set this submenu as the Window menu for the application on macOS.
+  ///
+  /// This will cause macOS to automatically add window-switching items and
+  /// certain other items to the menu.
+  #[cfg(target_os = "macos")]
+  pub fn set_as_windows_menu_for_nsapp(&self) -> crate::Result<()> {
+    run_main_thread!(self, |self_: Self| self_
+      .inner
+      .set_as_windows_menu_for_nsapp())?;
+    Ok(())
+  }
+
+  /// Set this submenu as the Help menu for the application on macOS.
+  ///
+  /// This will cause macOS to automatically add a search box to the menu.
+  ///
+  /// If no menu is set as the Help menu, macOS will automatically use any menu
+  /// which has a title matching the localized word "Help".
+  pub fn set_as_help_menu_for_nsapp(&self) -> crate::Result<()> {
+    #[cfg(target_os = "macos")]
+    run_main_thread!(self, |self_: Self| self_.inner.set_as_help_menu_for_nsapp())?;
+    Ok(())
+  }
+
+  /// Retrieves the menu item matching the given identifier.
+  pub fn get<'a, I>(&self, id: &'a I) -> Option<MenuItemKind<R>>
+  where
+    I: ?Sized,
+    MenuId: PartialEq<&'a I>,
+  {
+    self
+      .items()
+      .unwrap_or_default()
+      .into_iter()
+      .find(|i| i.id() == &id)
+  }
+}

+ 7 - 7
core/tauri/src/path/desktop.rs

@@ -194,7 +194,7 @@ impl<R: Runtime> PathResolver<R> {
 
   /// Returns the path to the suggested directory for your app's config files.
   ///
-  /// Resolves to [`config_dir`]`/${bundle_identifier}`.
+  /// Resolves to [`config_dir`](self.config_dir)`/${bundle_identifier}`.
   pub fn app_config_dir(&self) -> Result<PathBuf> {
     dirs_next::config_dir()
       .ok_or(Error::UnknownPath)
@@ -203,7 +203,7 @@ impl<R: Runtime> PathResolver<R> {
 
   /// Returns the path to the suggested directory for your app's data files.
   ///
-  /// Resolves to [`data_dir`]`/${bundle_identifier}`.
+  /// Resolves to [`data_dir`](self.data_dir)`/${bundle_identifier}`.
   pub fn app_data_dir(&self) -> Result<PathBuf> {
     dirs_next::data_dir()
       .ok_or(Error::UnknownPath)
@@ -212,7 +212,7 @@ impl<R: Runtime> PathResolver<R> {
 
   /// Returns the path to the suggested directory for your app's local data files.
   ///
-  /// Resolves to [`local_data_dir`]`/${bundle_identifier}`.
+  /// Resolves to [`local_data_dir`](self.local_data_dir)`/${bundle_identifier}`.
   pub fn app_local_data_dir(&self) -> Result<PathBuf> {
     dirs_next::data_local_dir()
       .ok_or(Error::UnknownPath)
@@ -221,7 +221,7 @@ impl<R: Runtime> PathResolver<R> {
 
   /// Returns the path to the suggested directory for your app's cache files.
   ///
-  /// Resolves to [`cache_dir`]`/${bundle_identifier}`.
+  /// Resolves to [`cache_dir`](self.cache_dir)`/${bundle_identifier}`.
   pub fn app_cache_dir(&self) -> Result<PathBuf> {
     dirs_next::cache_dir()
       .ok_or(Error::UnknownPath)
@@ -232,9 +232,9 @@ impl<R: Runtime> PathResolver<R> {
   ///
   /// ## Platform-specific
   ///
-  /// - **Linux:** Resolves to [`data_local_dir`]`/${bundle_identifier}/logs`.
-  /// - **macOS:** Resolves to [`home_dir`]`/Library/Logs/${bundle_identifier}`
-  /// - **Windows:** Resolves to [`data_local_dir`]`/${bundle_identifier}/logs`.
+  /// - **Linux:** Resolves to [`data_local_dir`](self.data_local_dir)`/${bundle_identifier}/logs`.
+  /// - **macOS:** Resolves to [`home_dir`](self.home_dir)`/Library/Logs/${bundle_identifier}`
+  /// - **Windows:** Resolves to [`data_local_dir`](self.data_local_dir)`/${bundle_identifier}/logs`.
   pub fn app_log_dir(&self) -> Result<PathBuf> {
     #[cfg(target_os = "macos")]
     let path = dirs_next::home_dir().ok_or(Error::UnknownPath).map(|dir| {

+ 1 - 1
core/tauri/src/path/mod.rs

@@ -65,7 +65,7 @@ impl<'de> Deserialize<'de> for SafePathBuf {
   }
 }
 
-/// A base directory to be used in [`resolve_directory`].
+/// A base directory for a path.
 ///
 /// The base directory is the optional root of a file system operation.
 /// If informed by the API call, all paths will be relative to the path of the given directory.

+ 37 - 31
core/tauri/src/plugin/mobile.rs

@@ -60,29 +60,29 @@ pub(crate) fn register_channel(channel: Channel) {
 /// Glue between Rust and the Kotlin code that sends the plugin response back.
 #[cfg(target_os = "android")]
 pub fn handle_android_plugin_response(
-  env: jni::JNIEnv<'_>,
+  env: &mut jni::JNIEnv<'_>,
   id: i32,
   success: jni::objects::JString<'_>,
   error: jni::objects::JString<'_>,
 ) {
   let (payload, is_ok): (serde_json::Value, bool) = match (
     env
-      .is_same_object(success, jni::objects::JObject::default())
+      .is_same_object(&success, jni::objects::JObject::default())
       .unwrap_or_default(),
     env
-      .is_same_object(error, jni::objects::JObject::default())
+      .is_same_object(&error, jni::objects::JObject::default())
       .unwrap_or_default(),
   ) {
     // both null
     (true, true) => (serde_json::Value::Null, true),
     // error null
     (false, true) => (
-      serde_json::from_str(env.get_string(success).unwrap().to_str().unwrap()).unwrap(),
+      serde_json::from_str(env.get_string(&success).unwrap().to_str().unwrap()).unwrap(),
       true,
     ),
     // success null
     (true, false) => (
-      serde_json::from_str(env.get_string(error).unwrap().to_str().unwrap()).unwrap(),
+      serde_json::from_str(env.get_string(&error).unwrap().to_str().unwrap()).unwrap(),
       false,
     ),
     // both are set - impossible in the Kotlin code
@@ -102,12 +102,12 @@ pub fn handle_android_plugin_response(
 /// Glue between Rust and the Kotlin code that sends the channel data.
 #[cfg(target_os = "android")]
 pub fn send_channel_data(
-  env: jni::JNIEnv<'_>,
+  env: &mut jni::JNIEnv<'_>,
   channel_id: i64,
   data_str: jni::objects::JString<'_>,
 ) {
   let data: serde_json::Value =
-    serde_json::from_str(env.get_string(data_str).unwrap().to_str().unwrap()).unwrap();
+    serde_json::from_str(env.get_string(&data_str).unwrap().to_str().unwrap()).unwrap();
 
   if let Some(channel) = CHANNELS
     .get_or_init(Default::default)
@@ -196,24 +196,15 @@ impl<R: Runtime, C: DeserializeOwned> PluginApi<R, C> {
   ) -> Result<PluginHandle<R>, PluginInvokeError> {
     use jni::{errors::Error as JniError, objects::JObject, JNIEnv};
 
-    fn initialize_plugin<'a, R: Runtime>(
-      env: JNIEnv<'a>,
-      activity: JObject<'a>,
-      webview: JObject<'a>,
+    fn initialize_plugin<R: Runtime>(
+      env: &mut JNIEnv<'_>,
+      activity: &JObject<'_>,
+      webview: &JObject<'_>,
       runtime_handle: &R::Handle,
       plugin_name: &'static str,
       plugin_class: String,
       plugin_config: &serde_json::Value,
     ) -> Result<(), JniError> {
-      let plugin_manager = env
-        .call_method(
-          activity,
-          "getPluginManager",
-          "()Lapp/tauri/plugin/PluginManager;",
-          &[],
-        )?
-        .l()?;
-
       // instantiate plugin
       let plugin_class = runtime_handle.find_class(env, activity, plugin_class)?;
       let plugin = env.new_object(
@@ -223,15 +214,28 @@ impl<R: Runtime, C: DeserializeOwned> PluginApi<R, C> {
       )?;
 
       // load plugin
+
+      let plugin_manager = env
+        .call_method(
+          activity,
+          "getPluginManager",
+          "()Lapp/tauri/plugin/PluginManager;",
+          &[],
+        )?
+        .l()?;
+
+      let plugin_name = env.new_string(plugin_name)?;
+      let config =
+        crate::jni_helpers::to_jsobject::<R>(env, activity, &runtime_handle, plugin_config)?;
       env.call_method(
         plugin_manager,
         "load",
         "(Landroid/webkit/WebView;Ljava/lang/String;Lapp/tauri/plugin/Plugin;Lapp/tauri/plugin/JSObject;)V",
         &[
           webview.into(),
-          env.new_string(plugin_name)?.into(),
-          plugin.into(),
-          crate::jni_helpers::to_jsobject::<R>(env, activity, runtime_handle, plugin_config)?
+          (&plugin_name).into(),
+          (&plugin).into(),
+          config.borrow()
         ],
       )?;
 
@@ -393,11 +397,13 @@ pub(crate) fn run_command<
     plugin: &str,
     command: String,
     payload: &serde_json::Value,
-    runtime_handle: &R::Handle,
-    env: JNIEnv<'_>,
-    activity: JObject<'_>,
+    runtime_handle: R::Handle,
+    env: &mut JNIEnv<'_>,
+    activity: &JObject<'_>,
   ) -> Result<(), JniError> {
-    let data = crate::jni_helpers::to_jsobject::<R>(env, activity, runtime_handle, payload)?;
+    let plugin = env.new_string(plugin)?;
+    let command = env.new_string(&command)?;
+    let data = crate::jni_helpers::to_jsobject::<R>(env, activity, &runtime_handle, payload)?;
     let plugin_manager = env
       .call_method(
         activity,
@@ -413,9 +419,9 @@ pub(crate) fn run_command<
       "(ILjava/lang/String;Ljava/lang/String;Lapp/tauri/plugin/JSObject;)V",
       &[
         id.into(),
-        env.new_string(plugin)?.into(),
-        env.new_string(&command)?.into(),
-        data,
+        (&plugin).into(),
+        (&command).into(),
+        data.borrow(),
       ],
     )?;
 
@@ -440,7 +446,7 @@ pub(crate) fn run_command<
     .insert(id, Box::new(handler.clone()));
 
   handle.run_on_android_context(move |env, activity, _webview| {
-    if let Err(e) = run::<R>(id, &plugin_name, command, &payload, &handle_, env, activity) {
+    if let Err(e) = run::<R>(id, &plugin_name, command, &payload, handle_, env, activity) {
       handler(Err(e.to_string().into()));
     }
   });

+ 28 - 114
core/tauri/src/test/mock_runtime.rs

@@ -6,21 +6,16 @@
 #![allow(missing_docs)]
 
 use tauri_runtime::{
-  menu::{Menu, MenuUpdate},
   monitor::Monitor,
   webview::{WindowBuilder, WindowBuilderBase},
   window::{
     dpi::{PhysicalPosition, PhysicalSize, Position, Size},
-    CursorIcon, DetachedWindow, MenuEvent, PendingWindow, WindowEvent,
+    CursorIcon, DetachedWindow, PendingWindow, RawWindow, WindowEvent,
   },
   DeviceEventFilter, Dispatch, Error, EventLoopProxy, ExitRequestedEventAction, Icon, Result,
-  RunEvent, Runtime, RuntimeHandle, UserAttentionType, UserEvent,
-};
-#[cfg(all(desktop, feature = "system-tray"))]
-use tauri_runtime::{
-  menu::{SystemTrayMenu, TrayHandle},
-  SystemTray, SystemTrayEvent, TrayId,
+  RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, UserEvent,
 };
+
 #[cfg(target_os = "macos")]
 use tauri_utils::TitleBarStyle;
 use tauri_utils::{config::WindowConfig, Theme};
@@ -105,9 +100,10 @@ impl<T: UserEvent> RuntimeHandle<T> for MockRuntimeHandle {
   }
 
   /// Create a new webview window.
-  fn create_window(
+  fn create_window<F: Fn(RawWindow<'_>) + Send + 'static>(
     &self,
     pending: PendingWindow<T, Self::Runtime>,
+    _before_webview_creation: Option<F>,
   ) -> Result<DetachedWindow<T, Self::Runtime>> {
     let id = rand::random();
     self.context.windows.borrow_mut().insert(id, Window);
@@ -119,7 +115,6 @@ impl<T: UserEvent> RuntimeHandle<T> for MockRuntimeHandle {
         last_evaluated_script: Default::default(),
         url: Arc::new(Mutex::new(pending.url)),
       },
-      menu_ids: Default::default(),
     })
   }
 
@@ -128,17 +123,6 @@ impl<T: UserEvent> RuntimeHandle<T> for MockRuntimeHandle {
     self.context.send_message(Message::Task(Box::new(f)))
   }
 
-  #[cfg(all(desktop, feature = "system-tray"))]
-  #[cfg_attr(doc_cfg, doc(cfg(all(desktop, feature = "system-tray"))))]
-  fn system_tray(
-    &self,
-    system_tray: SystemTray,
-  ) -> Result<<Self::Runtime as Runtime<T>>::TrayHandler> {
-    Ok(MockTrayHandler {
-      context: self.context.clone(),
-    })
-  }
-
   fn raw_display_handle(&self) -> raw_window_handle::RawDisplayHandle {
     #[cfg(target_os = "linux")]
     return raw_window_handle::RawDisplayHandle::Xlib(raw_window_handle::XlibDisplayHandle::empty());
@@ -177,8 +161,8 @@ impl<T: UserEvent> RuntimeHandle<T> for MockRuntimeHandle {
   #[cfg(target_os = "android")]
   fn find_class<'a>(
     &'a self,
-    env: jni::JNIEnv<'a>,
-    activity: jni::objects::JObject<'a>,
+    env: &'a mut jni::JNIEnv<'a>,
+    activity: &'a jni::objects::JObject<'a>,
     name: impl Into<String>,
   ) -> std::result::Result<jni::objects::JClass<'a>, jni::errors::Error> {
     todo!()
@@ -187,9 +171,7 @@ impl<T: UserEvent> RuntimeHandle<T> for MockRuntimeHandle {
   #[cfg(target_os = "android")]
   fn run_on_android_context<F>(&self, f: F)
   where
-    F: FnOnce(jni::JNIEnv<'_>, jni::objects::JObject<'_>, jni::objects::JObject<'_>)
-      + Send
-      + 'static,
+    F: FnOnce(&mut jni::JNIEnv, &jni::objects::JObject, &jni::objects::JObject) + Send + 'static,
   {
     todo!()
   }
@@ -223,10 +205,6 @@ impl WindowBuilder for MockWindowBuilder {
     Self {}
   }
 
-  fn menu(self, menu: Menu) -> Self {
-    self
-  }
-
   fn center(self) -> Self {
     self
   }
@@ -357,10 +335,6 @@ impl WindowBuilder for MockWindowBuilder {
   fn has_icon(&self) -> bool {
     false
   }
-
-  fn get_menu(&self) -> Option<&Menu> {
-    None
-  }
 }
 
 impl<T: UserEvent> Dispatch<T> for MockDispatcher {
@@ -376,10 +350,6 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
     Uuid::new_v4()
   }
 
-  fn on_menu_event<F: Fn(&MenuEvent) + Send + 'static>(&self, f: F) -> Uuid {
-    Uuid::new_v4()
-  }
-
   fn with_webview<F: FnOnce(Box<dyn std::any::Any>) + Send + 'static>(&self, f: F) -> Result<()> {
     Ok(())
   }
@@ -474,10 +444,6 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
     Ok(String::new())
   }
 
-  fn is_menu_visible(&self) -> Result<bool> {
-    Ok(true)
-  }
-
   fn current_monitor(&self) -> Result<Option<Monitor>> {
     Ok(None)
   }
@@ -505,6 +471,17 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
     unimplemented!()
   }
 
+  #[cfg(any(
+    target_os = "linux",
+    target_os = "dragonfly",
+    target_os = "freebsd",
+    target_os = "netbsd",
+    target_os = "openbsd"
+  ))]
+  fn default_vbox(&self) -> Result<gtk::Box> {
+    unimplemented!()
+  }
+
   fn raw_window_handle(&self) -> Result<raw_window_handle::RawWindowHandle> {
     #[cfg(target_os = "linux")]
     return Ok(raw_window_handle::RawWindowHandle::Xlib(
@@ -534,9 +511,10 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
     Ok(())
   }
 
-  fn create_window(
+  fn create_window<F: Fn(RawWindow<'_>) + Send + 'static>(
     &mut self,
     pending: PendingWindow<T, Self::Runtime>,
+    _before_webview_creation: Option<F>,
   ) -> Result<DetachedWindow<T, Self::Runtime>> {
     let id = rand::random();
     self.context.windows.borrow_mut().insert(id, Window);
@@ -548,7 +526,6 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
         last_evaluated_script: Default::default(),
         url: Arc::new(Mutex::new(pending.url)),
       },
-      menu_ids: Default::default(),
     })
   }
 
@@ -593,14 +570,6 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
     Ok(())
   }
 
-  fn show_menu(&self) -> Result<()> {
-    Ok(())
-  }
-
-  fn hide_menu(&self) -> Result<()> {
-    Ok(())
-  }
-
   fn show(&self) -> Result<()> {
     Ok(())
   }
@@ -698,46 +667,6 @@ impl<T: UserEvent> Dispatch<T> for MockDispatcher {
       .replace(script.into());
     Ok(())
   }
-
-  fn update_menu_item(&self, id: u16, update: MenuUpdate) -> Result<()> {
-    Ok(())
-  }
-}
-
-#[cfg(all(desktop, feature = "system-tray"))]
-#[derive(Debug, Clone)]
-pub struct MockTrayHandler {
-  context: RuntimeContext,
-}
-
-#[cfg(all(desktop, feature = "system-tray"))]
-impl TrayHandle for MockTrayHandler {
-  fn set_icon(&self, icon: Icon) -> Result<()> {
-    Ok(())
-  }
-  fn set_menu(&self, menu: SystemTrayMenu) -> Result<()> {
-    Ok(())
-  }
-  fn update_item(&self, id: u16, update: MenuUpdate) -> Result<()> {
-    Ok(())
-  }
-  #[cfg(target_os = "macos")]
-  fn set_icon_as_template(&self, is_template: bool) -> Result<()> {
-    Ok(())
-  }
-
-  #[cfg(target_os = "macos")]
-  fn set_title(&self, title: &str) -> tauri_runtime::Result<()> {
-    Ok(())
-  }
-
-  fn set_tooltip(&self, tooltip: &str) -> Result<()> {
-    Ok(())
-  }
-
-  fn destroy(&self) -> Result<()> {
-    Ok(())
-  }
 }
 
 #[derive(Debug, Clone)]
@@ -753,8 +682,6 @@ impl<T: UserEvent> EventLoopProxy<T> for EventProxy {
 pub struct MockRuntime {
   is_running: Arc<AtomicBool>,
   pub context: RuntimeContext,
-  #[cfg(all(desktop, feature = "system-tray"))]
-  tray_handler: MockTrayHandler,
   run_rx: Receiver<Message>,
 }
 
@@ -770,10 +697,6 @@ impl MockRuntime {
     };
     Self {
       is_running,
-      #[cfg(all(desktop, feature = "system-tray"))]
-      tray_handler: MockTrayHandler {
-        context: context.clone(),
-      },
       context,
       run_rx: rx,
     }
@@ -783,16 +706,14 @@ impl MockRuntime {
 impl<T: UserEvent> Runtime<T> for MockRuntime {
   type Dispatcher = MockDispatcher;
   type Handle = MockRuntimeHandle;
-  #[cfg(all(desktop, feature = "system-tray"))]
-  type TrayHandler = MockTrayHandler;
   type EventLoopProxy = EventProxy;
 
-  fn new() -> Result<Self> {
+  fn new(_args: RuntimeInitArgs) -> Result<Self> {
     Ok(Self::init())
   }
 
   #[cfg(any(windows, target_os = "linux"))]
-  fn new_any_thread() -> Result<Self> {
+  fn new_any_thread(_args: RuntimeInitArgs) -> Result<Self> {
     Ok(Self::init())
   }
 
@@ -806,7 +727,11 @@ impl<T: UserEvent> Runtime<T> for MockRuntime {
     }
   }
 
-  fn create_window(&self, pending: PendingWindow<T, Self>) -> Result<DetachedWindow<T, Self>> {
+  fn create_window<F: Fn(RawWindow<'_>) + Send + 'static>(
+    &self,
+    pending: PendingWindow<T, Self>,
+    _before_webview_creation: Option<F>,
+  ) -> Result<DetachedWindow<T, Self>> {
     let id = rand::random();
     self.context.windows.borrow_mut().insert(id, Window);
     Ok(DetachedWindow {
@@ -817,20 +742,9 @@ impl<T: UserEvent> Runtime<T> for MockRuntime {
         last_evaluated_script: Default::default(),
         url: Arc::new(Mutex::new(pending.url)),
       },
-      menu_ids: Default::default(),
     })
   }
 
-  #[cfg(all(desktop, feature = "system-tray"))]
-  #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-  fn system_tray(&self, system_tray: SystemTray) -> Result<Self::TrayHandler> {
-    Ok(self.tray_handler.clone())
-  }
-
-  #[cfg(all(desktop, feature = "system-tray"))]
-  #[cfg_attr(doc_cfg, doc(cfg(feature = "system-tray")))]
-  fn on_system_tray_event<F: Fn(TrayId, &SystemTrayEvent) + Send + 'static>(&mut self, f: F) {}
-
   fn primary_monitor(&self) -> Option<Monitor> {
     unimplemented!()
   }

+ 4 - 4
core/tauri/src/test/mod.rs

@@ -124,7 +124,7 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
         windows: Vec::new(),
         bundle: Default::default(),
         security: Default::default(),
-        system_tray: None,
+        tray_icon: None,
         macos_private_api: false,
       },
       build: Default::default(),
@@ -133,8 +133,8 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
     assets: Arc::new(assets),
     default_window_icon: None,
     app_icon: None,
-    #[cfg(desktop)]
-    system_tray_icon: None,
+    #[cfg(all(desktop, feature = "tray-icon"))]
+    tray_icon: None,
     package_info: crate::PackageInfo {
       name: "test".into(),
       version: "0.1.0".parse().unwrap(),
@@ -163,7 +163,7 @@ pub fn mock_context<A: Assets>(assets: A) -> crate::Context<A> {
 /// }
 /// ```
 pub fn mock_builder() -> Builder<MockRuntime> {
-  Builder::<MockRuntime>::new()
+  Builder::<MockRuntime>::new().enable_macos_default_menu(false)
 }
 
 /// Creates a new [`App`] for testing using the [`mock_context`] with a [`noop_assets`].

+ 351 - 0
core/tauri/src/tray.rs

@@ -0,0 +1,351 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#![cfg(all(desktop, feature = "tray-icon"))]
+
+//! Tray icon types and utility functions
+
+use crate::app::{GlobalMenuEventListener, GlobalTrayIconEventListener};
+use crate::menu::ContextMenu;
+use crate::menu::MenuEvent;
+use crate::{run_main_thread, AppHandle, Icon, Manager, Runtime};
+use std::path::Path;
+pub use tray_icon::{ClickType, Rectangle, TrayIconEvent, TrayIconId};
+
+// TODO(muda-migration): figure out js events
+
+/// [`TrayIcon`] builder struct and associated methods.
+#[derive(Default)]
+pub struct TrayIconBuilder<R: Runtime> {
+  on_menu_event: Option<GlobalMenuEventListener<AppHandle<R>>>,
+  on_tray_event: Option<GlobalTrayIconEventListener<TrayIcon<R>>>,
+  inner: tray_icon::TrayIconBuilder,
+}
+
+impl<R: Runtime> TrayIconBuilder<R> {
+  /// Creates a new tray icon builder.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
+  /// Setting an empty [`Menu`](crate::menu::Menu) is enough.
+  pub fn new() -> Self {
+    Self {
+      inner: tray_icon::TrayIconBuilder::new(),
+      on_menu_event: None,
+      on_tray_event: None,
+    }
+  }
+
+  /// Creates a new tray icon builder with the specified id.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
+  /// Setting an empty [`Menu`](crate::menu::Menu) is enough.
+  pub fn with_id<I: Into<TrayIconId>>(id: I) -> Self {
+    let mut builder = Self::new();
+    builder.inner = builder.inner.with_id(id);
+    builder
+  }
+
+  /// Set the a menu for this tray icon.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux**: once a menu is set, it cannot be removed or replaced but you can change its content.
+  pub fn menu<M: ContextMenu>(mut self, menu: &M) -> Self {
+    self.inner = self.inner.with_menu(menu.inner_owned());
+    self
+  }
+
+  /// Set an icon for this tray icon.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
+  /// Setting an empty [`Menu`](crate::menu::Menu) is enough.
+  pub fn icon(mut self, icon: Icon) -> Self {
+    let icon = icon.try_into().ok();
+    if let Some(icon) = icon {
+      self.inner = self.inner.with_icon(icon);
+    }
+    self
+  }
+
+  /// Set a tooltip for this tray icon.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported.
+  pub fn tooltip<S: AsRef<str>>(mut self, s: S) -> Self {
+    self.inner = self.inner.with_tooltip(s);
+    self
+  }
+
+  /// Set the tray icon title.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Linux:** The title will not be shown unless there is an icon
+  /// as well.  The title is useful for numerical and other frequently
+  /// updated information.  In general, it shouldn't be shown unless a
+  /// user requests it as it can take up a significant amount of space
+  /// on the user's panel.  This may not be shown in all visualizations.
+  /// - **Windows:** Unsupported.
+  pub fn title<S: AsRef<str>>(mut self, title: S) -> Self {
+    self.inner = self.inner.with_title(title);
+    self
+  }
+
+  /// Set tray icon temp dir path. **Linux only**.
+  ///
+  /// On Linux, we need to write the icon to the disk and usually it will
+  /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
+  pub fn temp_dir_path<P: AsRef<Path>>(mut self, s: P) -> Self {
+    self.inner = self.inner.with_temp_dir_path(s);
+    self
+  }
+
+  /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
+  pub fn icon_as_template(mut self, is_template: bool) -> Self {
+    self.inner = self.inner.with_icon_as_template(is_template);
+    self
+  }
+
+  /// Whether to show the tray menu on left click or not, default is `true`. **macOS only**.
+  pub fn menu_on_left_click(mut self, enable: bool) -> Self {
+    self.inner = self.inner.with_menu_on_left_click(enable);
+    self
+  }
+
+  /// Set a handler for menu events.
+  ///
+  /// Note that this handler is called for any menu event,
+  /// whether it is coming from this window, another window or from the tray icon menu.
+  pub fn on_menu_event<F: Fn(&AppHandle<R>, MenuEvent) + Sync + Send + 'static>(
+    mut self,
+    f: F,
+  ) -> Self {
+    self.on_menu_event.replace(Box::new(f));
+    self
+  }
+
+  /// Set a handler for this tray icon events.
+  pub fn on_tray_event<F: Fn(&TrayIcon<R>, TrayIconEvent) + Sync + Send + 'static>(
+    mut self,
+    f: F,
+  ) -> Self {
+    self.on_tray_event.replace(Box::new(f));
+    self
+  }
+
+  /// Access the unique id that will be assigned to the tray icon
+  /// this builder will create.
+  pub fn id(&self) -> &TrayIconId {
+    self.inner.id()
+  }
+
+  /// Builds and adds a new [`TrayIcon`] to the system tray.
+  pub fn build<M: Manager<R>>(self, manager: &M) -> crate::Result<TrayIcon<R>> {
+    let id = self.id().clone();
+    let inner = self.inner.build()?;
+    let icon = TrayIcon {
+      id,
+      inner,
+      app_handle: manager.app_handle().clone(),
+    };
+
+    icon.register(&icon.app_handle, self.on_menu_event, self.on_tray_event);
+
+    Ok(icon)
+  }
+}
+
+/// Tray icon struct and associated methods.
+///
+/// This type is reference-counted and the icon is removed when the last instance is dropped.
+///
+/// See [TrayIconBuilder] to construct this type.
+pub struct TrayIcon<R: Runtime> {
+  id: TrayIconId,
+  inner: tray_icon::TrayIcon,
+  app_handle: AppHandle<R>,
+}
+
+impl<R: Runtime> Clone for TrayIcon<R> {
+  fn clone(&self) -> Self {
+    Self {
+      id: self.id.clone(),
+      inner: self.inner.clone(),
+      app_handle: self.app_handle.clone(),
+    }
+  }
+}
+
+/// # Safety
+///
+/// We make sure it always runs on the main thread.
+unsafe impl<R: Runtime> Sync for TrayIcon<R> {}
+unsafe impl<R: Runtime> Send for TrayIcon<R> {}
+
+impl<R: Runtime> TrayIcon<R> {
+  fn register(
+    &self,
+    app_handle: &AppHandle<R>,
+    on_menu_event: Option<GlobalMenuEventListener<AppHandle<R>>>,
+    on_tray_event: Option<GlobalTrayIconEventListener<TrayIcon<R>>>,
+  ) {
+    if let Some(handler) = on_menu_event {
+      app_handle
+        .manager
+        .inner
+        .menu_event_listeners
+        .lock()
+        .unwrap()
+        .push(handler);
+    }
+
+    if let Some(handler) = on_tray_event {
+      app_handle
+        .manager
+        .inner
+        .tray_event_listeners
+        .lock()
+        .unwrap()
+        .insert(self.id.clone(), handler);
+    }
+
+    app_handle
+      .manager
+      .inner
+      .tray_icons
+      .lock()
+      .unwrap()
+      .push(self.clone());
+  }
+
+  /// The application handle associated with this type.
+  pub fn app_handle(&self) -> &AppHandle<R> {
+    &self.app_handle
+  }
+
+  /// Register a handler for menu events.
+  ///
+  /// Note that this handler is called for any menu event,
+  /// whether it is coming from this window, another window or from the tray icon menu.
+  pub fn on_menu_event<F: Fn(&AppHandle<R>, MenuEvent) + Sync + Send + 'static>(&self, f: F) {
+    self
+      .app_handle
+      .manager
+      .inner
+      .menu_event_listeners
+      .lock()
+      .unwrap()
+      .push(Box::new(f));
+  }
+
+  /// Register a handler for this tray icon events.
+  pub fn on_tray_event<F: Fn(&TrayIcon<R>, TrayIconEvent) + Sync + Send + 'static>(&self, f: F) {
+    self
+      .app_handle
+      .manager
+      .inner
+      .tray_event_listeners
+      .lock()
+      .unwrap()
+      .insert(self.id.clone(), Box::new(f));
+  }
+
+  /// Returns the id associated with this tray icon.
+  pub fn id(&self) -> &TrayIconId {
+    &self.id
+  }
+
+  /// Set new tray icon. If `None` is provided, it will remove the icon.
+  pub fn set_icon(&self, icon: Option<Icon>) -> crate::Result<()> {
+    let icon = icon.and_then(|i| i.try_into().ok());
+    run_main_thread!(self, |self_: Self| self_.inner.set_icon(icon))?.map_err(Into::into)
+  }
+
+  /// Set new tray menu.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux**: once a menu is set it cannot be removed so `None` has no effect
+  pub fn set_menu<M: ContextMenu + 'static>(&self, menu: Option<M>) -> crate::Result<()> {
+    run_main_thread!(self, |self_: Self| self_
+      .inner
+      .set_menu(menu.map(|m| m.inner_owned())))
+  }
+
+  /// Sets the tooltip for this tray icon.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** Unsupported
+  pub fn set_tooltip<S: AsRef<str>>(&self, tooltip: Option<S>) -> crate::Result<()> {
+    let s = tooltip.map(|s| s.as_ref().to_string());
+    run_main_thread!(self, |self_: Self| self_.inner.set_tooltip(s))?.map_err(Into::into)
+  }
+
+  /// Sets the tooltip for this tray icon.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **Linux:** The title will not be shown unless there is an icon
+  /// as well.  The title is useful for numerical and other frequently
+  /// updated information.  In general, it shouldn't be shown unless a
+  /// user requests it as it can take up a significant amount of space
+  /// on the user's panel.  This may not be shown in all visualizations.
+  /// - **Windows:** Unsupported
+  pub fn set_title<S: AsRef<str>>(&self, title: Option<S>) -> crate::Result<()> {
+    let s = title.map(|s| s.as_ref().to_string());
+    run_main_thread!(self, |self_: Self| self_.inner.set_title(s))
+  }
+
+  /// Show or hide this tray icon
+  pub fn set_visible(&self, visible: bool) -> crate::Result<()> {
+    run_main_thread!(self, |self_: Self| self_.inner.set_visible(visible))?.map_err(Into::into)
+  }
+
+  /// Sets the tray icon temp dir path. **Linux only**.
+  ///
+  /// On Linux, we need to write the icon to the disk and usually it will
+  /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
+  pub fn set_temp_dir_path<P: AsRef<Path>>(&self, path: Option<P>) -> crate::Result<()> {
+    #[allow(unused)]
+    let p = path.map(|p| p.as_ref().to_path_buf());
+    #[cfg(target_os = "linux")]
+    run_main_thread!(self, |self_: Self| self_.inner.set_temp_dir_path(p))?;
+    Ok(())
+  }
+
+  /// Set the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
+  pub fn set_icon_as_template(&self, #[allow(unused)] is_template: bool) -> crate::Result<()> {
+    #[cfg(target_os = "macos")]
+    run_main_thread!(self, |self_: Self| self_
+      .inner
+      .set_icon_as_template(is_template))?;
+    Ok(())
+  }
+
+  /// Disable or enable showing the tray menu on left click. **macOS only**.
+  pub fn set_show_menu_on_left_click(&self, #[allow(unused)] enable: bool) -> crate::Result<()> {
+    #[cfg(target_os = "macos")]
+    run_main_thread!(self, |self_: Self| self_
+      .inner
+      .set_show_menu_on_left_click(enable))?;
+    Ok(())
+  }
+}
+
+impl TryFrom<Icon> for tray_icon::Icon {
+  type Error = crate::Error;
+
+  fn try_from(value: Icon) -> Result<Self, Self::Error> {
+    let value: crate::runtime::Icon = value.try_into()?;
+    tray_icon::Icon::from_rgba(value.rgba, value.width, value.height).map_err(Into::into)
+  }
+}

+ 417 - 39
core/tauri/src/window.rs

@@ -4,10 +4,7 @@
 
 //! The Tauri window types and functions.
 
-pub(crate) mod menu;
-
 use http::HeaderMap;
-pub use menu::{MenuEvent, MenuHandle};
 pub use tauri_utils::{config::Color, WindowEffect as Effect, WindowEffectState as EffectState};
 use url::Url;
 
@@ -38,8 +35,8 @@ use crate::{
 };
 #[cfg(desktop)]
 use crate::{
+  menu::{ContextMenu, Menu, MenuId},
   runtime::{
-    menu::Menu,
     window::dpi::{Position, Size},
     UserAttentionType,
   },
@@ -125,9 +122,13 @@ pub struct WindowBuilder<'a, R: Runtime> {
   app_handle: AppHandle<R>,
   label: String,
   pub(crate) window_builder: <R::Dispatcher as Dispatch<EventLoopMessage>>::WindowBuilder,
+  #[cfg(desktop)]
+  pub(crate) menu: Option<Menu<R>>,
   pub(crate) webview_attributes: WebviewAttributes,
   web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
   navigation_handler: Option<Box<NavigationHandler>>,
+  #[cfg(desktop)]
+  on_menu_event: Option<crate::app::GlobalMenuEventListener<Window<R>>>,
 }
 
 impl<'a, R: Runtime> fmt::Debug for WindowBuilder<'a, R> {
@@ -168,7 +169,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
   /// ```
   /// tauri::Builder::default()
   ///   .setup(|app| {
-  ///     let handle = app.handle();
+  ///     let handle = app.handle().clone();
   ///     std::thread::spawn(move || {
   ///       let window = tauri::WindowBuilder::new(&handle, "label", tauri::WindowUrl::App("index.html".into()))
   ///         .build()
@@ -192,16 +193,20 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
   /// [the Webview2 issue]: https://github.com/tauri-apps/wry/issues/583
   pub fn new<M: Manager<R>, L: Into<String>>(manager: &'a M, label: L, url: WindowUrl) -> Self {
     let runtime = manager.runtime();
-    let app_handle = manager.app_handle();
+    let app_handle = manager.app_handle().clone();
     Self {
       manager: manager.manager().clone(),
       runtime,
       app_handle,
       label: label.into(),
       window_builder: <R::Dispatcher as Dispatch<EventLoopMessage>>::WindowBuilder::new(),
+      #[cfg(desktop)]
+      menu: None,
       webview_attributes: WebviewAttributes::new(url),
       web_resource_request_handler: None,
       navigation_handler: None,
+      #[cfg(desktop)]
+      on_menu_event: None,
     }
   }
 
@@ -232,14 +237,18 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     let builder = Self {
       manager: manager.manager().clone(),
       runtime: manager.runtime(),
-      app_handle: manager.app_handle(),
+      app_handle: manager.app_handle().clone(),
       label: config.label.clone(),
       webview_attributes: WebviewAttributes::from(&config),
       window_builder: <R::Dispatcher as Dispatch<EventLoopMessage>>::WindowBuilder::with_config(
         config,
       ),
       web_resource_request_handler: None,
+      #[cfg(desktop)]
+      menu: None,
       navigation_handler: None,
+      #[cfg(desktop)]
+      on_menu_event: None,
     };
 
     builder
@@ -317,6 +326,48 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     self
   }
 
+  /// Registers a global menu event listener.
+  ///
+  /// Note that this handler is called for any menu event,
+  /// whether it is coming from this window, another window or from the tray icon menu.
+  ///
+  /// Also note that this handler will not be called if
+  /// the window used to register it was closed.
+  ///
+  /// # Examples
+  /// ```
+  /// use tauri::menu::{Menu, Submenu, MenuItem};
+  /// tauri::Builder::default()
+  ///   .setup(|app| {
+  ///     let handle = app.handle();
+  ///     let save_menu_item = MenuItem::new(handle, "Save", true, None);
+  ///     let menu = Menu::with_items(handle, &[
+  ///       &Submenu::with_items(handle, "File", true, &[
+  ///         &save_menu_item,
+  ///       ])?,
+  ///     ])?;
+  ///     let window = tauri::WindowBuilder::new(app, "editor", tauri::WindowUrl::default())
+  ///       .menu(menu)
+  ///       .on_menu_event(move |window, event| {
+  ///         if event.id == save_menu_item.id() {
+  ///           // save menu item
+  ///         }
+  ///       })
+  ///       .build()
+  ///       .unwrap();
+  ///
+  ///     Ok(())
+  ///   });
+  /// ```
+  #[cfg(desktop)]
+  pub fn on_menu_event<F: Fn(&Window<R>, crate::menu::MenuEvent) + Send + Sync + 'static>(
+    mut self,
+    f: F,
+  ) -> Self {
+    self.on_menu_event.replace(Box::new(f));
+    self
+  }
+
   /// Creates a new webview window.
   pub fn build(mut self) -> crate::Result<Window<R>> {
     let mut pending = PendingWindow::new(
@@ -331,13 +382,43 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
     let pending = self
       .manager
       .prepare_window(self.app_handle.clone(), pending, &labels)?;
+
+    #[cfg(desktop)]
+    let window_menu = {
+      let is_app_wide = self.menu.is_none();
+      self
+        .menu
+        .or_else(|| self.app_handle.menu())
+        .map(|menu| WindowMenu { is_app_wide, menu })
+    };
+
+    #[cfg(desktop)]
+    let handler = self
+      .manager
+      .prepare_window_menu_creation_handler(window_menu.as_ref());
+    #[cfg(not(desktop))]
+    #[allow(clippy::type_complexity)]
+    let handler: Option<Box<dyn Fn(tauri_runtime::window::RawWindow<'_>) + Send>> = None;
+
     let window_effects = pending.webview_attributes.window_effects.clone();
     let window = match &mut self.runtime {
-      RuntimeOrDispatch::Runtime(runtime) => runtime.create_window(pending),
-      RuntimeOrDispatch::RuntimeHandle(handle) => handle.create_window(pending),
-      RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_window(pending),
+      RuntimeOrDispatch::Runtime(runtime) => runtime.create_window(pending, handler),
+      RuntimeOrDispatch::RuntimeHandle(handle) => handle.create_window(pending, handler),
+      RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_window(pending, handler),
+    }
+    .map(|window| {
+      self.manager.attach_window(
+        self.app_handle.clone(),
+        window,
+        #[cfg(desktop)]
+        window_menu,
+      )
+    })?;
+
+    #[cfg(desktop)]
+    if let Some(handler) = self.on_menu_event {
+      window.on_menu_event(handler);
     }
-    .map(|window| self.manager.attach_window(self.app_handle.clone(), window))?;
 
     if let Some(effects) = window_effects {
       crate::vibrancy::set_window_effects(&window, Some(effects))?;
@@ -365,8 +446,8 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
 impl<'a, R: Runtime> WindowBuilder<'a, R> {
   /// Sets the menu for the window.
   #[must_use]
-  pub fn menu(mut self, menu: Menu) -> Self {
-    self.window_builder = self.window_builder.menu(menu);
+  pub fn menu(mut self, menu: Menu<R>) -> Self {
+    self.menu.replace(menu);
     self
   }
 
@@ -659,7 +740,7 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> {
   ///
   /// ## Platform-specific:
   ///
-  /// - **Windows**: If using decorations or shadows, you may want to try this workaround https://github.com/tauri-apps/tao/issues/72#issuecomment-975607891
+  /// - **Windows**: If using decorations or shadows, you may want to try this workaround <https://github.com/tauri-apps/tao/issues/72#issuecomment-975607891>
   /// - **Linux**: Unsupported
   pub fn effects(mut self, effects: WindowEffectsConfig) -> Self {
     self.webview_attributes = self.webview_attributes.window_effects(effects);
@@ -793,13 +874,20 @@ pub struct InvokeRequest {
   pub headers: HeaderMap,
 }
 
+/// A wrapper struct to hold the window menu state
+/// and whether it is global per-app or specific to this window.
+#[cfg(desktop)]
+pub(crate) struct WindowMenu<R: Runtime> {
+  pub(crate) is_app_wide: bool,
+  pub(crate) menu: Menu<R>,
+}
+
 // TODO: expand these docs since this is a pretty important type
 /// A webview window managed by Tauri.
 ///
 /// This type also implements [`Manager`] which allows you to manage other windows attached to
 /// the same application.
 #[default_runtime(crate::Wry, wry)]
-#[derive(Debug)]
 pub struct Window<R: Runtime> {
   /// The webview window created by the runtime.
   pub(crate) window: DetachedWindow<EventLoopMessage, R>,
@@ -807,6 +895,20 @@ pub struct Window<R: Runtime> {
   manager: WindowManager<R>,
   pub(crate) app_handle: AppHandle<R>,
   js_event_listeners: Arc<Mutex<HashMap<JsEventListenerKey, HashSet<usize>>>>,
+  // The menu set for this window
+  #[cfg(desktop)]
+  pub(crate) menu: Arc<Mutex<Option<WindowMenu<R>>>>,
+}
+
+impl<R: Runtime> std::fmt::Debug for Window<R> {
+  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    f.debug_struct("Window")
+      .field("window", &self.window)
+      .field("manager", &self.manager)
+      .field("app_handle", &self.app_handle)
+      .field("js_event_listeners", &self.js_event_listeners)
+      .finish()
+  }
 }
 
 unsafe impl<R: Runtime> raw_window_handle::HasRawWindowHandle for Window<R> {
@@ -822,6 +924,8 @@ impl<R: Runtime> Clone for Window<R> {
       manager: self.manager.clone(),
       app_handle: self.app_handle.clone(),
       js_event_listeners: self.js_event_listeners.clone(),
+      #[cfg(desktop)]
+      menu: self.menu.clone(),
     }
   }
 }
@@ -868,8 +972,8 @@ impl<R: Runtime> ManagerBase<R> for Window<R> {
     RuntimeOrDispatch::Dispatch(self.dispatcher())
   }
 
-  fn managed_app_handle(&self) -> AppHandle<R> {
-    self.app_handle.clone()
+  fn managed_app_handle(&self) -> &AppHandle<R> {
+    &self.app_handle
   }
 }
 
@@ -968,12 +1072,15 @@ impl<R: Runtime> Window<R> {
     manager: WindowManager<R>,
     window: DetachedWindow<EventLoopMessage, R>,
     app_handle: AppHandle<R>,
+    #[cfg(desktop)] menu: Option<WindowMenu<R>>,
   ) -> Self {
     Self {
       window,
       manager,
       app_handle,
       js_event_listeners: Default::default(),
+      #[cfg(desktop)]
+      menu: Arc::new(Mutex::new(menu)),
     }
   }
 
@@ -1015,20 +1122,6 @@ impl<R: Runtime> Window<R> {
       .on_window_event(move |event| f(&event.clone().into()));
   }
 
-  /// Registers a menu event listener.
-  pub fn on_menu_event<F: Fn(MenuEvent) + Send + 'static>(&self, f: F) -> uuid::Uuid {
-    let menu_ids = self.window.menu_ids.clone();
-    self.window.dispatcher.on_menu_event(move |event| {
-      let id = menu_ids
-        .lock()
-        .unwrap()
-        .get(&event.menu_item_id)
-        .unwrap()
-        .clone();
-      f(MenuEvent { menu_item_id: id })
-    })
-  }
-
   /// Executes a closure, providing it with the webview handle that is specific to the current platform.
   ///
   /// The closure is executed on the main thread.
@@ -1094,16 +1187,270 @@ impl<R: Runtime> Window<R> {
   }
 }
 
-/// Window getters.
+/// Menu APIs
+#[cfg(desktop)]
 impl<R: Runtime> Window<R> {
-  /// Gets a handle to the window menu.
-  pub fn menu_handle(&self) -> MenuHandle<R> {
-    MenuHandle {
-      ids: self.window.menu_ids.clone(),
-      dispatcher: self.dispatcher(),
+  /// Registers a global menu event listener.
+  ///
+  /// Note that this handler is called for any menu event,
+  /// whether it is coming from this window, another window or from the tray icon menu.
+  ///
+  /// Also note that this handler will not be called if
+  /// the window used to register it was closed.
+  ///
+  /// # Examples
+  /// ```
+  /// use tauri::menu::{Menu, Submenu, MenuItem};
+  /// tauri::Builder::default()
+  ///   .setup(|app| {
+  ///     let handle = app.handle();
+  ///     let save_menu_item = MenuItem::new(handle, "Save", true, None);
+  ///     let menu = Menu::with_items(handle, &[
+  ///       &Submenu::with_items(handle, "File", true, &[
+  ///         &save_menu_item,
+  ///       ])?,
+  ///     ])?;
+  ///     let window = tauri::WindowBuilder::new(app, "editor", tauri::WindowUrl::default())
+  ///       .menu(menu)
+  ///       .build()
+  ///       .unwrap();
+  ///
+  ///     window.on_menu_event(move |window, event| {
+  ///       if event.id == save_menu_item.id() {
+  ///           // save menu item
+  ///       }
+  ///     });
+  ///
+  ///     Ok(())
+  ///   });
+  /// ```
+  pub fn on_menu_event<F: Fn(&Window<R>, crate::menu::MenuEvent) + Send + Sync + 'static>(
+    &self,
+    f: F,
+  ) {
+    self
+      .manager
+      .inner
+      .window_menu_event_listeners
+      .lock()
+      .unwrap()
+      .insert(self.label().to_string(), Box::new(f));
+  }
+
+  pub(crate) fn menu_lock(&self) -> std::sync::MutexGuard<'_, Option<WindowMenu<R>>> {
+    self.menu.lock().expect("poisoned window")
+  }
+
+  #[cfg_attr(target_os = "macos", allow(dead_code))]
+  pub(crate) fn has_app_wide_menu(&self) -> bool {
+    self
+      .menu_lock()
+      .as_ref()
+      .map(|m| m.is_app_wide)
+      .unwrap_or(false)
+  }
+
+  #[cfg_attr(target_os = "macos", allow(dead_code))]
+  pub(crate) fn is_menu_in_use<I: PartialEq<MenuId>>(&self, id: &I) -> bool {
+    self
+      .menu_lock()
+      .as_ref()
+      .map(|m| id.eq(m.menu.id()))
+      .unwrap_or(false)
+  }
+
+  /// Returns this window menu .
+  pub fn menu(&self) -> Option<Menu<R>> {
+    self.menu_lock().as_ref().map(|m| m.menu.clone())
+  }
+
+  /// Sets the window menu and returns the previous one.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one
+  /// window, if you need to set it, use [`AppHandle::set_menu`] instead.
+  #[cfg_attr(target_os = "macos", allow(unused_variables))]
+  pub fn set_menu(&self, menu: Menu<R>) -> crate::Result<Option<Menu<R>>> {
+    let prev_menu = self.remove_menu()?;
+
+    self.manager.insert_menu_into_stash(&menu);
+
+    let window = self.clone();
+    let menu_ = menu.clone();
+    self.run_on_main_thread(move || {
+      #[cfg(windows)]
+      if let Ok(hwnd) = window.hwnd() {
+        let _ = menu_.inner().init_for_hwnd(hwnd.0);
+      }
+      #[cfg(any(
+        target_os = "linux",
+        target_os = "dragonfly",
+        target_os = "freebsd",
+        target_os = "netbsd",
+        target_os = "openbsd"
+      ))]
+      if let (Ok(gtk_window), Ok(gtk_box)) = (window.gtk_window(), window.default_vbox()) {
+        let _ = menu_
+          .inner()
+          .init_for_gtk_window(&gtk_window, Some(&gtk_box));
+      }
+    })?;
+
+    self.menu_lock().replace(WindowMenu {
+      is_app_wide: false,
+      menu,
+    });
+
+    Ok(prev_menu)
+  }
+
+  /// Removes the window menu and returns it.
+  ///
+  /// ## Platform-specific:
+  ///
+  /// - **macOS:** Unsupported. The menu on macOS is app-wide and not specific to one
+  /// window, if you need to remove it, use [`AppHandle::remove_menu`] instead.
+  pub fn remove_menu(&self) -> crate::Result<Option<Menu<R>>> {
+    let current_menu = self.menu_lock().as_ref().map(|m| m.menu.clone());
+
+    // remove from the window
+    #[cfg_attr(target_os = "macos", allow(unused_variables))]
+    if let Some(menu) = current_menu {
+      let window = self.clone();
+      self.run_on_main_thread(move || {
+        #[cfg(windows)]
+        if let Ok(hwnd) = window.hwnd() {
+          let _ = menu.inner().remove_for_hwnd(hwnd.0);
+        }
+        #[cfg(any(
+          target_os = "linux",
+          target_os = "dragonfly",
+          target_os = "freebsd",
+          target_os = "netbsd",
+          target_os = "openbsd"
+        ))]
+        if let Ok(gtk_window) = window.gtk_window() {
+          let _ = menu.inner().remove_for_gtk_window(&gtk_window);
+        }
+      })?;
     }
+
+    let prev_menu = self.menu_lock().take().map(|m| m.menu);
+
+    self
+      .manager
+      .remove_menu_from_stash_by_id(prev_menu.as_ref().map(|m| m.id()));
+
+    Ok(prev_menu)
+  }
+
+  /// Hides the window menu.
+  pub fn hide_menu(&self) -> crate::Result<()> {
+    // remove from the window
+    #[cfg_attr(target_os = "macos", allow(unused_variables))]
+    if let Some(window_menu) = &*self.menu_lock() {
+      let window = self.clone();
+      let menu_ = window_menu.menu.clone();
+      self.run_on_main_thread(move || {
+        #[cfg(windows)]
+        if let Ok(hwnd) = window.hwnd() {
+          let _ = menu_.inner().hide_for_hwnd(hwnd.0);
+        }
+        #[cfg(any(
+          target_os = "linux",
+          target_os = "dragonfly",
+          target_os = "freebsd",
+          target_os = "netbsd",
+          target_os = "openbsd"
+        ))]
+        if let Ok(gtk_window) = window.gtk_window() {
+          let _ = menu_.inner().hide_for_gtk_window(&gtk_window);
+        }
+      })?;
+    }
+
+    Ok(())
   }
 
+  /// Shows the window menu.
+  pub fn show_menu(&self) -> crate::Result<()> {
+    // remove from the window
+    #[cfg_attr(target_os = "macos", allow(unused_variables))]
+    if let Some(window_menu) = &*self.menu_lock() {
+      let window = self.clone();
+      let menu_ = window_menu.menu.clone();
+      self.run_on_main_thread(move || {
+        #[cfg(windows)]
+        if let Ok(hwnd) = window.hwnd() {
+          let _ = menu_.inner().show_for_hwnd(hwnd.0);
+        }
+        #[cfg(any(
+          target_os = "linux",
+          target_os = "dragonfly",
+          target_os = "freebsd",
+          target_os = "netbsd",
+          target_os = "openbsd"
+        ))]
+        if let Ok(gtk_window) = window.gtk_window() {
+          let _ = menu_.inner().show_for_gtk_window(&gtk_window);
+        }
+      })?;
+    }
+
+    Ok(())
+  }
+
+  /// Shows the window menu.
+  pub fn is_menu_visible(&self) -> crate::Result<bool> {
+    // remove from the window
+    #[cfg_attr(target_os = "macos", allow(unused_variables))]
+    if let Some(window_menu) = &*self.menu_lock() {
+      let (tx, rx) = std::sync::mpsc::channel();
+      let window = self.clone();
+      let menu_ = window_menu.menu.clone();
+      self.run_on_main_thread(move || {
+        #[cfg(windows)]
+        if let Ok(hwnd) = window.hwnd() {
+          let _ = tx.send(menu_.inner().is_visible_on_hwnd(hwnd.0));
+        }
+        #[cfg(any(
+          target_os = "linux",
+          target_os = "dragonfly",
+          target_os = "freebsd",
+          target_os = "netbsd",
+          target_os = "openbsd"
+        ))]
+        if let Ok(gtk_window) = window.gtk_window() {
+          let _ = tx.send(menu_.inner().is_visible_on_gtk_window(&gtk_window));
+        }
+      })?;
+
+      return Ok(rx.recv().unwrap_or(false));
+    }
+
+    Ok(false)
+  }
+
+  /// Shows the specified menu as a context menu at the cursor position.
+  pub fn popup_menu<M: ContextMenu>(&self, menu: &M) -> crate::Result<()> {
+    menu.popup(self.clone())
+  }
+
+  /// Shows the specified menu as a context menu at the specified position.
+  ///
+  /// The position is relative to the window's top-left corner.
+  pub fn popup_menu_at<M: ContextMenu, P: Into<Position>>(
+    &self,
+    menu: &M,
+    position: P,
+  ) -> crate::Result<()> {
+    menu.popup_at(self.clone(), position)
+  }
+}
+
+/// Window getters.
+impl<R: Runtime> Window<R> {
   /// 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)
@@ -1251,6 +1598,23 @@ impl<R: Runtime> Window<R> {
       })
   }
 
+  /// Returns the pointer to the content view of this window.
+  #[cfg(target_os = "macos")]
+  pub fn ns_view(&self) -> crate::Result<*mut std::ffi::c_void> {
+    self
+      .window
+      .dispatcher
+      .raw_window_handle()
+      .map_err(Into::into)
+      .and_then(|handle| {
+        if let raw_window_handle::RawWindowHandle::AppKit(h) = handle {
+          Ok(h.ns_view)
+        } else {
+          Err(crate::Error::InvalidWindowHandle)
+        }
+      })
+  }
+
   /// Returns the native handle that is used by this window.
   #[cfg(windows)]
   pub fn hwnd(&self) -> crate::Result<HWND> {
@@ -1270,7 +1634,7 @@ impl<R: Runtime> Window<R> {
 
   /// Returns the `ApplicationWindow` from gtk crate that is used by this window.
   ///
-  /// Note that this can only be used on the main thread.
+  /// Note that this type can only be used on the main thread.
   #[cfg(any(
     target_os = "linux",
     target_os = "dragonfly",
@@ -1282,6 +1646,20 @@ impl<R: Runtime> Window<R> {
     self.window.dispatcher.gtk_window().map_err(Into::into)
   }
 
+  /// Returns the vertical [`gtk::Box`] that is added by default as the sole child of this window.
+  ///
+  /// Note that this type can only be used on the main thread.
+  #[cfg(any(
+    target_os = "linux",
+    target_os = "dragonfly",
+    target_os = "freebsd",
+    target_os = "netbsd",
+    target_os = "openbsd"
+  ))]
+  pub fn default_vbox(&self) -> crate::Result<gtk::Box> {
+    self.window.dispatcher.default_vbox().map_err(Into::into)
+  }
+
   /// Returns the current window theme.
   ///
   /// ## Platform-specific
@@ -1486,7 +1864,7 @@ impl<R: Runtime> Window<R> {
   ///
   /// ## Platform-specific:
   ///
-  /// - **Windows**: If using decorations or shadows, you may want to try this workaround https://github.com/tauri-apps/tao/issues/72#issuecomment-975607891
+  /// - **Windows**: If using decorations or shadows, you may want to try this workaround <https://github.com/tauri-apps/tao/issues/72#issuecomment-975607891>
   /// - **Linux**: Unsupported
   pub fn set_effects<E: Into<Option<WindowEffectsConfig>>>(&self, effects: E) -> crate::Result<()> {
     let effects = effects.into();

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

@@ -1,155 +0,0 @@
-// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-License-Identifier: MIT
-
-use crate::{
-  runtime::{
-    menu::{MenuHash, MenuId, MenuIdRef, MenuUpdate},
-    Dispatch,
-  },
-  Runtime,
-};
-
-use tauri_macros::default_runtime;
-
-use std::{
-  collections::HashMap,
-  sync::{Arc, Mutex},
-};
-
-/// The window menu event.
-#[derive(Debug, Clone)]
-pub struct MenuEvent {
-  pub(crate) menu_item_id: MenuId,
-}
-
-impl MenuEvent {
-  /// The menu item id.
-  pub fn menu_item_id(&self) -> MenuIdRef<'_> {
-    &self.menu_item_id
-  }
-}
-
-/// A handle to a system tray. Allows updating the context menu items.
-#[default_runtime(crate::Wry, wry)]
-#[derive(Debug)]
-pub struct MenuHandle<R: Runtime> {
-  pub(crate) ids: Arc<Mutex<HashMap<MenuHash, MenuId>>>,
-  pub(crate) dispatcher: R::Dispatcher,
-}
-
-impl<R: Runtime> Clone for MenuHandle<R> {
-  fn clone(&self) -> Self {
-    Self {
-      ids: self.ids.clone(),
-      dispatcher: self.dispatcher.clone(),
-    }
-  }
-}
-
-/// A handle to a system tray menu item.
-#[default_runtime(crate::Wry, wry)]
-#[derive(Debug)]
-pub struct MenuItemHandle<R: Runtime> {
-  id: u16,
-  dispatcher: R::Dispatcher,
-}
-
-impl<R: Runtime> Clone for MenuItemHandle<R> {
-  fn clone(&self) -> Self {
-    Self {
-      id: self.id,
-      dispatcher: self.dispatcher.clone(),
-    }
-  }
-}
-
-impl<R: Runtime> MenuHandle<R> {
-  /// Gets a handle to the menu item that has the specified `id`.
-  pub fn get_item(&self, id: MenuIdRef<'_>) -> MenuItemHandle<R> {
-    let ids = self.ids.lock().unwrap();
-    let iter = ids.iter();
-    for (raw, item_id) in iter {
-      if item_id == id {
-        return MenuItemHandle {
-          id: *raw,
-          dispatcher: self.dispatcher.clone(),
-        };
-      }
-    }
-    panic!("item id not found")
-  }
-
-  /// Attempts to get a handle to the menu item that has the specified `id`, return an error if `id` is not found.
-  pub fn try_get_item(&self, id: MenuIdRef<'_>) -> Option<MenuItemHandle<R>> {
-    self
-      .ids
-      .lock()
-      .unwrap()
-      .iter()
-      .find(|i| i.1 == id)
-      .map(|i| MenuItemHandle {
-        id: *i.0,
-        dispatcher: self.dispatcher.clone(),
-      })
-  }
-
-  /// Shows the menu.
-  pub fn show(&self) -> crate::Result<()> {
-    self.dispatcher.show_menu().map_err(Into::into)
-  }
-
-  /// Hides the menu.
-  pub fn hide(&self) -> crate::Result<()> {
-    self.dispatcher.hide_menu().map_err(Into::into)
-  }
-
-  /// Whether the menu is visible or not.
-  pub fn is_visible(&self) -> crate::Result<bool> {
-    self.dispatcher.is_menu_visible().map_err(Into::into)
-  }
-
-  /// Toggles the menu visibility.
-  pub fn toggle(&self) -> crate::Result<()> {
-    if self.is_visible()? {
-      self.hide()
-    } else {
-      self.show()
-    }
-  }
-}
-
-impl<R: Runtime> MenuItemHandle<R> {
-  /// 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)
-  }
-}

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
examples/api/dist/assets/index.css


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
examples/api/dist/assets/index.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 247 - 211
examples/api/src-tauri/Cargo.lock


+ 1 - 1
examples/api/src-tauri/Cargo.toml

@@ -40,7 +40,7 @@ features = [
   "icon-png",
   "isolation",
   "macos-private-api",
-  "system-tray"
+  "tray-icon"
 ]
 
 [dev-dependencies.tauri]

+ 34 - 0
examples/api/src-tauri/src/cmd.rs

@@ -29,3 +29,37 @@ pub fn perform_request(endpoint: String, body: RequestBody) -> ApiResponse {
     message: "message response".into(),
   }
 }
+
+#[cfg(all(desktop, not(target_os = "macos")))]
+#[command]
+pub fn toggle_menu<R: tauri::Runtime>(window: tauri::Window<R>) {
+  if window.is_menu_visible().unwrap_or_default() {
+    let _ = window.hide_menu();
+  } else {
+    let _ = window.show_menu();
+  }
+}
+
+#[cfg(target_os = "macos")]
+#[command]
+pub fn toggle_menu<R: tauri::Runtime>(
+  app: tauri::AppHandle<R>,
+  app_menu: tauri::State<'_, crate::AppMenu<R>>,
+) {
+  if let Some(menu) = app.remove_menu().unwrap() {
+    app_menu.0.lock().unwrap().replace(menu);
+  } else {
+    app
+      .set_menu(app_menu.0.lock().unwrap().clone().expect("no app menu"))
+      .unwrap();
+  }
+}
+
+#[cfg(desktop)]
+#[command]
+pub fn popup_context_menu<R: tauri::Runtime>(
+  window: tauri::Window<R>,
+  popup_menu: tauri::State<'_, crate::PopupMenu<R>>,
+) {
+  window.popup_menu(&popup_menu.0).unwrap();
+}

+ 33 - 12
examples/api/src-tauri/src/lib.rs

@@ -15,13 +15,22 @@ use serde::Serialize;
 use tauri::{ipc::Channel, window::WindowBuilder, App, AppHandle, RunEvent, Runtime, WindowUrl};
 use tauri_plugin_sample::{PingRequest, SampleExt};
 
+#[cfg(desktop)]
+use tauri::Manager;
+
+pub type SetupHook = Box<dyn FnOnce(&mut App) -> Result<(), Box<dyn std::error::Error>> + Send>;
+pub type OnEvent = Box<dyn FnMut(&AppHandle, RunEvent)>;
+
 #[derive(Clone, Serialize)]
 struct Reply {
   data: String,
 }
 
-pub type SetupHook = Box<dyn FnOnce(&mut App) -> Result<(), Box<dyn std::error::Error>> + Send>;
-pub type OnEvent = Box<dyn FnMut(&AppHandle, RunEvent)>;
+#[cfg(target_os = "macos")]
+pub struct AppMenu<R: Runtime>(pub std::sync::Mutex<Option<tauri::menu::Menu<R>>>);
+
+#[cfg(desktop)]
+pub struct PopupMenu<R: Runtime>(tauri::menu::Menu<R>);
 
 #[cfg_attr(mobile, tauri::mobile_entry_point)]
 pub fn run() {
@@ -43,11 +52,23 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
     .setup(move |app| {
       #[cfg(desktop)]
       {
-        tray::create_tray(app)?;
-
-        app.handle().plugin(tauri_plugin_cli::init())?;
+        let handle = app.handle();
+        tray::create_tray(&handle)?;
+        handle.plugin(tauri_plugin_cli::init())?;
       }
 
+      #[cfg(target_os = "macos")]
+      app.manage(AppMenu::<R>(Default::default()));
+
+      #[cfg(desktop)]
+      app.manage(PopupMenu(
+        tauri::menu::MenuBuilder::new(app)
+          .check("Tauri is awesome!")
+          .text("Do something")
+          .copy()
+          .build()?,
+      ));
+
       let mut window_builder = WindowBuilder::new(app, "main", WindowUrl::default());
       #[cfg(desktop)]
       {
@@ -55,7 +76,8 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
           .title("Tauri API Validation")
           .inner_size(1000., 800.)
           .min_inner_size(600., 400.)
-          .content_protected(true);
+          .content_protected(true)
+          .menu(tauri::menu::Menu::default(&app.handle())?);
       }
 
       let window = window_builder.build().unwrap();
@@ -119,16 +141,15 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
       });
     });
 
-  #[cfg(target_os = "macos")]
-  {
-    builder = builder.menu(tauri::Menu::os_default("Tauri API Validation"));
-  }
-
   #[allow(unused_mut)]
   let mut app = builder
     .invoke_handler(tauri::generate_handler![
       cmd::log_operation,
       cmd::perform_request,
+      #[cfg(desktop)]
+      cmd::toggle_menu,
+      #[cfg(desktop)]
+      cmd::popup_context_menu
     ])
     .build(tauri::tauri_build_context!())
     .expect("error while building tauri application");
@@ -140,7 +161,7 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + Send + 'static>(
     #[cfg(all(desktop, not(test)))]
     if let RunEvent::ExitRequested { api, .. } = &_event {
       // Keep the event loop running even if all windows are closed
-      // This allow us to catch system tray events when there is no window
+      // This allow us to catch tray icon events when there is no window
       api.prevent_exit();
     }
   })

+ 98 - 106
examples/api/src-tauri/src/tray.rs

@@ -4,121 +4,113 @@
 
 use std::sync::atomic::{AtomicBool, Ordering};
 use tauri::{
-  CustomMenuItem, Manager, Runtime, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowBuilder,
-  WindowUrl,
+  menu::{Menu, MenuItem},
+  tray::{ClickType, TrayIconBuilder},
+  Manager, Runtime, WindowBuilder, WindowUrl,
 };
 
-pub fn create_tray<R: Runtime>(app: &tauri::App<R>) -> tauri::Result<()> {
-  let mut tray_menu1 = SystemTrayMenu::new()
-    .add_item(CustomMenuItem::new("toggle", "Toggle"))
-    .add_item(CustomMenuItem::new("new", "New window"))
-    .add_item(CustomMenuItem::new("icon_1", "Tray Icon 1"))
-    .add_item(CustomMenuItem::new("icon_2", "Tray Icon 2"));
-
+pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
+  let toggle_i = MenuItem::with_id(app, "toggle", "Toggle", true, None);
+  let new_window_i = MenuItem::with_id(app, "new-window", "New window", true, None);
+  let icon_i_1 = MenuItem::with_id(app, "icon-1", "Icon 1", true, None);
+  let icon_i_2 = MenuItem::with_id(app, "icon-2", "Icon 2", true, None);
   #[cfg(target_os = "macos")]
-  {
-    tray_menu1 = tray_menu1.add_item(CustomMenuItem::new("set_title", "Set Title"));
-  }
-
-  tray_menu1 = tray_menu1
-    .add_item(CustomMenuItem::new("switch_menu", "Switch Menu"))
-    .add_item(CustomMenuItem::new("exit_app", "Quit"))
-    .add_item(CustomMenuItem::new("destroy", "Destroy"));
+  let set_title_i = MenuItem::with_id(app, "set-title", "Set Title", true, None);
+  let switch_i = MenuItem::with_id(app, "switch-menu", "Switch Menu", true, None);
+  let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None);
+  let remove_tray_i = MenuItem::with_id(app, "remove-tray", "Remove Tray icon", true, None);
+  let menu1 = Menu::with_items(
+    app,
+    &[
+      &toggle_i,
+      &new_window_i,
+      &icon_i_1,
+      &icon_i_2,
+      #[cfg(target_os = "macos")]
+      &set_title_i,
+      &switch_i,
+      &quit_i,
+      &remove_tray_i,
+    ],
+  )?;
+  let menu2 = Menu::with_items(
+    app,
+    &[&toggle_i, &new_window_i, &switch_i, &quit_i, &remove_tray_i],
+  )?;
 
-  let tray_menu2 = SystemTrayMenu::new()
-    .add_item(CustomMenuItem::new("toggle", "Toggle"))
-    .add_item(CustomMenuItem::new("new", "New window"))
-    .add_item(CustomMenuItem::new("switch_menu", "Switch Menu"))
-    .add_item(CustomMenuItem::new("exit_app", "Quit"))
-    .add_item(CustomMenuItem::new("destroy", "Destroy"));
   let is_menu1 = AtomicBool::new(true);
 
-  let handle = app.handle();
-  let tray_id = "my-tray".to_string();
-  SystemTray::new()
-    .with_id(&tray_id)
-    .with_menu(tray_menu1.clone())
-    .with_tooltip("Tauri")
-    .on_event(move |event| {
-      let tray_handle = handle.tray_handle_by_id(&tray_id).unwrap();
-      match event {
-        SystemTrayEvent::LeftClick {
-          position: _,
-          size: _,
-          ..
-        } => {
-          let window = handle.get_window("main").unwrap();
-          window.show().unwrap();
-          window.set_focus().unwrap();
+  let _ = TrayIconBuilder::with_id("tray-1")
+    .tooltip("Tauri")
+    .icon(app.default_window_icon().unwrap().clone())
+    .menu(&menu1)
+    .menu_on_left_click(false)
+    .on_menu_event(move |app, event| match event.id.as_ref() {
+      "quit" => {
+        app.exit(0);
+      }
+      "remove-tray" => {
+        app.remove_tray_by_id("tray-1");
+      }
+      "toggle" => {
+        if let Some(window) = app.get_window("main") {
+          let new_title = if window.is_visible().unwrap_or_default() {
+            let _ = window.hide();
+            "Show"
+          } else {
+            let _ = window.show();
+            let _ = window.set_focus();
+            "Hide"
+          };
+          toggle_i.set_text(new_title).unwrap();
         }
-        SystemTrayEvent::MenuItemClick { id, .. } => {
-          let item_handle = tray_handle.get_item(&id);
-          match id.as_str() {
-            "exit_app" => {
-              // exit the app
-              handle.exit(0);
-            }
-            "destroy" => {
-              tray_handle.destroy().unwrap();
-            }
-            "toggle" => {
-              let window = handle.get_window("main").unwrap();
-              let new_title = if window.is_visible().unwrap() {
-                window.hide().unwrap();
-                "Show"
-              } else {
-                window.show().unwrap();
-                "Hide"
-              };
-              item_handle.set_title(new_title).unwrap();
-            }
-            "new" => {
-              WindowBuilder::new(&handle, "new", WindowUrl::App("index.html".into()))
-                .title("Tauri")
-                .build()
-                .unwrap();
-            }
-            "set_title" => {
-              #[cfg(target_os = "macos")]
-              tray_handle.set_title("Tauri").unwrap();
-            }
-            "icon_1" => {
-              #[cfg(target_os = "macos")]
-              tray_handle.set_icon_as_template(true).unwrap();
-
-              tray_handle
-                .set_icon(tauri::Icon::Raw(
-                  include_bytes!("../../../.icons/tray_icon_with_transparency.png").to_vec(),
-                ))
-                .unwrap();
-            }
-            "icon_2" => {
-              #[cfg(target_os = "macos")]
-              tray_handle.set_icon_as_template(true).unwrap();
+      }
+      "new-window" => {
+        let _ = WindowBuilder::new(app, "new", WindowUrl::App("index.html".into()))
+          .title("Tauri")
+          .build();
+      }
+      #[cfg(target_os = "macos")]
+      "set-title" => {
+        if let Some(tray) = app.tray_by_id("tray-1") {
+          let _ = tray.set_title(Some("Tauri"));
+        }
+      }
+      i @ "icon-1" | i @ "icon-2" => {
+        if let Some(tray) = app.tray_by_id("tray-1") {
+          let _ = tray.set_icon(Some(tauri::Icon::Raw(if i == "icon-1" {
+            include_bytes!("../../../.icons/icon.ico").to_vec()
+          } else {
+            include_bytes!("../../../.icons/tray_icon_with_transparency.png").to_vec()
+          })));
+        }
+      }
+      "switch-menu" => {
+        let flag = is_menu1.load(Ordering::Relaxed);
+        let (menu, tooltip) = if flag {
+          (menu2.clone(), "Menu 2")
+        } else {
+          (menu1.clone(), "Tauri")
+        };
+        if let Some(tray) = app.tray_by_id("tray-1") {
+          let _ = tray.set_menu(Some(menu));
+          let _ = tray.set_tooltip(Some(tooltip));
+        }
+        is_menu1.store(!flag, Ordering::Relaxed);
+      }
 
-              tray_handle
-                .set_icon(tauri::Icon::Raw(
-                  include_bytes!("../../../.icons/icon.ico").to_vec(),
-                ))
-                .unwrap();
-            }
-            "switch_menu" => {
-              let flag = is_menu1.load(Ordering::Relaxed);
-              let (menu, tooltip) = if flag {
-                (tray_menu2.clone(), "Menu 2")
-              } else {
-                (tray_menu1.clone(), "Tauri")
-              };
-              tray_handle.set_menu(menu).unwrap();
-              tray_handle.set_tooltip(tooltip).unwrap();
-              is_menu1.store(!flag, Ordering::Relaxed);
-            }
-            _ => {}
-          }
+      _ => {}
+    })
+    .on_tray_event(|tray, event| {
+      if event.click_type == ClickType::Left {
+        let app = tray.app_handle();
+        if let Some(window) = app.get_window("main") {
+          let _ = window.show();
+          let _ = window.set_focus();
         }
-        _ => {}
       }
     })
-    .build(app)
-    .map(|_| ())
+    .build(app);
+
+  Ok(())
 }

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

@@ -112,11 +112,6 @@
           ]
         }
       }
-    },
-    "systemTray": {
-      "iconPath": "../../.icons/tray_icon_with_transparency.png",
-      "iconAsTemplate": true,
-      "menuOnLeftClick": false
     }
   }
 }

+ 6 - 2
examples/api/src/App.svelte

@@ -1,13 +1,17 @@
 <script>
   import { onMount } from 'svelte'
   import { writable } from 'svelte/store'
+  import { invoke } from '@tauri-apps/api/tauri'
 
   import Welcome from './views/Welcome.svelte'
   import Communication from './views/Communication.svelte'
   import WebRTC from './views/WebRTC.svelte'
 
-  const userAgent = navigator.userAgent.toLowerCase()
-  const isMobile = userAgent.includes('android') || userAgent.includes('iphone')
+  document.addEventListener('keydown', (event) => {
+    if (event.ctrlKey && event.key === 'b') {
+      invoke('toggle_menu')
+    }
+  })
 
   const views = [
     {

+ 19 - 7
examples/api/src/views/Welcome.svelte

@@ -1,7 +1,19 @@
-<p>
-  This is a demo of Tauri's API capabilities using the <code
-    >@tauri-apps/api</code
-  > package. It's used as the main validation app, serving as the test bed of our
-  development process. In the future, this app will be used on Tauri's integration
-  tests.
-</p>
+<script>
+  import { invoke } from '@tauri-apps/api/tauri'
+
+  function contextMenu() {
+    invoke('popup_context_menu')
+  }
+</script>
+
+<div>
+  <p>
+    This is a demo of Tauri's API capabilities using the <code
+      >@tauri-apps/api</code
+    > package. It's used as the main validation app, serving as the test bed of our
+    development process. In the future, this app will be used on Tauri's integration
+    tests.
+  </p>
+
+  <button class="btn" on:click={contextMenu}>Context menu</button>
+</div>

+ 1 - 5
examples/splashscreen/main.rs

@@ -61,11 +61,7 @@ mod ui {
   pub fn main() {
     let context = super::context();
     tauri::Builder::default()
-      .menu(if cfg!(target_os = "macos") {
-        tauri::Menu::os_default(&context.package_info().name)
-      } else {
-        tauri::Menu::default()
-      })
+      .menu(tauri::menu::Menu::default)
       .setup(|app| {
         // set the splashscreen and main windows to be globally available with the tauri state API
         app.manage(SplashscreenWindow(Arc::new(Mutex::new(

+ 39 - 33
tooling/cli/Cargo.lock

@@ -299,9 +299,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitflags"
-version = "2.3.1"
+version = "2.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84"
+checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
 
 [[package]]
 name = "bitness"
@@ -1105,12 +1105,9 @@ dependencies = [
 
 [[package]]
 name = "fastrand"
-version = "1.9.0"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
-dependencies = [
- "instant",
-]
+checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
 
 [[package]]
 name = "fdeflate"
@@ -1674,9 +1671,9 @@ dependencies = [
 
 [[package]]
 name = "image"
-version = "0.24.6"
+version = "0.24.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a"
+checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711"
 dependencies = [
  "bytemuck",
  "byteorder",
@@ -1759,15 +1756,6 @@ dependencies = [
  "generic-array",
 ]
 
-[[package]]
-name = "instant"
-version = "0.1.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
-dependencies = [
- "cfg-if",
-]
-
 [[package]]
 name = "io-lifetimes"
 version = "1.0.11"
@@ -1793,7 +1781,7 @@ checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
 dependencies = [
  "hermit-abi 0.3.1",
  "io-lifetimes",
- "rustix",
+ "rustix 0.37.19",
  "windows-sys 0.48.0",
 ]
 
@@ -2085,9 +2073,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.144"
+version = "0.2.147"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
 
 [[package]]
 name = "libflate"
@@ -2138,6 +2126,12 @@ version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
+
 [[package]]
 name = "local-ip-address"
 version = "0.4.9"
@@ -2314,7 +2308,7 @@ version = "2.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0ede2d12cd6fce44da537a4be1f5510c73be2506c2e32dfaaafd1f36968f3a0e"
 dependencies = [
- "bitflags 2.3.1",
+ "bitflags 2.3.3",
  "ctor 0.2.0",
  "napi-derive",
  "napi-sys",
@@ -3261,7 +3255,20 @@ dependencies = [
  "errno",
  "io-lifetimes",
  "libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.3.8",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399"
+dependencies = [
+ "bitflags 2.3.3",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.5",
  "windows-sys 0.48.0",
 ]
 
@@ -3907,9 +3914,9 @@ dependencies = [
 
 [[package]]
 name = "tar"
-version = "0.4.39"
+version = "0.4.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec96d2ffad078296368d46ff1cb309be1c23c513b4ab0e22a45de0185275ac96"
+checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb"
 dependencies = [
  "filetime",
  "libc",
@@ -4142,15 +4149,14 @@ dependencies = [
 
 [[package]]
 name = "tempfile"
-version = "3.6.0"
+version = "3.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6"
+checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651"
 dependencies = [
- "autocfg",
  "cfg-if",
  "fastrand",
  "redox_syscall 0.3.5",
- "rustix",
+ "rustix 0.38.7",
  "windows-sys 0.48.0",
 ]
 
@@ -4232,9 +4238,9 @@ dependencies = [
 
 [[package]]
 name = "tiff"
-version = "0.8.1"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471"
+checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211"
 dependencies = [
  "flate2",
  "jpeg-decoder",
@@ -5099,9 +5105,9 @@ dependencies = [
 
 [[package]]
 name = "xattr"
-version = "0.2.3"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
+checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985"
 dependencies = [
  "libc",
 ]

+ 14 - 7
tooling/cli/schema.json

@@ -1,7 +1,7 @@
 {
   "$schema": "http://json-schema.org/draft-07/schema#",
   "title": "Config",
-  "description": "The Tauri configuration object. It is read from a file where you can define your frontend assets, configure the bundler and define a system tray.\n\nThe configuration file is generated by the [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in your Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively. The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`. The TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can read a platform-specific configuration from `tauri.linux.conf.json`, `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json` (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used), which gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`package`](#packageconfig): Package settings - [`tauri`](#tauriconfig): The Tauri config - [`build`](#buildconfig): The build configuration - [`plugins`](#pluginconfig): The plugins config\n\n```json title=\"Example tauri.config.json file\" { \"build\": { \"beforeBuildCommand\": \"\", \"beforeDevCommand\": \"\", \"devPath\": \"../dist\", \"distDir\": \"../dist\" }, \"package\": { \"productName\": \"tauri-app\", \"version\": \"0.1.0\" }, \"tauri\": { \"bundle\": {}, \"security\": { \"csp\": null }, \"windows\": [ { \"fullscreen\": false, \"height\": 600, \"resizable\": true, \"title\": \"Tauri App\", \"width\": 800 } ] } } ```",
+  "description": "The Tauri configuration object. It is read from a file where you can define your frontend assets, configure the bundler and define a tray icon.\n\nThe configuration file is generated by the [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in your Tauri application source directory (src-tauri).\n\nOnce generated, you may modify it at will to customize your Tauri application.\n\n## File Formats\n\nBy default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\nTauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively. The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`. The TOML file name is `Tauri.toml`.\n\n## Platform-Specific Configuration\n\nIn addition to the default configuration file, Tauri can read a platform-specific configuration from `tauri.linux.conf.json`, `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json` (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used), which gets merged with the main configuration object.\n\n## Configuration Structure\n\nThe configuration is composed of the following objects:\n\n- [`package`](#packageconfig): Package settings - [`tauri`](#tauriconfig): The Tauri config - [`build`](#buildconfig): The build configuration - [`plugins`](#pluginconfig): The plugins config\n\n```json title=\"Example tauri.config.json file\" { \"build\": { \"beforeBuildCommand\": \"\", \"beforeDevCommand\": \"\", \"devPath\": \"../dist\", \"distDir\": \"../dist\" }, \"package\": { \"productName\": \"tauri-app\", \"version\": \"0.1.0\" }, \"tauri\": { \"bundle\": {}, \"security\": { \"csp\": null }, \"windows\": [ { \"fullscreen\": false, \"height\": 600, \"resizable\": true, \"title\": \"Tauri App\", \"width\": 800 } ] } } ```",
   "type": "object",
   "properties": {
     "$schema": {
@@ -223,11 +223,11 @@
             }
           ]
         },
-        "systemTray": {
-          "description": "Configuration for app system tray.",
+        "trayIcon": {
+          "description": "Configuration for app tray icon.",
           "anyOf": [
             {
-              "$ref": "#/definitions/SystemTrayConfig"
+              "$ref": "#/definitions/TrayIconConfig"
             },
             {
               "type": "null"
@@ -2065,15 +2065,15 @@
         }
       ]
     },
-    "SystemTrayConfig": {
-      "description": "Configuration for application system tray icon.\n\nSee more: https://tauri.app/v1/api/config#systemtrayconfig",
+    "TrayIconConfig": {
+      "description": "Configuration for application tray icon.\n\nSee more: https://tauri.app/v1/api/config#trayiconconfig",
       "type": "object",
       "required": [
         "iconPath"
       ],
       "properties": {
         "iconPath": {
-          "description": "Path to the default icon to use on the system tray.",
+          "description": "Path to the default icon to use for the tray icon.",
           "type": "string"
         },
         "iconAsTemplate": {
@@ -2092,6 +2092,13 @@
             "string",
             "null"
           ]
+        },
+        "tooltip": {
+          "description": "Tray icon tooltip on Windows and macOS",
+          "type": [
+            "string",
+            "null"
+          ]
         }
       },
       "additionalProperties": false

+ 0 - 59
tooling/cli/src/build.rs

@@ -143,31 +143,6 @@ pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
     // set env vars used by the bundler
     #[cfg(target_os = "linux")]
     {
-      if config_.tauri.system_tray.is_some() {
-        if let Ok(tray) = std::env::var("TAURI_TRAY") {
-          std::env::set_var(
-            "TRAY_LIBRARY_PATH",
-            if tray == "ayatana" {
-              format!(
-                "{}/libayatana-appindicator3.so.1",
-                pkgconfig_utils::get_library_path("ayatana-appindicator3-0.1")
-                  .expect("failed to get ayatana-appindicator library path using pkg-config.")
-              )
-            } else {
-              format!(
-                "{}/libappindicator3.so.1",
-                pkgconfig_utils::get_library_path("appindicator3-0.1")
-                  .expect("failed to get libappindicator-gtk library path using pkg-config.")
-              )
-            },
-          );
-        } else {
-          std::env::set_var(
-            "TRAY_LIBRARY_PATH",
-            pkgconfig_utils::get_appindicator_library_path(),
-          );
-        }
-      }
       if config_.tauri.bundle.appimage.bundle_media_framework {
         std::env::set_var("APPIMAGE_BUNDLE_GSTREAMER", "1");
       }
@@ -394,37 +369,3 @@ fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
   }
   Ok(())
 }
-
-#[cfg(target_os = "linux")]
-mod pkgconfig_utils {
-  use std::{path::PathBuf, process::Command};
-
-  pub fn get_appindicator_library_path() -> PathBuf {
-    match get_library_path("ayatana-appindicator3-0.1") {
-      Some(p) => format!("{p}/libayatana-appindicator3.so.1").into(),
-      None => match get_library_path("appindicator3-0.1") {
-        Some(p) => format!("{p}/libappindicator3.so.1").into(),
-        None => panic!("Can't detect any appindicator library"),
-      },
-    }
-  }
-
-  /// Gets the folder in which a library is located using `pkg-config`.
-  pub fn get_library_path(name: &str) -> Option<String> {
-    let mut cmd = Command::new("pkg-config");
-    cmd.env("PKG_CONFIG_ALLOW_SYSTEM_LIBS", "1");
-    cmd.arg("--libs-only-L");
-    cmd.arg(name);
-    if let Ok(output) = cmd.output() {
-      if !output.stdout.is_empty() {
-        // output would be "-L/path/to/library\n"
-        let word = output.stdout[2..].to_vec();
-        return Some(String::from_utf8_lossy(&word).trim().to_string());
-      } else {
-        None
-      }
-    } else {
-      None
-    }
-  }
-}

+ 82 - 13
tooling/cli/src/interface/rust.rs

@@ -697,12 +697,7 @@ impl AppSettings for RustAppSettings {
     config: &Config,
     features: &[String],
   ) -> crate::Result<BundleSettings> {
-    tauri_config_to_bundle_settings(
-      &self.manifest,
-      features,
-      config.tauri.bundle.clone(),
-      config.tauri.system_tray.clone(),
-    )
+    tauri_config_to_bundle_settings(&self.manifest, features, config.tauri.bundle.clone())
   }
 
   fn app_binary_path(&self, options: &Options) -> crate::Result<PathBuf> {
@@ -1045,7 +1040,6 @@ fn tauri_config_to_bundle_settings(
   manifest: &Manifest,
   features: &[String],
   config: crate::helpers::config::BundleConfig,
-  system_tray_config: Option<crate::helpers::config::SystemTrayConfig>,
 ) -> crate::Result<BundleSettings> {
   let enabled_features = manifest.all_enabled_features(features);
 
@@ -1066,15 +1060,45 @@ fn tauri_config_to_bundle_settings(
   #[allow(unused_mut)]
   let mut depends = config.deb.depends.unwrap_or_default();
 
+  // set env vars used by the bundler and inject dependencies
   #[cfg(target_os = "linux")]
   {
-    if let Some(system_tray_config) = &system_tray_config {
-      let tray = std::env::var("TAURI_TRAY").unwrap_or_else(|_| "ayatana".to_string());
-      if tray == "ayatana" {
-        depends.push("libayatana-appindicator3-1".into());
-      } else {
-        depends.push("libappindicator3-1".into());
+    if enabled_features.contains(&"tray-icon".into())
+      || enabled_features.contains(&"tauri/tray-icon".into())
+    {
+      let (tray_kind, path) = std::env::var("TAURI_TRAY")
+        .map(|kind| {
+          if kind == "ayatana" {
+            (
+              pkgconfig_utils::TrayKind::Ayatana,
+              format!(
+                "{}/libayatana-appindicator3.so.1",
+                pkgconfig_utils::get_library_path("ayatana-appindicator3-0.1")
+                  .expect("failed to get ayatana-appindicator library path using pkg-config.")
+              ),
+            )
+          } else {
+            (
+              pkgconfig_utils::TrayKind::Libappindicator,
+              format!(
+                "{}/libappindicator3.so.1",
+                pkgconfig_utils::get_library_path("appindicator3-0.1")
+                  .expect("failed to get libappindicator-gtk library path using pkg-config.")
+              ),
+            )
+          }
+        })
+        .unwrap_or_else(|_| pkgconfig_utils::get_appindicator_library_path());
+      match tray_kind {
+        pkgconfig_utils::TrayKind::Ayatana => {
+          depends.push("libayatana-appindicator3-1".into());
+        }
+        pkgconfig_utils::TrayKind::Libappindicator => {
+          depends.push("libappindicator3-1".into());
+        }
       }
+
+      std::env::set_var("TRAY_LIBRARY_PATH", path);
     }
 
     // provides `libwebkit2gtk-4.1.so.37` and all `4.0` versions have the -37 package name
@@ -1184,3 +1208,48 @@ fn tauri_config_to_bundle_settings(
     ..Default::default()
   })
 }
+
+#[cfg(target_os = "linux")]
+mod pkgconfig_utils {
+  use std::process::Command;
+
+  pub enum TrayKind {
+    Ayatana,
+    Libappindicator,
+  }
+
+  pub fn get_appindicator_library_path() -> (TrayKind, String) {
+    match get_library_path("ayatana-appindicator3-0.1") {
+      Some(p) => (
+        TrayKind::Ayatana,
+        format!("{p}/libayatana-appindicator3.so.1"),
+      ),
+      None => match get_library_path("appindicator3-0.1") {
+        Some(p) => (
+          TrayKind::Libappindicator,
+          format!("{p}/libappindicator3.so.1"),
+        ),
+        None => panic!("Can't detect any appindicator library"),
+      },
+    }
+  }
+
+  /// Gets the folder in which a library is located using `pkg-config`.
+  pub fn get_library_path(name: &str) -> Option<String> {
+    let mut cmd = Command::new("pkg-config");
+    cmd.env("PKG_CONFIG_ALLOW_SYSTEM_LIBS", "1");
+    cmd.arg("--libs-only-L");
+    cmd.arg(name);
+    if let Ok(output) = cmd.output() {
+      if !output.stdout.is_empty() {
+        // output would be "-L/path/to/library\n"
+        let word = output.stdout[2..].to_vec();
+        return Some(String::from_utf8_lossy(&word).trim().to_string());
+      } else {
+        None
+      }
+    } else {
+      None
+    }
+  }
+}

+ 4 - 0
tooling/cli/src/migrate/config.rs

@@ -54,6 +54,10 @@ fn migrate_config(config: &mut Value) -> Result<()> {
         process_security(security)?;
       }
 
+      if let Some(tray) = tauri_config.remove("systemTray") {
+        tauri_config.insert("trayIcon".into(), tray);
+      }
+
       // cli
       if let Some(cli) = tauri_config.remove("cli") {
         process_cli(&mut plugins, cli)?;

+ 3 - 0
tooling/cli/src/migrate/manifest.rs

@@ -89,6 +89,7 @@ fn features_to_remove() -> Vec<&'static str> {
   features_to_remove.push("shell-open-api");
   features_to_remove.push("windows7-compat");
   features_to_remove.push("updater");
+  features_to_remove.push("system-tray");
 
   // this allowlist feature was not removed
   let index = features_to_remove
@@ -152,6 +153,8 @@ fn migrate_dependency_table<D: TableLike>(dep: &mut D, version: String, remove:
           features_array.remove(index);
           if f == "reqwest-native-tls-vendored" {
             add_features.push("native-tls-vendored");
+          } else if f == "system-tray" {
+            add_features.push("tray-icon");
           }
         }
       }

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff