1pub(crate) mod plugin;
8
9use crate::app::{GlobalMenuEventListener, GlobalTrayIconEventListener};
10use crate::menu::ContextMenu;
11use crate::menu::MenuEvent;
12use crate::resources::Resource;
13use crate::{
14 image::Image, menu::run_item_main_thread, AppHandle, Manager, PhysicalPosition, Rect, Runtime,
15};
16use crate::{ResourceId, UnsafeSend};
17use serde::Serialize;
18use std::path::Path;
19pub use tray_icon::TrayIconId;
20
21#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Serialize)]
23pub enum MouseButtonState {
24 #[default]
26 Up,
27 Down,
29}
30
31impl From<tray_icon::MouseButtonState> for MouseButtonState {
32 fn from(value: tray_icon::MouseButtonState) -> Self {
33 match value {
34 tray_icon::MouseButtonState::Up => MouseButtonState::Up,
35 tray_icon::MouseButtonState::Down => MouseButtonState::Down,
36 }
37 }
38}
39
40#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Default)]
42pub enum MouseButton {
43 #[default]
45 Left,
46 Right,
48 Middle,
50}
51
52impl From<tray_icon::MouseButton> for MouseButton {
53 fn from(value: tray_icon::MouseButton) -> Self {
54 match value {
55 tray_icon::MouseButton::Left => MouseButton::Left,
56 tray_icon::MouseButton::Right => MouseButton::Right,
57 tray_icon::MouseButton::Middle => MouseButton::Middle,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Serialize)]
69#[serde(tag = "type")]
70#[non_exhaustive]
71pub enum TrayIconEvent {
72 #[serde(rename_all = "camelCase")]
74 Click {
75 id: TrayIconId,
77 position: PhysicalPosition<f64>,
79 rect: Rect,
81 button: MouseButton,
83 button_state: MouseButtonState,
85 },
86 DoubleClick {
88 id: TrayIconId,
90 position: PhysicalPosition<f64>,
92 rect: Rect,
94 button: MouseButton,
96 },
97 Enter {
99 id: TrayIconId,
101 position: PhysicalPosition<f64>,
103 rect: Rect,
105 },
106 Move {
108 id: TrayIconId,
110 position: PhysicalPosition<f64>,
112 rect: Rect,
114 },
115 Leave {
117 id: TrayIconId,
119 position: PhysicalPosition<f64>,
121 rect: Rect,
123 },
124}
125
126impl TrayIconEvent {
127 pub fn id(&self) -> &TrayIconId {
129 match self {
130 TrayIconEvent::Click { id, .. } => id,
131 TrayIconEvent::DoubleClick { id, .. } => id,
132 TrayIconEvent::Enter { id, .. } => id,
133 TrayIconEvent::Move { id, .. } => id,
134 TrayIconEvent::Leave { id, .. } => id,
135 }
136 }
137}
138
139impl From<tray_icon::TrayIconEvent> for TrayIconEvent {
140 fn from(value: tray_icon::TrayIconEvent) -> Self {
141 match value {
142 tray_icon::TrayIconEvent::Click {
143 id,
144 position,
145 rect,
146 button,
147 button_state,
148 } => TrayIconEvent::Click {
149 id,
150 position,
151 rect: Rect {
152 position: rect.position.into(),
153 size: rect.size.into(),
154 },
155 button: button.into(),
156 button_state: button_state.into(),
157 },
158 tray_icon::TrayIconEvent::DoubleClick {
159 id,
160 position,
161 rect,
162 button,
163 } => TrayIconEvent::DoubleClick {
164 id,
165 position,
166 rect: Rect {
167 position: rect.position.into(),
168 size: rect.size.into(),
169 },
170 button: button.into(),
171 },
172 tray_icon::TrayIconEvent::Enter { id, position, rect } => TrayIconEvent::Enter {
173 id,
174 position,
175 rect: Rect {
176 position: rect.position.into(),
177 size: rect.size.into(),
178 },
179 },
180 tray_icon::TrayIconEvent::Move { id, position, rect } => TrayIconEvent::Move {
181 id,
182 position,
183 rect: Rect {
184 position: rect.position.into(),
185 size: rect.size.into(),
186 },
187 },
188 tray_icon::TrayIconEvent::Leave { id, position, rect } => TrayIconEvent::Leave {
189 id,
190 position,
191 rect: Rect {
192 position: rect.position.into(),
193 size: rect.size.into(),
194 },
195 },
196 _ => todo!(),
197 }
198 }
199}
200
201#[derive(Default)]
203pub struct TrayIconBuilder<R: Runtime> {
204 on_menu_event: Option<GlobalMenuEventListener<AppHandle<R>>>,
205 on_tray_icon_event: Option<GlobalTrayIconEventListener<TrayIcon<R>>>,
206 inner: tray_icon::TrayIconBuilder,
207}
208
209impl<R: Runtime> TrayIconBuilder<R> {
210 pub fn new() -> Self {
217 Self {
218 inner: tray_icon::TrayIconBuilder::new(),
219 on_menu_event: None,
220 on_tray_icon_event: None,
221 }
222 }
223
224 pub fn with_id<I: Into<TrayIconId>>(id: I) -> Self {
231 let mut builder = Self::new();
232 builder.inner = builder.inner.with_id(id);
233 builder
234 }
235
236 pub fn menu<M: ContextMenu>(mut self, menu: &M) -> Self {
242 self.inner = self.inner.with_menu(menu.inner_context_owned());
243 self
244 }
245
246 pub fn icon(mut self, icon: Image<'_>) -> Self {
253 let icon = icon.try_into().ok();
254 if let Some(icon) = icon {
255 self.inner = self.inner.with_icon(icon);
256 }
257 self
258 }
259
260 pub fn tooltip<S: AsRef<str>>(mut self, s: S) -> Self {
266 self.inner = self.inner.with_tooltip(s);
267 self
268 }
269
270 pub fn title<S: AsRef<str>>(mut self, title: S) -> Self {
281 self.inner = self.inner.with_title(title);
282 self
283 }
284
285 pub fn temp_dir_path<P: AsRef<Path>>(mut self, s: P) -> Self {
290 self.inner = self.inner.with_temp_dir_path(s);
291 self
292 }
293
294 pub fn icon_as_template(mut self, is_template: bool) -> Self {
296 self.inner = self.inner.with_icon_as_template(is_template);
297 self
298 }
299
300 #[deprecated(
306 since = "2.2.0",
307 note = "Use `TrayIconBuilder::show_menu_on_left_click` instead."
308 )]
309 pub fn menu_on_left_click(mut self, enable: bool) -> Self {
310 self.inner = self.inner.with_menu_on_left_click(enable);
311 self
312 }
313
314 pub fn show_menu_on_left_click(mut self, enable: bool) -> Self {
320 self.inner = self.inner.with_menu_on_left_click(enable);
321 self
322 }
323
324 pub fn on_menu_event<F: Fn(&AppHandle<R>, MenuEvent) + Sync + Send + 'static>(
329 mut self,
330 f: F,
331 ) -> Self {
332 self.on_menu_event.replace(Box::new(f));
333 self
334 }
335
336 pub fn on_tray_icon_event<F: Fn(&TrayIcon<R>, TrayIconEvent) + Sync + Send + 'static>(
338 mut self,
339 f: F,
340 ) -> Self {
341 self.on_tray_icon_event.replace(Box::new(f));
342 self
343 }
344
345 pub fn id(&self) -> &TrayIconId {
348 self.inner.id()
349 }
350
351 pub(crate) fn build_inner(
352 self,
353 app_handle: &AppHandle<R>,
354 ) -> crate::Result<(TrayIcon<R>, ResourceId)> {
355 let id = self.id().clone();
356
357 let unsafe_builder = UnsafeSend(self.inner);
361
362 let (tx, rx) = std::sync::mpsc::channel();
363 let unsafe_tray = app_handle
364 .run_on_main_thread(move || {
365 let _ = tx.send(unsafe_builder.take().build().map(UnsafeSend));
367 })
368 .and_then(|_| rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage))??;
369
370 let icon = TrayIcon {
371 id,
372 inner: unsafe_tray.take(),
373 app_handle: app_handle.clone(),
374 };
375
376 let rid = icon.register(
377 &icon.app_handle,
378 self.on_menu_event,
379 self.on_tray_icon_event,
380 );
381
382 Ok((icon, rid))
383 }
384
385 pub fn build<M: Manager<R>>(self, manager: &M) -> crate::Result<TrayIcon<R>> {
387 let (icon, _rid) = self.build_inner(manager.app_handle())?;
388 Ok(icon)
389 }
390}
391
392#[tauri_macros::default_runtime(crate::Wry, wry)]
398pub struct TrayIcon<R: Runtime> {
399 id: TrayIconId,
400 inner: tray_icon::TrayIcon,
401 app_handle: AppHandle<R>,
402}
403
404impl<R: Runtime> Clone for TrayIcon<R> {
405 fn clone(&self) -> Self {
406 Self {
407 id: self.id.clone(),
408 inner: self.inner.clone(),
409 app_handle: self.app_handle.clone(),
410 }
411 }
412}
413
414unsafe impl<R: Runtime> Sync for TrayIcon<R> {}
418unsafe impl<R: Runtime> Send for TrayIcon<R> {}
419
420impl<R: Runtime> TrayIcon<R> {
421 fn register(
422 &self,
423 app_handle: &AppHandle<R>,
424 on_menu_event: Option<GlobalMenuEventListener<AppHandle<R>>>,
425 on_tray_icon_event: Option<GlobalTrayIconEventListener<TrayIcon<R>>>,
426 ) -> ResourceId {
427 if let Some(handler) = on_menu_event {
428 app_handle
429 .manager
430 .menu
431 .global_event_listeners
432 .lock()
433 .unwrap()
434 .push(handler);
435 }
436
437 if let Some(handler) = on_tray_icon_event {
438 app_handle
439 .manager
440 .tray
441 .event_listeners
442 .lock()
443 .unwrap()
444 .insert(self.id.clone(), handler);
445 }
446
447 let rid = app_handle.resources_table().add(self.clone());
448 app_handle
449 .manager
450 .tray
451 .icons
452 .lock()
453 .unwrap()
454 .push((self.id().clone(), rid));
455 rid
456 }
457
458 pub fn app_handle(&self) -> &AppHandle<R> {
460 &self.app_handle
461 }
462
463 pub fn on_menu_event<F: Fn(&AppHandle<R>, MenuEvent) + Sync + Send + 'static>(&self, f: F) {
468 self
469 .app_handle
470 .manager
471 .menu
472 .global_event_listeners
473 .lock()
474 .unwrap()
475 .push(Box::new(f));
476 }
477
478 pub fn on_tray_icon_event<F: Fn(&TrayIcon<R>, TrayIconEvent) + Sync + Send + 'static>(
480 &self,
481 f: F,
482 ) {
483 self
484 .app_handle
485 .manager
486 .tray
487 .event_listeners
488 .lock()
489 .unwrap()
490 .insert(self.id.clone(), Box::new(f));
491 }
492
493 pub fn id(&self) -> &TrayIconId {
495 &self.id
496 }
497
498 pub fn set_icon(&self, icon: Option<Image<'_>>) -> crate::Result<()> {
500 let icon = match icon {
501 Some(i) => Some(i.try_into()?),
502 None => None,
503 };
504 run_item_main_thread!(self, |self_: Self| self_.inner.set_icon(icon))?.map_err(Into::into)
505 }
506
507 pub fn set_menu<M: ContextMenu + 'static>(&self, menu: Option<M>) -> crate::Result<()> {
513 run_item_main_thread!(self, |self_: Self| {
514 self_.inner.set_menu(menu.map(|m| m.inner_context_owned()))
515 })
516 }
517
518 pub fn set_tooltip<S: AsRef<str>>(&self, tooltip: Option<S>) -> crate::Result<()> {
524 let s = tooltip.map(|s| s.as_ref().to_string());
525 run_item_main_thread!(self, |self_: Self| self_.inner.set_tooltip(s))?.map_err(Into::into)
526 }
527
528 pub fn set_title<S: AsRef<str>>(&self, title: Option<S>) -> crate::Result<()> {
539 let s = title.map(|s| s.as_ref().to_string());
540 run_item_main_thread!(self, |self_: Self| self_.inner.set_title(s))
541 }
542
543 pub fn set_visible(&self, visible: bool) -> crate::Result<()> {
545 run_item_main_thread!(self, |self_: Self| self_.inner.set_visible(visible))?.map_err(Into::into)
546 }
547
548 pub fn set_temp_dir_path<P: AsRef<Path>>(&self, path: Option<P>) -> crate::Result<()> {
553 #[allow(unused)]
554 let p = path.map(|p| p.as_ref().to_path_buf());
555 #[cfg(target_os = "linux")]
556 run_item_main_thread!(self, |self_: Self| self_.inner.set_temp_dir_path(p))?;
557 Ok(())
558 }
559
560 pub fn set_icon_as_template(&self, #[allow(unused)] is_template: bool) -> crate::Result<()> {
562 #[cfg(target_os = "macos")]
563 run_item_main_thread!(self, |self_: Self| {
564 self_.inner.set_icon_as_template(is_template)
565 })?;
566 Ok(())
567 }
568
569 pub fn set_icon_with_as_template(
578 &self,
579 icon: Option<Image<'_>>,
580 #[allow(unused)] is_template: bool,
581 ) -> crate::Result<()> {
582 #[cfg(target_os = "macos")]
583 {
584 let tray_icon = match icon {
585 Some(i) => Some(i.try_into()?),
586 None => None,
587 };
588 run_item_main_thread!(self, |self_: Self| {
589 self_
590 .inner
591 .set_icon_with_as_template(tray_icon, is_template)
592 })??;
593 }
594 #[cfg(not(target_os = "macos"))]
595 {
596 self.set_icon(icon)?;
597 }
598 Ok(())
599 }
600
601 pub fn set_show_menu_on_left_click(&self, #[allow(unused)] enable: bool) -> crate::Result<()> {
608 #[cfg(any(target_os = "macos", windows))]
609 run_item_main_thread!(self, |self_: Self| {
610 self_.inner.set_show_menu_on_left_click(enable)
611 })?;
612 Ok(())
613 }
614
615 pub fn rect(&self) -> crate::Result<Option<crate::Rect>> {
621 run_item_main_thread!(self, |self_: Self| {
622 self_.inner.rect().map(|rect| Rect {
623 position: rect.position.into(),
624 size: rect.size.into(),
625 })
626 })
627 }
628
629 pub fn with_inner_tray_icon<F, T>(&self, f: F) -> crate::Result<T>
634 where
635 F: FnOnce(&tray_icon::TrayIcon) -> T + Send + 'static,
636 T: Send + 'static,
637 {
638 run_item_main_thread!(self, |self_: Self| { f(&self_.inner) })
639 }
640}
641
642impl<R: Runtime> Resource for TrayIcon<R> {
643 fn close(self: std::sync::Arc<Self>) {
644 let mut icons = self.app_handle.manager.tray.icons.lock().unwrap();
645 for (i, (tray_icon_id, _rid)) in icons.iter_mut().enumerate() {
646 if tray_icon_id == &self.id {
647 icons.swap_remove(i);
648 return;
649 }
650 }
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 #[test]
657 fn tray_event_json_serialization() {
658 use super::*;
661 let event = TrayIconEvent::Click {
662 button: MouseButton::Left,
663 button_state: MouseButtonState::Down,
664 id: TrayIconId::new("id"),
665 position: crate::PhysicalPosition::default(),
666 rect: crate::Rect {
667 position: tray_icon::Rect::default().position.into(),
668 size: tray_icon::Rect::default().size.into(),
669 },
670 };
671
672 let value = serde_json::to_value(&event).unwrap();
673 assert_eq!(
674 value,
675 serde_json::json!({
676 "type": "Click",
677 "button": "Left",
678 "buttonState": "Down",
679 "id": "id",
680 "position": {
681 "x": 0.0,
682 "y": 0.0,
683 },
684 "rect": {
685 "size": {
686 "Physical": {
687 "width": 0,
688 "height": 0,
689 }
690 },
691 "position": {
692 "Physical": {
693 "x": 0,
694 "y": 0,
695 }
696 },
697 }
698 })
699 );
700 }
701}