tray_icon/
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. You must make sure that the event loop is already running and not just created before creating a TrayIcon to prevent issues with fullscreen apps. In Winit for example the earliest you can create icons is on [`StartCause::Init`](https://docs.rs/winit/latest/winit/event/enum.StartCause.html#variant.Init).
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::{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::{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::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::{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//!
92//! ### Note for [winit] or [tao] users:
93//!
94//! You should use [`TrayIconEvent::set_event_handler`] and forward
95//! the tray icon events to the event loop by using [`EventLoopProxy`]
96//! so that the event loop is awakened on each tray icon event.
97//! Same can be done for menu events using [`MenuEvent::set_event_handler`].
98//!
99//! ```no_run
100//! # use winit::event_loop::EventLoop;
101//! enum UserEvent {
102//!   TrayIconEvent(tray_icon::TrayIconEvent),
103//!   MenuEvent(tray_icon::menu::MenuEvent)
104//! }
105//!
106//! let event_loop = EventLoop::<UserEvent>::with_user_event().build().unwrap();
107//!
108//! let proxy = event_loop.create_proxy();
109//! tray_icon::TrayIconEvent::set_event_handler(Some(move |event| {
110//!     proxy.send_event(UserEvent::TrayIconEvent(event));
111//! }));
112//!
113//! let proxy = event_loop.create_proxy();
114//! tray_icon::menu::MenuEvent::set_event_handler(Some(move |event| {
115//!     proxy.send_event(UserEvent::MenuEvent(event));
116//! }));
117//! ```
118//!
119//! [`EventLoopProxy`]: https://docs.rs/winit/latest/winit/event_loop/struct.EventLoopProxy.html
120//! [winit]: https://docs.rs/winit
121//! [tao]: https://docs.rs/tao
122
123use std::{
124    cell::RefCell,
125    path::{Path, PathBuf},
126    rc::Rc,
127};
128
129use counter::Counter;
130use crossbeam_channel::{unbounded, Receiver, Sender};
131use once_cell::sync::{Lazy, OnceCell};
132
133mod counter;
134mod error;
135mod icon;
136mod platform_impl;
137mod tray_icon_id;
138
139pub use self::error::*;
140pub use self::icon::{BadIcon, Icon};
141pub use self::tray_icon_id::TrayIconId;
142
143/// Re-export of [muda](::muda) crate and used for tray context menu.
144pub mod menu {
145    pub use muda::*;
146}
147pub use muda::dpi;
148
149static COUNTER: Counter = Counter::new();
150
151/// Attributes to use when creating a tray icon.
152pub struct TrayIconAttributes {
153    /// Tray icon tooltip
154    ///
155    /// ## Platform-specific:
156    ///
157    /// - **Linux:** Unsupported.
158    pub tooltip: Option<String>,
159
160    /// Tray menu
161    ///
162    /// ## Platform-specific:
163    ///
164    /// - **Linux**: once a menu is set, it cannot be removed.
165    pub menu: Option<Box<dyn menu::ContextMenu>>,
166
167    /// Tray icon
168    ///
169    /// ## Platform-specific:
170    ///
171    /// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
172    ///     Setting an empty [`Menu`](crate::menu::Menu) is enough.
173    pub icon: Option<Icon>,
174
175    /// Tray icon temp dir path. **Linux only**.
176    pub temp_dir_path: Option<PathBuf>,
177
178    /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
179    pub icon_is_template: bool,
180
181    /// Whether to show the tray menu on left click or not, default is `true`. **macOS & Windows only**.
182    pub menu_on_left_click: bool,
183
184    /// Tray icon title.
185    ///
186    /// ## Platform-specific
187    ///
188    /// - **Linux:** The title will not be shown unless there is an icon
189    ///   as well.  The title is useful for numerical and other frequently
190    ///   updated information.  In general, it shouldn't be shown unless a
191    ///   user requests it as it can take up a significant amount of space
192    ///   on the user's panel.  This may not be shown in all visualizations.
193    /// - **Windows:** Unsupported.
194    pub title: Option<String>,
195}
196
197impl Default for TrayIconAttributes {
198    fn default() -> Self {
199        Self {
200            tooltip: None,
201            menu: None,
202            icon: None,
203            temp_dir_path: None,
204            icon_is_template: false,
205            menu_on_left_click: true,
206            title: None,
207        }
208    }
209}
210
211/// [`TrayIcon`] builder struct and associated methods.
212#[derive(Default)]
213pub struct TrayIconBuilder {
214    id: TrayIconId,
215    attrs: TrayIconAttributes,
216}
217
218impl TrayIconBuilder {
219    /// Creates a new [`TrayIconBuilder`] with default [`TrayIconAttributes`].
220    ///
221    /// See [`TrayIcon::new`] for more info.
222    pub fn new() -> Self {
223        Self {
224            id: TrayIconId(COUNTER.next().to_string()),
225            attrs: TrayIconAttributes::default(),
226        }
227    }
228
229    /// Sets the unique id to build the tray icon with.
230    pub fn with_id<I: Into<TrayIconId>>(mut self, id: I) -> Self {
231        self.id = id.into();
232        self
233    }
234
235    /// Set the a menu for this tray icon.
236    ///
237    /// ## Platform-specific:
238    ///
239    /// - **Linux**: once a menu is set, it cannot be removed or replaced but you can change its content.
240    pub fn with_menu(mut self, menu: Box<dyn menu::ContextMenu>) -> Self {
241        self.attrs.menu = Some(menu);
242        self
243    }
244
245    /// Set an icon for this tray icon.
246    ///
247    /// ## Platform-specific:
248    ///
249    /// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
250    ///   Setting an empty [`Menu`](crate::menu::Menu) is enough.
251    pub fn with_icon(mut self, icon: Icon) -> Self {
252        self.attrs.icon = Some(icon);
253        self
254    }
255
256    /// Set a tooltip for this tray icon.
257    ///
258    /// ## Platform-specific:
259    ///
260    /// - **Linux:** Unsupported.
261    pub fn with_tooltip<S: AsRef<str>>(mut self, s: S) -> Self {
262        self.attrs.tooltip = Some(s.as_ref().to_string());
263        self
264    }
265
266    /// Set the tray icon title.
267    ///
268    /// ## Platform-specific
269    ///
270    /// - **Linux:** The title will not be shown unless there is an icon
271    ///   as well.  The title is useful for numerical and other frequently
272    ///   updated information.  In general, it shouldn't be shown unless a
273    ///   user requests it as it can take up a significant amount of space
274    ///   on the user's panel.  This may not be shown in all visualizations.
275    /// - **Windows:** Unsupported.
276    pub fn with_title<S: AsRef<str>>(mut self, title: S) -> Self {
277        self.attrs.title.replace(title.as_ref().to_string());
278        self
279    }
280
281    /// Set tray icon temp dir path. **Linux only**.
282    ///
283    /// On Linux, we need to write the icon to the disk and usually it will
284    /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
285    pub fn with_temp_dir_path<P: AsRef<Path>>(mut self, s: P) -> Self {
286        self.attrs.temp_dir_path = Some(s.as_ref().to_path_buf());
287        self
288    }
289
290    /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
291    pub fn with_icon_as_template(mut self, is_template: bool) -> Self {
292        self.attrs.icon_is_template = is_template;
293        self
294    }
295
296    /// Whether to show the tray menu on left click or not, default is `true`. **macOS only**.
297    pub fn with_menu_on_left_click(mut self, enable: bool) -> Self {
298        self.attrs.menu_on_left_click = enable;
299        self
300    }
301
302    /// Access the unique id that will be assigned to the tray icon
303    /// this builder will create.
304    pub fn id(&self) -> &TrayIconId {
305        &self.id
306    }
307
308    /// Builds and adds a new [`TrayIcon`] to the system tray.
309    pub fn build(self) -> Result<TrayIcon> {
310        TrayIcon::with_id(self.id, self.attrs)
311    }
312}
313
314/// Tray icon struct and associated methods.
315///
316/// This type is reference-counted and the icon is removed when the last instance is dropped.
317#[derive(Clone)]
318pub struct TrayIcon {
319    id: TrayIconId,
320    tray: Rc<RefCell<platform_impl::TrayIcon>>,
321}
322
323impl TrayIcon {
324    /// Builds and adds a new tray icon to the system tray.
325    ///
326    /// ## Platform-specific:
327    ///
328    /// - **Linux:** Sometimes the icon won't be visible unless a menu is set.
329    ///   Setting an empty [`Menu`](crate::menu::Menu) is enough.
330    pub fn new(attrs: TrayIconAttributes) -> Result<Self> {
331        let id = TrayIconId(COUNTER.next().to_string());
332        Ok(Self {
333            tray: Rc::new(RefCell::new(platform_impl::TrayIcon::new(
334                id.clone(),
335                attrs,
336            )?)),
337            id,
338        })
339    }
340
341    /// Builds and adds a new tray icon to the system tray with the specified Id.
342    ///
343    /// See [`TrayIcon::new`] for more info.
344    pub fn with_id<I: Into<TrayIconId>>(id: I, attrs: TrayIconAttributes) -> Result<Self> {
345        let id = id.into();
346        Ok(Self {
347            tray: Rc::new(RefCell::new(platform_impl::TrayIcon::new(
348                id.clone(),
349                attrs,
350            )?)),
351            id,
352        })
353    }
354
355    /// Returns the id associated with this tray icon.
356    pub fn id(&self) -> &TrayIconId {
357        &self.id
358    }
359
360    /// Set new tray icon. If `None` is provided, it will remove the icon.
361    pub fn set_icon(&self, icon: Option<Icon>) -> Result<()> {
362        self.tray.borrow_mut().set_icon(icon)
363    }
364
365    /// Set new tray menu.
366    ///
367    /// ## Platform-specific:
368    ///
369    /// - **Linux**: once a menu is set it cannot be removed so `None` has no effect
370    pub fn set_menu(&self, menu: Option<Box<dyn menu::ContextMenu>>) {
371        self.tray.borrow_mut().set_menu(menu)
372    }
373
374    /// Sets the tooltip for this tray icon.
375    ///
376    /// ## Platform-specific:
377    ///
378    /// - **Linux:** Unsupported
379    pub fn set_tooltip<S: AsRef<str>>(&self, tooltip: Option<S>) -> Result<()> {
380        self.tray.borrow_mut().set_tooltip(tooltip)
381    }
382
383    /// Sets the tooltip for this tray icon.
384    ///
385    /// ## Platform-specific:
386    ///
387    /// - **Linux:** The title will not be shown unless there is an icon
388    ///   as well.  The title is useful for numerical and other frequently
389    ///   updated information.  In general, it shouldn't be shown unless a
390    ///   user requests it as it can take up a significant amount of space
391    ///   on the user's panel.  This may not be shown in all visualizations.
392    /// - **Windows:** Unsupported
393    pub fn set_title<S: AsRef<str>>(&self, title: Option<S>) {
394        self.tray.borrow_mut().set_title(title)
395    }
396
397    /// Show or hide this tray icon
398    pub fn set_visible(&self, visible: bool) -> Result<()> {
399        self.tray.borrow_mut().set_visible(visible)
400    }
401
402    /// Sets the tray icon temp dir path. **Linux only**.
403    ///
404    /// On Linux, we need to write the icon to the disk and usually it will
405    /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
406    pub fn set_temp_dir_path<P: AsRef<Path>>(&self, path: Option<P>) {
407        #[cfg(target_os = "linux")]
408        self.tray.borrow_mut().set_temp_dir_path(path);
409        #[cfg(not(target_os = "linux"))]
410        let _ = path;
411    }
412
413    /// Set the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
414    pub fn set_icon_as_template(&self, is_template: bool) {
415        #[cfg(target_os = "macos")]
416        self.tray.borrow_mut().set_icon_as_template(is_template);
417        #[cfg(not(target_os = "macos"))]
418        let _ = is_template;
419    }
420
421    pub fn set_icon_with_as_template(&self, icon: Option<Icon>, is_template: bool) -> Result<()> {
422        #[cfg(target_os = "macos")]
423        return self
424            .tray
425            .borrow_mut()
426            .set_icon_with_as_template(icon, is_template);
427        #[cfg(not(target_os = "macos"))]
428        {
429            let _ = icon;
430            let _ = is_template;
431            Ok(())
432        }
433    }
434
435    /// Disable or enable showing the tray menu on left click.
436    ///
437    /// ## Platform-specific:
438    ///
439    /// - **Linux:** Unsupported.
440    pub fn set_show_menu_on_left_click(&self, enable: bool) {
441        #[cfg(any(target_os = "macos", target_os = "windows"))]
442        self.tray.borrow_mut().set_show_menu_on_left_click(enable);
443        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
444        let _ = enable;
445    }
446
447    /// Get tray icon rect.
448    ///
449    /// ## Platform-specific:
450    ///
451    /// - **Linux**: Unsupported.
452    pub fn rect(&self) -> Option<Rect> {
453        self.tray.borrow().rect()
454    }
455}
456
457/// Describes a tray icon event.
458///
459/// ## Platform-specific:
460///
461/// - **Linux**: Unsupported. The event is not emmited even though the icon is shown
462///   and will still show a context menu on right click.
463#[derive(Debug, Clone)]
464#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
465#[cfg_attr(feature = "serde", serde(tag = "type"))]
466#[non_exhaustive]
467pub enum TrayIconEvent {
468    /// A click happened on the tray icon.
469    #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
470    Click {
471        /// Id of the tray icon which triggered this event.
472        id: TrayIconId,
473        /// Physical Position of this event.
474        position: dpi::PhysicalPosition<f64>,
475        /// Position and size of the tray icon.
476        rect: Rect,
477        /// Mouse button that triggered this event.
478        button: MouseButton,
479        /// Mouse button state when this event was triggered.
480        button_state: MouseButtonState,
481    },
482    /// A double click happened on the tray icon. **Windows Only**
483    DoubleClick {
484        /// Id of the tray icon which triggered this event.
485        id: TrayIconId,
486        /// Physical Position of this event.
487        position: dpi::PhysicalPosition<f64>,
488        /// Position and size of the tray icon.
489        rect: Rect,
490        /// Mouse button that triggered this event.
491        button: MouseButton,
492    },
493    /// The mouse entered the tray icon region.
494    Enter {
495        /// Id of the tray icon which triggered this event.
496        id: TrayIconId,
497        /// Physical Position of this event.
498        position: dpi::PhysicalPosition<f64>,
499        /// Position and size of the tray icon.
500        rect: Rect,
501    },
502    /// The mouse moved over the tray icon region.
503    Move {
504        /// Id of the tray icon which triggered this event.
505        id: TrayIconId,
506        /// Physical Position of this event.
507        position: dpi::PhysicalPosition<f64>,
508        /// Position and size of the tray icon.
509        rect: Rect,
510    },
511    /// The mouse left the tray icon region.
512    Leave {
513        /// Id of the tray icon which triggered this event.
514        id: TrayIconId,
515        /// Physical Position of this event.
516        position: dpi::PhysicalPosition<f64>,
517        /// Position and size of the tray icon.
518        rect: Rect,
519    },
520}
521
522/// Describes the mouse button state.
523#[derive(Clone, Copy, PartialEq, Eq, Debug)]
524#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
525pub enum MouseButtonState {
526    Up,
527    Down,
528}
529
530impl Default for MouseButtonState {
531    fn default() -> Self {
532        Self::Up
533    }
534}
535
536/// Describes which mouse button triggered the event..
537#[derive(Clone, Copy, PartialEq, Eq, Debug)]
538#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
539pub enum MouseButton {
540    Left,
541    Right,
542    Middle,
543}
544
545impl Default for MouseButton {
546    fn default() -> Self {
547        Self::Left
548    }
549}
550
551/// Describes a rectangle including position (x - y axis) and size.
552#[derive(Debug, PartialEq, Clone, Copy)]
553#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
554pub struct Rect {
555    pub size: dpi::PhysicalSize<u32>,
556    pub position: dpi::PhysicalPosition<f64>,
557}
558
559impl Default for Rect {
560    fn default() -> Self {
561        Self {
562            size: dpi::PhysicalSize::new(0, 0),
563            position: dpi::PhysicalPosition::new(0., 0.),
564        }
565    }
566}
567
568/// A reciever that could be used to listen to tray events.
569pub type TrayIconEventReceiver = Receiver<TrayIconEvent>;
570type TrayIconEventHandler = Box<dyn Fn(TrayIconEvent) + Send + Sync + 'static>;
571
572static TRAY_CHANNEL: Lazy<(Sender<TrayIconEvent>, TrayIconEventReceiver)> = Lazy::new(unbounded);
573static TRAY_EVENT_HANDLER: OnceCell<Option<TrayIconEventHandler>> = OnceCell::new();
574
575impl TrayIconEvent {
576    /// Returns the id of the tray icon which triggered this event.
577    pub fn id(&self) -> &TrayIconId {
578        match self {
579            TrayIconEvent::Click { id, .. } => id,
580            TrayIconEvent::DoubleClick { id, .. } => id,
581            TrayIconEvent::Enter { id, .. } => id,
582            TrayIconEvent::Move { id, .. } => id,
583            TrayIconEvent::Leave { id, .. } => id,
584        }
585    }
586
587    /// Gets a reference to the event channel's [`TrayIconEventReceiver`]
588    /// which can be used to listen for tray events.
589    ///
590    /// ## Note
591    ///
592    /// This will not receive any events if [`TrayIconEvent::set_event_handler`] has been called with a `Some` value.
593    pub fn receiver<'a>() -> &'a TrayIconEventReceiver {
594        &TRAY_CHANNEL.1
595    }
596
597    /// Set a handler to be called for new events. Useful for implementing custom event sender.
598    ///
599    /// ## Note
600    ///
601    /// Calling this function with a `Some` value,
602    /// will not send new events to the channel associated with [`TrayIconEvent::receiver`]
603    pub fn set_event_handler<F: Fn(TrayIconEvent) + Send + Sync + 'static>(f: Option<F>) {
604        if let Some(f) = f {
605            let _ = TRAY_EVENT_HANDLER.set(Some(Box::new(f)));
606        } else {
607            let _ = TRAY_EVENT_HANDLER.set(None);
608        }
609    }
610
611    #[allow(unused)]
612    pub(crate) fn send(event: TrayIconEvent) {
613        if let Some(handler) = TRAY_EVENT_HANDLER.get_or_init(|| None) {
614            handler(event);
615        } else {
616            let _ = TRAY_CHANNEL.0.send(event);
617        }
618    }
619}
620
621#[cfg(test)]
622mod tests {
623
624    #[cfg(feature = "serde")]
625    #[test]
626    fn it_serializes() {
627        use super::*;
628        let event = TrayIconEvent::Click {
629            button: MouseButton::Left,
630            button_state: MouseButtonState::Down,
631            id: TrayIconId::new("id"),
632            position: dpi::PhysicalPosition::default(),
633            rect: Rect::default(),
634        };
635
636        let value = serde_json::to_value(&event).unwrap();
637        assert_eq!(
638            value,
639            serde_json::json!({
640                "type": "Click",
641                "button": "Left",
642                "buttonState": "Down",
643                "id": "id",
644                "position": {
645                    "x": 0.0,
646                    "y": 0.0,
647                },
648                "rect": {
649                    "size": {
650                        "width": 0,
651                        "height": 0,
652                    },
653                    "position": {
654                        "x": 0.0,
655                        "y": 0.0,
656                    },
657                }
658            })
659        )
660    }
661}