tray_icon_ex/
lib.rs

1// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5#![allow(clippy::uninlined_format_args)]
6
7//! tray-icon lets you create tray icons for desktop applications.
8//!
9//! # Platforms supported:
10//!
11//! - Windows
12//! - macOS
13//! - Linux (gtk Only)
14//!
15//! # Platform-specific notes:
16//!
17//! - On Windows and Linux, an event loop must be running on the thread, on Windows, a win32 event loop and on Linux, a gtk event loop. It doesn't need to be the main thread but you have to create the tray icon on the same thread as the event loop.
18//! - On macOS, an event loop must be running on the main thread so you also need to create the tray icon on the main thread.
19//!
20//! # Dependencies (Linux Only)
21//!
22//! On Linux, `gtk`, `libxdo` is used to make the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu items work and `libappindicator` or `libayatnat-appindicator` are used to create the tray icon, so make sure to install them on your system.
23//!
24//! #### Arch Linux / Manjaro:
25//!
26//! ```sh
27//! pacman -S gtk3 xdotool libappindicator-gtk3 #or libayatana-appindicator
28//! ```
29//!
30//! #### Debian / Ubuntu:
31//!
32//! ```sh
33//! sudo apt install libgtk-3-dev libxdo-dev libappindicator3-dev #or libayatana-appindicator3-dev
34//! ```
35//!
36//! # Examples
37//!
38//! #### Create a tray icon without a menu.
39//!
40//! ```no_run
41//! use tray_icon_ex::{TrayIconBuilder, Icon};
42//!
43//! # let icon = Icon::from_rgba(Vec::new(), 0, 0).unwrap();
44//! let tray_icon = TrayIconBuilder::new()
45//!     .with_tooltip("system-tray - tray icon library!")
46//!     .with_icon(icon)
47//!     .build()
48//!     .unwrap();
49//! ```
50//!
51//! #### Create a tray icon with a menu.
52//!
53//! ```no_run
54//! use tray_icon_ex::{TrayIconBuilder, menu::Menu,Icon};
55//!
56//! # let icon = Icon::from_rgba(Vec::new(), 0, 0).unwrap();
57//! let tray_menu = Menu::new();
58//! let tray_icon = TrayIconBuilder::new()
59//!     .with_menu(Box::new(tray_menu))
60//!     .with_tooltip("system-tray - tray icon library!")
61//!     .with_icon(icon)
62//!     .build()
63//!     .unwrap();
64//! ```
65//!
66//! # Processing tray events
67//!
68//! You can use [`TrayIconEvent::receiver`] to get a reference to the [`TrayIconEventReceiver`]
69//! which you can use to listen to events when a click happens on the tray icon
70//! ```no_run
71//! use tray_icon_ex::TrayIconEvent;
72//!
73//! if let Ok(event) = TrayIconEvent::receiver().try_recv() {
74//!     println!("{:?}", event);
75//! }
76//! ```
77//!
78//! You can also listen for the menu events using [`MenuEvent::receiver`](crate::menu::MenuEvent::receiver) to get events for the tray context menu.
79//!
80//! ```no_run
81//! use tray_icon_ex::{TrayIconEvent, menu::MenuEvent};
82//!
83//! if let Ok(event) = TrayIconEvent::receiver().try_recv() {
84//!     println!("tray event: {:?}", event);
85//! }
86//!
87//! if let Ok(event) = MenuEvent::receiver().try_recv() {
88//!     println!("menu event: {:?}", event);
89//! }
90//! ```
91
92use std::{
93    cell::RefCell,
94    path::{Path, PathBuf},
95    rc::Rc,
96};
97
98use counter::Counter;
99use crossbeam_channel::{unbounded, Receiver, Sender};
100use once_cell::sync::{Lazy, OnceCell};
101
102mod counter;
103mod error;
104mod icon;
105mod platform_impl;
106mod tray_icon_id;
107
108pub use self::error::*;
109pub use self::icon::{BadIcon, Icon};
110pub use self::tray_icon_id::TrayIconId;
111
112/// Re-export of [muda](::muda) crate and used for tray context menu.
113pub mod menu {
114    pub use muda::*;
115}
116
117static COUNTER: Counter = Counter::new();
118
119/// Attributes to use when creating a tray icon.
120pub struct TrayIconAttributes {
121    /// Tray icon tooltip
122    ///
123    /// ## Platform-specific:
124    ///
125    /// - **Linux:** Unsupported.
126    pub tooltip: Option<String>,
127
128    /// Tray menu
129    ///
130    /// ## Platform-specific:
131    ///
132    /// - **Linux**: once a menu is set, it cannot be removed.
133    pub menu: Option<Box<dyn menu::ContextMenu>>,
134
135    /// Tray icon
136    ///
137    /// ## Platform-specific:
138    ///
139    /// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
140    ///     Setting an empty [`Menu`](crate::menu::Menu) is enough.
141    pub icon: Option<Icon>,
142
143    /// Tray icon temp dir path. **Linux only**.
144    pub temp_dir_path: Option<PathBuf>,
145
146    /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
147    pub icon_is_template: bool,
148
149    /// Whether to show the tray menu on left click or not, default is `true`. **macOS only**.
150    pub menu_on_left_click: bool,
151
152    /// Tray icon title.
153    ///
154    /// ## Platform-specific
155    ///
156    /// - **Linux:** The title will not be shown unless there is an icon
157    /// as well.  The title is useful for numerical and other frequently
158    /// updated information.  In general, it shouldn't be shown unless a
159    /// user requests it as it can take up a significant amount of space
160    /// on the user's panel.  This may not be shown in all visualizations.
161    /// - **Windows:** Unsupported.
162    pub title: Option<String>,
163}
164
165impl Default for TrayIconAttributes {
166    fn default() -> Self {
167        Self {
168            tooltip: None,
169            menu: None,
170            icon: None,
171            temp_dir_path: None,
172            icon_is_template: false,
173            menu_on_left_click: true,
174            title: None,
175        }
176    }
177}
178
179/// [`TrayIcon`] builder struct and associated methods.
180#[derive(Default)]
181pub struct TrayIconBuilder {
182    id: TrayIconId,
183    attrs: TrayIconAttributes,
184}
185
186impl TrayIconBuilder {
187    /// Creates a new [`TrayIconBuilder`] with default [`TrayIconAttributes`].
188    ///
189    /// See [`TrayIcon::new`] for more info.
190    pub fn new() -> Self {
191        Self {
192            id: TrayIconId(COUNTER.next().to_string()),
193            attrs: TrayIconAttributes::default(),
194        }
195    }
196
197    /// Sets the unique id to build the tray icon with.
198    pub fn with_id<I: Into<TrayIconId>>(mut self, id: I) -> Self {
199        self.id = id.into();
200        self
201    }
202
203    /// Set the a menu for this tray icon.
204    ///
205    /// ## Platform-specific:
206    ///
207    /// - **Linux**: once a menu is set, it cannot be removed or replaced but you can change its content.
208    pub fn with_menu(mut self, menu: Box<dyn menu::ContextMenu>) -> Self {
209        self.attrs.menu = Some(menu);
210        self
211    }
212
213    /// Set an icon for this tray icon.
214    ///
215    /// ## Platform-specific:
216    ///
217    /// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
218    /// Setting an empty [`Menu`](crate::menu::Menu) is enough.
219    pub fn with_icon(mut self, icon: Icon) -> Self {
220        self.attrs.icon = Some(icon);
221        self
222    }
223
224    /// Set a tooltip for this tray icon.
225    ///
226    /// ## Platform-specific:
227    ///
228    /// - **Linux:** Unsupported.
229    pub fn with_tooltip<S: AsRef<str>>(mut self, s: S) -> Self {
230        self.attrs.tooltip = Some(s.as_ref().to_string());
231        self
232    }
233
234    /// Set the tray icon title.
235    ///
236    /// ## Platform-specific
237    ///
238    /// - **Linux:** The title will not be shown unless there is an icon
239    /// as well.  The title is useful for numerical and other frequently
240    /// updated information.  In general, it shouldn't be shown unless a
241    /// user requests it as it can take up a significant amount of space
242    /// on the user's panel.  This may not be shown in all visualizations.
243    /// - **Windows:** Unsupported.
244    pub fn with_title<S: AsRef<str>>(mut self, title: S) -> Self {
245        self.attrs.title.replace(title.as_ref().to_string());
246        self
247    }
248
249    /// Set tray icon temp dir path. **Linux only**.
250    ///
251    /// On Linux, we need to write the icon to the disk and usually it will
252    /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
253    pub fn with_temp_dir_path<P: AsRef<Path>>(mut self, s: P) -> Self {
254        self.attrs.temp_dir_path = Some(s.as_ref().to_path_buf());
255        self
256    }
257
258    /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
259    pub fn with_icon_as_template(mut self, is_template: bool) -> Self {
260        self.attrs.icon_is_template = is_template;
261        self
262    }
263
264    /// Whether to show the tray menu on left click or not, default is `true`. **macOS only**.
265    pub fn with_menu_on_left_click(mut self, enable: bool) -> Self {
266        self.attrs.menu_on_left_click = enable;
267        self
268    }
269
270    /// Access the unique id that will be assigned to the tray icon
271    /// this builder will create.
272    pub fn id(&self) -> &TrayIconId {
273        &self.id
274    }
275
276    /// Builds and adds a new [`TrayIcon`] to the system tray.
277    pub fn build(self) -> Result<TrayIcon> {
278        TrayIcon::with_id(self.id, self.attrs)
279    }
280}
281
282/// Tray icon struct and associated methods.
283///
284/// This type is reference-counted and the icon is removed when the last instance is dropped.
285#[derive(Clone)]
286pub struct TrayIcon {
287    id: TrayIconId,
288    tray: Rc<RefCell<platform_impl::TrayIcon>>,
289}
290
291impl TrayIcon {
292    /// Builds and adds a new tray icon to the system tray.
293    ///
294    /// ## Platform-specific:
295    ///
296    /// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
297    /// Setting an empty [`Menu`](crate::menu::Menu) is enough.
298    pub fn new(attrs: TrayIconAttributes) -> Result<Self> {
299        let id = TrayIconId(COUNTER.next().to_string());
300        Ok(Self {
301            tray: Rc::new(RefCell::new(platform_impl::TrayIcon::new(
302                id.clone(),
303                attrs,
304            )?)),
305            id,
306        })
307    }
308
309    /// Builds and adds a new tray icon to the system tray with the specified Id.
310    ///
311    /// See [`TrayIcon::new`] for more info.
312    pub fn with_id<I: Into<TrayIconId>>(id: I, attrs: TrayIconAttributes) -> Result<Self> {
313        let id = id.into();
314        Ok(Self {
315            tray: Rc::new(RefCell::new(platform_impl::TrayIcon::new(
316                id.clone(),
317                attrs,
318            )?)),
319            id,
320        })
321    }
322
323    /// Returns the id associated with this tray icon.
324    pub fn id(&self) -> &TrayIconId {
325        &self.id
326    }
327
328    /// Set new tray icon. If `None` is provided, it will remove the icon.
329    pub fn set_icon(&self, icon: Option<Icon>) -> Result<()> {
330        self.tray.borrow_mut().set_icon(icon)
331    }
332
333    /// Set new tray menu.
334    ///
335    /// ## Platform-specific:
336    ///
337    /// - **Linux**: once a menu is set it cannot be removed so `None` has no effect
338    pub fn set_menu(&self, menu: Option<Box<dyn menu::ContextMenu>>) {
339        self.tray.borrow_mut().set_menu(menu)
340    }
341
342    /// Sets the tooltip for this tray icon.
343    ///
344    /// ## Platform-specific:
345    ///
346    /// - **Linux:** Unsupported
347    pub fn set_tooltip<S: AsRef<str>>(&self, tooltip: Option<S>) -> Result<()> {
348        self.tray.borrow_mut().set_tooltip(tooltip)
349    }
350
351    /// Sets the tooltip for this tray icon.
352    ///
353    /// ## Platform-specific:
354    ///
355    /// - **Linux:** The title will not be shown unless there is an icon
356    /// as well.  The title is useful for numerical and other frequently
357    /// updated information.  In general, it shouldn't be shown unless a
358    /// user requests it as it can take up a significant amount of space
359    /// on the user's panel.  This may not be shown in all visualizations.
360    /// - **Windows:** Unsupported
361    pub fn set_title<S: AsRef<str>>(&self, title: Option<S>) {
362        self.tray.borrow_mut().set_title(title)
363    }
364
365    /// Show or hide this tray icon
366    pub fn set_visible(&self, visible: bool) -> Result<()> {
367        self.tray.borrow_mut().set_visible(visible)
368    }
369
370    /// Sets the tray icon temp dir path. **Linux only**.
371    ///
372    /// On Linux, we need to write the icon to the disk and usually it will
373    /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
374    pub fn set_temp_dir_path<P: AsRef<Path>>(&self, path: Option<P>) {
375        #[cfg(target_os = "linux")]
376        self.tray.borrow_mut().set_temp_dir_path(path);
377        #[cfg(not(target_os = "linux"))]
378        let _ = path;
379    }
380
381    /// Set the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
382    pub fn set_icon_as_template(&self, is_template: bool) {
383        #[cfg(target_os = "macos")]
384        self.tray.borrow_mut().set_icon_as_template(is_template);
385        #[cfg(not(target_os = "macos"))]
386        let _ = is_template;
387    }
388
389    /// Disable or enable showing the tray menu on left click. **macOS only**.
390    pub fn set_show_menu_on_left_click(&self, enable: bool) {
391        #[cfg(target_os = "macos")]
392        self.tray.borrow_mut().set_show_menu_on_left_click(enable);
393        #[cfg(not(target_os = "macos"))]
394        let _ = enable;
395    }
396
397    pub fn is_dark_mode(&self) -> bool {
398        #[cfg(target_os = "macos")]
399        return self.tray.borrow_mut().is_dark_mode();
400
401        #[cfg(not(target_os = "macos"))]
402        return false;
403    }
404}
405
406/// Describes a tray event emitted when a tray icon is clicked
407///
408/// ## Platform-specific:
409///
410/// - **Linux**: Unsupported. The event is not emmited even though the icon is shown,
411/// the icon will still show a context menu on right click.
412#[derive(Debug, Clone, Default)]
413#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
414pub struct TrayIconEvent {
415    /// Id of the tray icon which triggered this event.
416    pub id: TrayIconId,
417    /// Physical X Position of the click the triggered this event.
418    pub x: f64,
419    /// Physical Y Position of the click the triggered this event.
420    pub y: f64,
421    /// Position and size of the tray icon
422    pub icon_rect: Rectangle,
423    /// The click type that triggered this event.
424    pub click_type: ClickType,
425}
426
427#[derive(Clone, Copy, PartialEq, Eq, Debug)]
428#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
429pub enum ClickType {
430    Left,
431    Right,
432    Double,
433}
434
435impl Default for ClickType {
436    fn default() -> Self {
437        Self::Left
438    }
439}
440
441/// Describes a rectangle including position (x - y axis) and size.
442#[derive(Debug, PartialEq, Clone, Copy, Default)]
443#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
444pub struct Rectangle {
445    pub left: f64,
446    pub right: f64,
447    pub top: f64,
448    pub bottom: f64,
449}
450
451/// A reciever that could be used to listen to tray events.
452pub type TrayIconEventReceiver = Receiver<TrayIconEvent>;
453type TrayIconEventHandler = Box<dyn Fn(TrayIconEvent) + Send + Sync + 'static>;
454
455static TRAY_CHANNEL: Lazy<(Sender<TrayIconEvent>, TrayIconEventReceiver)> = Lazy::new(unbounded);
456static TRAY_EVENT_HANDLER: OnceCell<Option<TrayIconEventHandler>> = OnceCell::new();
457
458impl TrayIconEvent {
459    /// Returns the id of the tray icon which triggered this event.
460    pub fn id(&self) -> &TrayIconId {
461        &self.id
462    }
463
464    /// Gets a reference to the event channel's [`TrayIconEventReceiver`]
465    /// which can be used to listen for tray events.
466    ///
467    /// ## Note
468    ///
469    /// This will not receive any events if [`TrayIconEvent::set_event_handler`] has been called with a `Some` value.
470    pub fn receiver<'a>() -> &'a TrayIconEventReceiver {
471        &TRAY_CHANNEL.1
472    }
473
474    /// Set a handler to be called for new events. Useful for implementing custom event sender.
475    ///
476    /// ## Note
477    ///
478    /// Calling this function with a `Some` value,
479    /// will not send new events to the channel associated with [`TrayIconEvent::receiver`]
480    pub fn set_event_handler<F: Fn(TrayIconEvent) + Send + Sync + 'static>(f: Option<F>) {
481        if let Some(f) = f {
482            let _ = TRAY_EVENT_HANDLER.set(Some(Box::new(f)));
483        } else {
484            let _ = TRAY_EVENT_HANDLER.set(None);
485        }
486    }
487
488    #[allow(unused)]
489    pub(crate) fn send(event: TrayIconEvent) {
490        if let Some(handler) = TRAY_EVENT_HANDLER.get_or_init(|| None) {
491            handler(event);
492        } else {
493            let _ = TRAY_CHANNEL.0.send(event);
494        }
495    }
496}