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

This commit is contained in:
Lucas Fernandes Nogueira
2021-06-04 13:51:15 -03:00
committed by GitHub
parent 8f29a260e6
commit f7e9fe8f3f
24 changed files with 1752 additions and 418 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

164
core/tauri/src/app/tray.rs Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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