Skip to main content

tauri/tray/
mod.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Tray icon types and utilities.
6
7pub(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/// Describes the mouse button state.
22#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Serialize)]
23pub enum MouseButtonState {
24  /// Mouse button pressed.
25  #[default]
26  Up,
27  /// Mouse button released.
28  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/// Describes which mouse button triggered the event..
41#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Default)]
42pub enum MouseButton {
43  /// Left mouse button.
44  #[default]
45  Left,
46  /// Right mouse button.
47  Right,
48  /// Middle mouse button.
49  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/// Describes a tray icon event.
63///
64/// ## Platform-specific:
65///
66/// - **Linux**: Unsupported. The event is not emitted even though the icon is shown
67///   and will still show a context menu on right click.
68#[derive(Debug, Clone, Serialize)]
69#[serde(tag = "type")]
70#[non_exhaustive]
71pub enum TrayIconEvent {
72  /// A click happened on the tray icon.
73  #[serde(rename_all = "camelCase")]
74  Click {
75    /// Id of the tray icon which triggered this event.
76    id: TrayIconId,
77    /// Physical Position of this event.
78    position: PhysicalPosition<f64>,
79    /// Position and size of the tray icon.
80    rect: Rect,
81    /// Mouse button that triggered this event.
82    button: MouseButton,
83    /// Mouse button state when this event was triggered.
84    button_state: MouseButtonState,
85  },
86  /// A double click happened on the tray icon. **Windows Only**
87  DoubleClick {
88    /// Id of the tray icon which triggered this event.
89    id: TrayIconId,
90    /// Physical Position of this event.
91    position: PhysicalPosition<f64>,
92    /// Position and size of the tray icon.
93    rect: Rect,
94    /// Mouse button that triggered this event.
95    button: MouseButton,
96  },
97  /// The mouse entered the tray icon region.
98  Enter {
99    /// Id of the tray icon which triggered this event.
100    id: TrayIconId,
101    /// Physical Position of this event.
102    position: PhysicalPosition<f64>,
103    /// Position and size of the tray icon.
104    rect: Rect,
105  },
106  /// The mouse moved over the tray icon region.
107  Move {
108    /// Id of the tray icon which triggered this event.
109    id: TrayIconId,
110    /// Physical Position of this event.
111    position: PhysicalPosition<f64>,
112    /// Position and size of the tray icon.
113    rect: Rect,
114  },
115  /// The mouse left the tray icon region.
116  Leave {
117    /// Id of the tray icon which triggered this event.
118    id: TrayIconId,
119    /// Physical Position of this event.
120    position: PhysicalPosition<f64>,
121    /// Position and size of the tray icon.
122    rect: Rect,
123  },
124}
125
126impl TrayIconEvent {
127  /// Get the id of the tray icon that triggered this event.
128  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/// [`TrayIcon`] builder struct and associated methods.
202#[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  /// Creates a new tray icon builder.
211  ///
212  /// ## Platform-specific:
213  ///
214  /// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
215  ///   Setting an empty [`Menu`](crate::menu::Menu) is enough.
216  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  /// Creates a new tray icon builder with the specified id.
225  ///
226  /// ## Platform-specific:
227  ///
228  /// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
229  ///   Setting an empty [`Menu`](crate::menu::Menu) is enough.
230  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  /// Set the a menu for this tray icon.
237  ///
238  /// ## Platform-specific:
239  ///
240  /// - **Linux**: once a menu is set, it cannot be removed or replaced but you can change its content.
241  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  /// Set an icon for this tray icon.
247  ///
248  /// ## Platform-specific:
249  ///
250  /// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
251  ///   Setting an empty [`Menu`](crate::menu::Menu) is enough.
252  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  /// Set a tooltip for this tray icon.
261  ///
262  /// ## Platform-specific:
263  ///
264  /// - **Linux:** Unsupported.
265  pub fn tooltip<S: AsRef<str>>(mut self, s: S) -> Self {
266    self.inner = self.inner.with_tooltip(s);
267    self
268  }
269
270  /// Set the tray icon title.
271  ///
272  /// ## Platform-specific
273  ///
274  /// - **Linux:** The title will not be shown unless there is an icon
275  ///   as well.  The title is useful for numerical and other frequently
276  ///   updated information.  In general, it shouldn't be shown unless a
277  ///   user requests it as it can take up a significant amount of space
278  ///   on the user's panel.  This may not be shown in all visualizations.
279  /// - **Windows:** Unsupported.
280  pub fn title<S: AsRef<str>>(mut self, title: S) -> Self {
281    self.inner = self.inner.with_title(title);
282    self
283  }
284
285  /// Set tray icon temp dir path. **Linux only**.
286  ///
287  /// On Linux, we need to write the icon to the disk and usually it will
288  /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
289  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  /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
295  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  /// Whether to show the tray menu on left click or not, default is `true`.
301  ///
302  /// ## Platform-specific:
303  ///
304  /// - **Linux:** Unsupported.
305  #[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  /// Whether to show the tray menu on left click or not, default is `true`.
315  ///
316  /// ## Platform-specific:
317  ///
318  /// - **Linux:** Unsupported.
319  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  /// Set a handler for menu events.
325  ///
326  /// Note that this handler is called for any menu event,
327  /// whether it is coming from this window, another window or from the tray icon menu.
328  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  /// Set a handler for this tray icon events.
337  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  /// Access the unique id that will be assigned to the tray icon
346  /// this builder will create.
347  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    // SAFETY:
358    // the menu within this builder was created on main thread
359    // and will be accessed on the main thread
360    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        // SAFETY: will only be accessed on main thread
366        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  /// Builds and adds a new [`TrayIcon`] to the system tray.
386  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/// Tray icon struct and associated methods.
393///
394/// This type is reference-counted and the icon is removed when the last instance is dropped.
395///
396/// See [TrayIconBuilder] to construct this type.
397#[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
414/// # Safety
415///
416/// We make sure it always runs on the main thread.
417unsafe 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  /// The application handle associated with this type.
459  pub fn app_handle(&self) -> &AppHandle<R> {
460    &self.app_handle
461  }
462
463  /// Register a handler for menu events.
464  ///
465  /// Note that this handler is called for any menu event,
466  /// whether it is coming from this window, another window or from the tray icon menu.
467  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  /// Register a handler for this tray icon events.
479  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  /// Returns the id associated with this tray icon.
494  pub fn id(&self) -> &TrayIconId {
495    &self.id
496  }
497
498  /// Sets a new tray icon. If `None` is provided, it will remove the icon.
499  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  /// Sets a new tray menu.
508  ///
509  /// ## Platform-specific:
510  ///
511  /// - **Linux**: once a menu is set it cannot be removed so `None` has no effect
512  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  /// Sets the tooltip for this tray icon.
519  ///
520  /// ## Platform-specific:
521  ///
522  /// - **Linux:** Unsupported
523  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  /// Sets the title for this tray icon.
529  ///
530  /// ## Platform-specific:
531  ///
532  /// - **Linux:** The title will not be shown unless there is an icon
533  ///   as well.  The title is useful for numerical and other frequently
534  ///   updated information.  In general, it shouldn't be shown unless a
535  ///   user requests it as it can take up a significant amount of space
536  ///   on the user's panel.  This may not be shown in all visualizations.
537  /// - **Windows:** Unsupported
538  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  /// Show or hide this tray icon.
544  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  /// Sets the tray icon temp dir path. **Linux only**.
549  ///
550  /// On Linux, we need to write the icon to the disk and usually it will
551  /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
552  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  /// Sets the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
561  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  /// Sets the tray icon and template status atomically. **macOS only**.
570  ///
571  /// On macOS, calling `set_icon` followed by `set_icon_as_template` causes a visible
572  /// flicker as the icon is rendered twice. This method sets both atomically to prevent that.
573  ///
574  /// ## Platform-specific:
575  ///
576  /// - **Linux / Windows:** Falls back to calling `set_icon`.
577  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  /// Disable or enable showing the tray menu on left click.
602  ///
603  ///
604  /// ## Platform-specific:
605  ///
606  /// - **Linux**: Unsupported.
607  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  /// Get tray icon rect.
616  ///
617  /// ## Platform-specific:
618  ///
619  /// - **Linux**: Unsupported, always returns `None`.
620  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  /// Do something with the inner [`tray_icon::TrayIcon`] on main thread
630  ///
631  /// Note that `tray-icon` crate may be updated in minor releases of Tauri.
632  /// Therefore, it’s recommended to pin Tauri to at least a minor version when you’re using `with_inner_tray_icon`.
633  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    // NOTE: if this test is ever changed, you probably need to change `TrayIconEvent` in JS as well
659
660    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}