mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-01-31 00:35:19 +01:00
This commit is contained in:
committed by
GitHub
parent
8f29a260e6
commit
f7e9fe8f3f
5
.changes/menu-refactor.md
Normal file
5
.changes/menu-refactor.md
Normal 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.
|
||||
5
.changes/system-tray-refactor.md
Normal file
5
.changes/system-tray-refactor.md
Normal 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.
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<()>;
|
||||
}
|
||||
|
||||
@@ -6,35 +6,245 @@ use std::{collections::hash_map::DefaultHasher, hash::Hasher};
|
||||
|
||||
use super::MenuId;
|
||||
|
||||
/// Named images defined by the system.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NativeImage {
|
||||
/// An add item template image.
|
||||
Add,
|
||||
/// Advanced preferences toolbar icon for the preferences window.
|
||||
Advanced,
|
||||
/// A Bluetooth template image.
|
||||
Bluetooth,
|
||||
/// Bookmarks image suitable for a template.
|
||||
Bookmarks,
|
||||
/// A caution image.
|
||||
Caution,
|
||||
/// A color panel toolbar icon.
|
||||
ColorPanel,
|
||||
/// A column view mode template image.
|
||||
ColumnView,
|
||||
/// A computer icon.
|
||||
Computer,
|
||||
/// An enter full-screen mode template image.
|
||||
EnterFullScreen,
|
||||
/// Permissions for all users.
|
||||
Everyone,
|
||||
/// An exit full-screen mode template image.
|
||||
ExitFullScreen,
|
||||
/// A cover flow view mode template image.
|
||||
FlowView,
|
||||
/// A folder image.
|
||||
Folder,
|
||||
/// A burnable folder icon.
|
||||
FolderBurnable,
|
||||
/// A smart folder icon.
|
||||
FolderSmart,
|
||||
/// A link template image.
|
||||
FollowLinkFreestanding,
|
||||
/// A font panel toolbar icon.
|
||||
FontPanel,
|
||||
/// A `go back` template image.
|
||||
GoLeft,
|
||||
/// A `go forward` template image.
|
||||
GoRight,
|
||||
/// Home image suitable for a template.
|
||||
Home,
|
||||
/// An iChat Theater template image.
|
||||
IChatTheater,
|
||||
/// An icon view mode template image.
|
||||
IconView,
|
||||
/// An information toolbar icon.
|
||||
Info,
|
||||
/// A template image used to denote invalid data.
|
||||
InvalidDataFreestanding,
|
||||
/// A generic left-facing triangle template image.
|
||||
LeftFacingTriangle,
|
||||
/// A list view mode template image.
|
||||
ListView,
|
||||
/// A locked padlock template image.
|
||||
LockLocked,
|
||||
/// An unlocked padlock template image.
|
||||
LockUnlocked,
|
||||
/// A horizontal dash, for use in menus.
|
||||
MenuMixedState,
|
||||
/// A check mark template image, for use in menus.
|
||||
MenuOnState,
|
||||
/// A MobileMe icon.
|
||||
MobileMe,
|
||||
/// A drag image for multiple items.
|
||||
MultipleDocuments,
|
||||
/// A network icon.
|
||||
Network,
|
||||
/// A path button template image.
|
||||
Path,
|
||||
/// General preferences toolbar icon for the preferences window.
|
||||
PreferencesGeneral,
|
||||
/// A Quick Look template image.
|
||||
QuickLook,
|
||||
/// A refresh template image.
|
||||
RefreshFreestanding,
|
||||
/// A refresh template image.
|
||||
Refresh,
|
||||
/// A remove item template image.
|
||||
Remove,
|
||||
/// A reveal contents template image.
|
||||
RevealFreestanding,
|
||||
/// A generic right-facing triangle template image.
|
||||
RightFacingTriangle,
|
||||
/// A share view template image.
|
||||
Share,
|
||||
/// A slideshow template image.
|
||||
Slideshow,
|
||||
/// A badge for a `smart` item.
|
||||
SmartBadge,
|
||||
/// Small green indicator, similar to iChat’s available image.
|
||||
StatusAvailable,
|
||||
/// Small clear indicator.
|
||||
StatusNone,
|
||||
/// Small yellow indicator, similar to iChat’s idle image.
|
||||
StatusPartiallyAvailable,
|
||||
/// Small red indicator, similar to iChat’s unavailable image.
|
||||
StatusUnavailable,
|
||||
/// A stop progress template image.
|
||||
StopProgressFreestanding,
|
||||
/// A stop progress button template image.
|
||||
StopProgress,
|
||||
|
||||
/// An image of the empty trash can.
|
||||
TrashEmpty,
|
||||
/// An image of the full trash can.
|
||||
TrashFull,
|
||||
/// Permissions for a single user.
|
||||
User,
|
||||
/// User account toolbar icon for the preferences window.
|
||||
UserAccounts,
|
||||
/// Permissions for a group of users.
|
||||
UserGroup,
|
||||
/// Permissions for guests.
|
||||
UserGuest,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MenuUpdate {
|
||||
/// Modifies the enabled state of the menu item.
|
||||
SetEnabled(bool),
|
||||
/// Modifies the title (label) of the menu item.
|
||||
SetTitle(String),
|
||||
/// Modifies the selected state of the menu item.
|
||||
SetSelected(bool),
|
||||
/// Update native image.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]
|
||||
SetNativeImage(NativeImage),
|
||||
}
|
||||
|
||||
pub trait TrayHandle {
|
||||
fn set_icon(&self, icon: crate::Icon) -> crate::Result<()>;
|
||||
fn update_item(&self, id: u32, update: MenuUpdate) -> crate::Result<()>;
|
||||
}
|
||||
|
||||
/// A window menu.
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct Menu<I: MenuId> {
|
||||
pub items: Vec<MenuEntry<I>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct Submenu<I: MenuId> {
|
||||
pub title: String,
|
||||
pub items: Vec<MenuItem<I>>,
|
||||
pub enabled: bool,
|
||||
pub inner: Menu<I>,
|
||||
}
|
||||
|
||||
impl<I: MenuId> 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
164
core/tauri/src/app/tray.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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!(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[cfg(feature = "menu")]
|
||||
use crate::runtime::MenuId;
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "menu")))]
|
||||
pub(crate) mod menu;
|
||||
|
||||
use crate::{
|
||||
api::config::WindowUrl,
|
||||
command::{CommandArg, CommandItem},
|
||||
@@ -31,22 +33,6 @@ use std::{
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
/// The window menu event.
|
||||
#[cfg(feature = "menu")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "menu")))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MenuEvent<I: MenuId> {
|
||||
pub(crate) menu_item_id: I,
|
||||
}
|
||||
|
||||
#[cfg(feature = "menu")]
|
||||
impl<I: MenuId> MenuEvent<I> {
|
||||
/// The menu item id.
|
||||
pub fn menu_item_id(&self) -> &I {
|
||||
&self.menu_item_id
|
||||
}
|
||||
}
|
||||
|
||||
/// Monitor descriptor.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -301,10 +287,10 @@ impl<P: Params> Window<P> {
|
||||
/// Registers a menu event listener.
|
||||
#[cfg(feature = "menu")]
|
||||
#[cfg_attr(doc_cfg, doc(cfg(feature = "menu")))]
|
||||
pub fn on_menu_event<F: Fn(MenuEvent<P::MenuId>) + Send + 'static>(&self, f: F) {
|
||||
pub fn on_menu_event<F: Fn(menu::MenuEvent<P::MenuId>) + Send + 'static>(&self, f: F) {
|
||||
let menu_ids = self.manager.menu_ids();
|
||||
self.window.dispatcher.on_menu_event(move |event| {
|
||||
f(MenuEvent {
|
||||
f(menu::MenuEvent {
|
||||
menu_item_id: menu_ids.get(&event.menu_item_id).unwrap().clone(),
|
||||
})
|
||||
});
|
||||
@@ -312,6 +298,15 @@ impl<P: Params> Window<P> {
|
||||
|
||||
// Getters
|
||||
|
||||
/// Gets a handle to the window menu.
|
||||
#[cfg(feature = "menu")]
|
||||
pub fn menu_handle(&self) -> menu::MenuHandle<P> {
|
||||
menu::MenuHandle {
|
||||
ids: self.manager.menu_ids(),
|
||||
dispatcher: self.dispatcher(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the scale factor that can be used to map logical pixels to physical pixels, and vice versa.
|
||||
pub fn scale_factor(&self) -> crate::Result<f64> {
|
||||
self.window.dispatcher.scale_factor().map_err(Into::into)
|
||||
|
||||
108
core/tauri/src/window/menu.rs
Normal file
108
core/tauri/src/window/menu.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
161
docs/usage/guides/visual/menu.md
Normal file
161
docs/usage/guides/visual/menu.md
Normal 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(())
|
||||
})
|
||||
}
|
||||
```
|
||||
180
docs/usage/guides/visual/system-tray.md
Normal file
180
docs/usage/guides/visual/system-tray.md
Normal 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
@@ -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![
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user