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>
This commit is contained in:
Amr Bashir 2023-08-14 04:25:50 +03:00 committed by GitHub
parent ec827760ab
commit 7fb419c326
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 5826 additions and 4348 deletions

View File

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

View File

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

View File

@ -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.

View File

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

View File

@ -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.

View File

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

View File

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

View File

@ -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.

View File

@ -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.**

View File

@ -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.

View File

@ -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.

View File

@ -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
.changes/tauri-nsview.md Normal file
View File

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

View File

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

View File

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

View File

@ -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' }

View File

@ -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
}

View File

@ -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",

View File

@ -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()

View File

@ -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()

View File

@ -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!(

View File

@ -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.

View File

@ -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
}))
}

View File

@ -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(

View File

@ -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

View File

@ -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",

File diff suppressed because it is too large Load Diff

View File

@ -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
}

View File

@ -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 = [ ]

View File

@ -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<()>;
}

View File

@ -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 iChats available image.
StatusAvailable,
/// Small clear indicator.
StatusNone,
/// Small yellow indicator, similar to iChats idle image.
StatusPartiallyAvailable,
/// Small red indicator, similar to iChats 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,
}

View File

@ -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.

View File

@ -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<()>,
}

View File

@ -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,
};

View File

@ -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" ]

File diff suppressed because it is too large Load Diff

View File

@ -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)
}
}

View File

@ -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),
}

View File

@ -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)
}

View File

@ -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::*;

View File

@ -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

View File

@ -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,
)
}
}
}

View File

@ -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,
)
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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;

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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
core/tauri/src/menu/icon.rs Normal file
View File

@ -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
core/tauri/src/menu/menu.rs Normal file
View File

@ -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
core/tauri/src/menu/mod.rs Normal file
View File

@ -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)),
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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| {

View File

@ -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.

View File

@ -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()));
}
});

View File

@ -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!()
}

View File

@ -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
core/tauri/src/tray.rs Normal file
View File

@ -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)
}
}

View File

@ -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();

View File

@ -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)
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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();
}

View File

@ -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();
}
})

View File

@ -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"));
}
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],
)?;
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 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();
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);
}
_ => {}
}
}
"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);
}
_ => {}
})
.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(())
}

View File

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

View File

@ -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 = [
{

View File

@ -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>

View File

@ -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(

72
tooling/cli/Cargo.lock generated
View File

@ -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",
]

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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)?;

View File

@ -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");
}
}
}