Skip to main content

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 and BSD (AppIndicator or KSNI)
14//!
15//! # Platform-specific notes:
16//!
17//! - On Windows and the Linux/BSD AppIndicator backend, an event loop must be running on the thread. The
18//!   KSNI backend runs its D-Bus service on a worker thread and does not require a GTK event loop.
19//! - When both the `libappindicator` and `ksni` features are enabled on Linux or BSD, tray-icon
20//!   uses the KSNI backend and emits a Cargo warning.
21//! - 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).
22//!
23//! # Dependencies (Linux/BSD)
24//!
25//! The default Linux backend uses GTK, `libxdo`, and `libappindicator` or
26//! `libayatana-appindicator`. The `ksni` backend does not require these system libraries unless a
27//!  GTK backend is also enabled.
28//!
29//! #### Arch Linux / Manjaro:
30//!
31//! ```sh
32//! pacman -S gtk3 xdotool libappindicator-gtk3 #or libayatana-appindicator
33//! ```
34//!
35//! #### Debian / Ubuntu:
36//!
37//! ```sh
38//! sudo apt install libgtk-3-dev libxdo-dev libappindicator3-dev #or libayatana-appindicator3-dev
39//! ```
40//!
41//! # Examples
42//!
43//! #### Create a tray icon without a menu.
44//!
45//! ```no_run
46//! use tray_icon::{TrayIconBuilder, Icon};
47//!
48//! # let icon = Icon::from_rgba(Vec::new(), 0, 0).unwrap();
49//! let tray_icon = TrayIconBuilder::new()
50//!     .with_tooltip("system-tray - tray icon library!")
51//!     .with_icon(icon)
52//!     .build()
53//!     .unwrap();
54//! ```
55//!
56//! #### Create a tray icon with a menu.
57//!
58//! ```no_run
59//! use tray_icon::{TrayIconBuilder, menu::Menu,Icon};
60//!
61//! # let icon = Icon::from_rgba(Vec::new(), 0, 0).unwrap();
62//! let tray_menu = Menu::new();
63//! let tray_icon = TrayIconBuilder::new()
64//!     .with_menu(Box::new(tray_menu))
65//!     .with_tooltip("system-tray - tray icon library!")
66//!     .with_icon(icon)
67//!     .build()
68//!     .unwrap();
69//! ```
70//!
71//! # Processing tray events
72//!
73//! You can use [`TrayIconEvent::receiver`] to get a reference to the [`TrayIconEventReceiver`]
74//! which you can use to listen to events when a click happens on the tray icon
75//! ```no_run
76//! use tray_icon::TrayIconEvent;
77//!
78//! if let Ok(event) = TrayIconEvent::receiver().try_recv() {
79//!     println!("{:?}", event);
80//! }
81//! ```
82//!
83//! You can also listen for the menu events using [`MenuEvent::receiver`](crate::menu::MenuEvent::receiver) to get events for the tray context menu.
84//!
85//! ```no_run
86//! use tray_icon::{TrayIconEvent, menu::MenuEvent};
87//!
88//! if let Ok(event) = TrayIconEvent::receiver().try_recv() {
89//!     println!("tray event: {:?}", event);
90//! }
91//!
92//! if let Ok(event) = MenuEvent::receiver().try_recv() {
93//!     println!("menu event: {:?}", event);
94//! }
95//! ```
96//!
97//! ### Note for [winit] or [tao] users:
98//!
99//! You should use [`TrayIconEvent::set_event_handler`] and forward
100//! the tray icon events to the event loop by using [`EventLoopProxy`]
101//! so that the event loop is awakened on each tray icon event.
102//! Same can be done for menu events using [`crate::menu::MenuEvent::set_event_handler`].
103//!
104//! ```no_run
105//! # use winit::event_loop::EventLoop;
106//! enum UserEvent {
107//!   TrayIconEvent(tray_icon::TrayIconEvent),
108//!   MenuEvent(tray_icon::menu::MenuEvent)
109//! }
110//!
111//! let event_loop = EventLoop::<UserEvent>::with_user_event().build().unwrap();
112//!
113//! let proxy = event_loop.create_proxy();
114//! tray_icon::TrayIconEvent::set_event_handler(Some(move |event| {
115//!     proxy.send_event(UserEvent::TrayIconEvent(event));
116//! }));
117//!
118//! let proxy = event_loop.create_proxy();
119//! tray_icon::menu::MenuEvent::set_event_handler(Some(move |event| {
120//!     proxy.send_event(UserEvent::MenuEvent(event));
121//! }));
122//! ```
123//!
124//! [`EventLoopProxy`]: https://docs.rs/winit/latest/winit/event_loop/struct.EventLoopProxy.html
125//! [winit]: https://docs.rs/winit
126//! [tao]: https://docs.rs/tao
127
128#[cfg(all(
129    any(
130        target_os = "linux",
131        target_os = "dragonfly",
132        target_os = "freebsd",
133        target_os = "netbsd",
134        target_os = "openbsd"
135    ),
136    not(any(feature = "libappindicator", feature = "ksni"))
137))]
138compile_error!("either the `libappindicator` or `ksni` feature must be enabled on Linux and BSD");
139
140use std::{
141    cell::RefCell,
142    path::{Path, PathBuf},
143    rc::Rc,
144};
145
146use counter::Counter;
147use crossbeam_channel::{unbounded, Receiver, Sender};
148use once_cell::sync::{Lazy, OnceCell};
149
150mod counter;
151mod error;
152mod icon;
153mod platform_impl;
154mod tray_icon_id;
155
156pub use self::error::*;
157pub use self::icon::{BadIcon, Icon};
158pub use self::tray_icon_id::TrayIconId;
159
160/// Re-export of the [muda] crate and used for tray context menu.
161pub mod menu {
162    pub use muda::*;
163}
164pub use muda::dpi;
165
166static COUNTER: Counter = Counter::new();
167
168/// Attributes to use when creating a tray icon.
169pub struct TrayIconAttributes {
170    /// Tray icon tooltip
171    ///
172    /// ## Platform-specific:
173    ///
174    /// - **Linux/BSD AppIndicator backend:** Unsupported.
175    pub tooltip: Option<String>,
176
177    /// Tray menu
178    ///
179    /// ## Platform-specific:
180    ///
181    /// - **Linux/BSD AppIndicator backend:** Once a menu is set, it cannot be removed.
182    pub menu: Option<Box<dyn menu::ContextMenu>>,
183
184    /// Tray icon
185    ///
186    /// ## Platform-specific:
187    ///
188    /// - **Linux/BSD AppIndicator backend:** Sometimes the icon won't be visible unless a menu is set.
189    ///   Setting an empty [`Menu`](crate::menu::Menu) is enough.
190    pub icon: Option<Icon>,
191
192    /// Tray icon temp dir path. **Linux/BSD AppIndicator backend only**.
193    pub temp_dir_path: Option<PathBuf>,
194
195    /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
196    pub icon_is_template: bool,
197
198    /// Whether to show the tray menu on left click or not, default is `true`.
199    ///
200    /// ## Platform-specific:
201    ///
202    /// - **Linux:** Unsupported.
203    pub menu_on_left_click: bool,
204
205    /// Whether to show the tray menu on right click or not, default is `true`.
206    ///
207    /// ## Platform-specific:
208    ///
209    /// - **Linux:** Unsupported.
210    pub menu_on_right_click: bool,
211
212    /// Tray icon title.
213    ///
214    /// ## Platform-specific
215    ///
216    /// - **Linux/BSD AppIndicator backend:** The title will not be shown unless there is an icon
217    ///   as well.  The title is useful for numerical and other frequently
218    ///   updated information.  In general, it shouldn't be shown unless a
219    ///   user requests it as it can take up a significant amount of space
220    ///   on the user's panel.  This may not be shown in all visualizations.
221    /// - **Windows:** Unsupported.
222    pub title: Option<String>,
223
224    /// A stable identity for the tray icon, as a UUID in `u128` form. **Windows only**.
225    ///
226    /// Windows remembers per-icon user settings (most importantly whether the
227    /// icon is pinned to the taskbar or hidden in the overflow) keyed on the
228    /// icon's identity. Without a GUID that identity is the executable path
229    /// plus a per-process counter, so the setting is lost whenever the binary
230    /// moves - for example every update of an installer that uses versioned
231    /// directories. With a GUID, and an executable that is Authenticode-signed
232    /// by the same publisher across versions, the setting survives.
233    ///
234    /// Use one fixed GUID per tray icon your application creates, and never
235    /// share it between two icons that can be alive at the same time. See
236    /// <https://learn.microsoft.com/windows/win32/api/shellapi/ns-shellapi-notifyicondataw#troubleshooting>
237    /// for the rules Windows applies (the GUID is bound to the binary's path
238    /// unless the binary is signed).
239    ///
240    /// ## Platform-specific:
241    ///
242    /// - **Linux / macOS:** Ignored.
243    pub guid: Option<u128>,
244}
245
246impl Default for TrayIconAttributes {
247    fn default() -> Self {
248        Self {
249            tooltip: None,
250            menu: None,
251            icon: None,
252            temp_dir_path: None,
253            icon_is_template: false,
254            menu_on_left_click: true,
255            menu_on_right_click: true,
256            title: None,
257            guid: None,
258        }
259    }
260}
261
262/// [`TrayIcon`] builder struct and associated methods.
263#[derive(Default)]
264pub struct TrayIconBuilder {
265    id: TrayIconId,
266    attrs: TrayIconAttributes,
267}
268
269impl TrayIconBuilder {
270    /// Creates a new [`TrayIconBuilder`] with default [`TrayIconAttributes`].
271    ///
272    /// See [`TrayIcon::new`] for more info.
273    pub fn new() -> Self {
274        Self {
275            id: TrayIconId::new_unique(),
276            attrs: TrayIconAttributes::default(),
277        }
278    }
279
280    /// Sets the unique id to build the tray icon with.
281    pub fn with_id<I: Into<TrayIconId>>(mut self, id: I) -> Self {
282        self.id = id.into();
283        self
284    }
285
286    /// Set the a menu for this tray icon.
287    ///
288    /// ## Platform-specific:
289    ///
290    /// - **Linux/BSD AppIndicator backend:** Once a menu is set, it cannot be removed or replaced, but its
291    ///   content can be changed.
292    pub fn with_menu(mut self, menu: Box<dyn menu::ContextMenu>) -> Self {
293        self.attrs.menu = Some(menu);
294        self
295    }
296
297    /// Set an icon for this tray icon.
298    ///
299    /// ## Platform-specific:
300    ///
301    /// - **Linux/BSD AppIndicator backend:** Sometimes the icon won't be visible unless a menu is set.
302    ///   Setting an empty [`Menu`](crate::menu::Menu) is enough.
303    pub fn with_icon(mut self, icon: Icon) -> Self {
304        self.attrs.icon = Some(icon);
305        self
306    }
307
308    /// Set a tooltip for this tray icon.
309    ///
310    /// ## Platform-specific:
311    ///
312    /// - **Linux/BSD AppIndicator backend:** Unsupported.
313    pub fn with_tooltip<S: AsRef<str>>(mut self, s: S) -> Self {
314        self.attrs.tooltip = Some(s.as_ref().to_string());
315        self
316    }
317
318    /// Set the tray icon title.
319    ///
320    /// ## Platform-specific
321    ///
322    /// - **Linux/BSD AppIndicator backend:** The title will not be shown unless there is an icon
323    ///   as well.  The title is useful for numerical and other frequently
324    ///   updated information.  In general, it shouldn't be shown unless a
325    ///   user requests it as it can take up a significant amount of space
326    ///   on the user's panel.  This may not be shown in all visualizations.
327    /// - **Windows:** Unsupported.
328    pub fn with_title<S: AsRef<str>>(mut self, title: S) -> Self {
329        self.attrs.title.replace(title.as_ref().to_string());
330        self
331    }
332
333    /// Set tray icon temp dir path. **Linux/BSD AppIndicator backend only**.
334    ///
335    /// On Linux, we need to write the icon to the disk and usually it will
336    /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
337    pub fn with_temp_dir_path<P: AsRef<Path>>(mut self, s: P) -> Self {
338        self.attrs.temp_dir_path = Some(s.as_ref().to_path_buf());
339        self
340    }
341
342    /// Use the icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
343    pub fn with_icon_as_template(mut self, is_template: bool) -> Self {
344        self.attrs.icon_is_template = is_template;
345        self
346    }
347
348    /// Whether to show the tray menu on left click or not, default is `true`.
349    ///
350    /// ## Platform-specific:
351    ///
352    /// - **Linux:** Unsupported.
353    pub fn with_menu_on_left_click(mut self, enable: bool) -> Self {
354        self.attrs.menu_on_left_click = enable;
355        self
356    }
357
358    /// Whether to show the tray menu on right click or not, default is `true`.
359    ///
360    /// ## Platform-specific:
361    ///
362    /// - **Linux:** Unsupported.
363    pub fn with_menu_on_right_click(mut self, enable: bool) -> Self {
364        self.attrs.menu_on_right_click = enable;
365        self
366    }
367
368    /// Set a stable identity GUID for this tray icon. **Windows only**.
369    ///
370    /// See [`TrayIconAttributes::guid`] for why and how to pick one.
371    pub fn with_guid(mut self, guid: u128) -> Self {
372        self.attrs.guid = Some(guid);
373        self
374    }
375
376    /// Access the unique id that will be assigned to the tray icon
377    /// this builder will create.
378    pub fn id(&self) -> &TrayIconId {
379        &self.id
380    }
381
382    /// Builds and adds a new [`TrayIcon`] to the system tray.
383    pub fn build(self) -> Result<TrayIcon> {
384        TrayIcon::with_id(self.id, self.attrs)
385    }
386}
387
388/// Tray icon struct and associated methods.
389///
390/// This type is reference-counted and the icon is removed when the last instance is dropped.
391#[derive(Clone)]
392pub struct TrayIcon {
393    id: TrayIconId,
394    tray: Rc<RefCell<platform_impl::TrayIcon>>,
395}
396
397impl TrayIcon {
398    /// Builds and adds a new tray icon to the system tray.
399    ///
400    /// ## Platform-specific:
401    ///
402    /// - **Linux/BSD AppIndicator backend:** Sometimes the icon won't be visible unless a menu is set.
403    ///   Setting an empty [`Menu`](crate::menu::Menu) is enough.
404    pub fn new(attrs: TrayIconAttributes) -> Result<Self> {
405        let id = TrayIconId::new_unique();
406        Ok(Self {
407            tray: Rc::new(RefCell::new(platform_impl::TrayIcon::new(
408                id.clone(),
409                attrs,
410            )?)),
411            id,
412        })
413    }
414
415    /// Builds and adds a new tray icon to the system tray with the specified Id.
416    ///
417    /// See [`TrayIcon::new`] for more info.
418    pub fn with_id<I: Into<TrayIconId>>(id: I, attrs: TrayIconAttributes) -> Result<Self> {
419        let id = id.into();
420        Ok(Self {
421            tray: Rc::new(RefCell::new(platform_impl::TrayIcon::new(
422                id.clone(),
423                attrs,
424            )?)),
425            id,
426        })
427    }
428
429    /// Returns the id associated with this tray icon.
430    pub fn id(&self) -> &TrayIconId {
431        &self.id
432    }
433
434    /// Set new tray icon. If `None` is provided, it will remove the icon.
435    pub fn set_icon(&self, icon: Option<Icon>) -> Result<()> {
436        self.tray.borrow_mut().set_icon(icon)
437    }
438
439    /// Set new tray menu.
440    ///
441    /// ## Platform-specific:
442    ///
443    /// - **Linux/BSD AppIndicator backend:** Once a menu is set it cannot be removed, so `None` has no
444    ///   effect.
445    pub fn set_menu(&self, menu: Option<Box<dyn menu::ContextMenu>>) {
446        self.tray.borrow_mut().set_menu(menu)
447    }
448
449    /// Sets the tooltip for this tray icon.
450    ///
451    /// ## Platform-specific:
452    ///
453    /// - **Linux/BSD AppIndicator backend:** Unsupported.
454    pub fn set_tooltip<S: AsRef<str>>(&self, tooltip: Option<S>) -> Result<()> {
455        self.tray.borrow_mut().set_tooltip(tooltip)
456    }
457
458    /// Sets the tooltip for this tray icon.
459    ///
460    /// ## Platform-specific:
461    ///
462    /// - **Linux/BSD AppIndicator backend:** The title will not be shown unless there is an icon
463    ///   as well.  The title is useful for numerical and other frequently
464    ///   updated information.  In general, it shouldn't be shown unless a
465    ///   user requests it as it can take up a significant amount of space
466    ///   on the user's panel.  This may not be shown in all visualizations.
467    /// - **Windows:** Unsupported
468    pub fn set_title<S: AsRef<str>>(&self, title: Option<S>) {
469        self.tray.borrow_mut().set_title(title)
470    }
471
472    /// Show or hide this tray icon
473    pub fn set_visible(&self, visible: bool) -> Result<()> {
474        self.tray.borrow_mut().set_visible(visible)
475    }
476
477    /// Sets the tray icon temp dir path. **Linux/BSD AppIndicator backend only**.
478    ///
479    /// On Linux, we need to write the icon to the disk and usually it will
480    /// be `$XDG_RUNTIME_DIR/tray-icon` or `$TEMP/tray-icon`.
481    pub fn set_temp_dir_path<P: AsRef<Path>>(&self, path: Option<P>) {
482        #[cfg(any(
483            target_os = "linux",
484            target_os = "dragonfly",
485            target_os = "freebsd",
486            target_os = "netbsd",
487            target_os = "openbsd"
488        ))]
489        self.tray.borrow_mut().set_temp_dir_path(path);
490        #[cfg(not(any(
491            target_os = "linux",
492            target_os = "dragonfly",
493            target_os = "freebsd",
494            target_os = "netbsd",
495            target_os = "openbsd"
496        )))]
497        let _ = path;
498    }
499
500    /// Set the current icon as a [template](https://developer.apple.com/documentation/appkit/nsimage/1520017-template?language=objc). **macOS only**.
501    pub fn set_icon_as_template(&self, is_template: bool) {
502        #[cfg(target_os = "macos")]
503        self.tray.borrow_mut().set_icon_as_template(is_template);
504        #[cfg(not(target_os = "macos"))]
505        let _ = is_template;
506    }
507
508    pub fn set_icon_with_as_template(&self, icon: Option<Icon>, is_template: bool) -> Result<()> {
509        #[cfg(target_os = "macos")]
510        return self
511            .tray
512            .borrow_mut()
513            .set_icon_with_as_template(icon, is_template);
514        #[cfg(not(target_os = "macos"))]
515        {
516            let _ = icon;
517            let _ = is_template;
518            Ok(())
519        }
520    }
521
522    /// Disable or enable showing the tray menu on left click.
523    ///
524    /// ## Platform-specific:
525    ///
526    /// - **Linux:** Unsupported.
527    pub fn set_show_menu_on_left_click(&self, enable: bool) {
528        #[cfg(any(target_os = "macos", target_os = "windows"))]
529        self.tray.borrow_mut().set_show_menu_on_left_click(enable);
530        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
531        let _ = enable;
532    }
533
534    /// Disable or enable showing the tray menu on right click.
535    ///
536    /// ## Platform-specific:
537    ///
538    /// - **Linux:** Unsupported.
539    pub fn set_show_menu_on_right_click(&self, enable: bool) {
540        #[cfg(any(target_os = "macos", target_os = "windows"))]
541        self.tray.borrow_mut().set_show_menu_on_right_click(enable);
542        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
543        let _ = enable;
544    }
545
546    /// Manually show the tray menu at the current cursor position.
547    ///
548    /// This is useful when you want to control when the menu is displayed,
549    /// for example after updating menu items dynamically.
550    ///
551    /// ## Platform-specific:
552    ///
553    /// - **Linux:** Unsupported.
554    pub fn show_menu(&self) {
555        #[cfg(any(target_os = "macos", target_os = "windows"))]
556        self.tray.borrow().show_menu();
557    }
558
559    /// Get tray icon rect.
560    ///
561    /// ## Platform-specific:
562    ///
563    /// - **Linux**: Unsupported.
564    pub fn rect(&self) -> Option<Rect> {
565        self.tray.borrow().rect()
566    }
567
568    /// Get the tray icon's underlying [window handle](windows_sys::Win32::Foundation::HWND) **Windows only**.
569    ///
570    /// This window handle is valid as long as the tray icon.
571    #[cfg(windows)]
572    pub fn window_handle(&self) -> windows_sys::Win32::Foundation::HWND {
573        self.tray.borrow().hwnd()
574    }
575
576    /// Get the tray icon's underlying [NSStatusItem](objc2_app_kit::NSStatusItem) **macOS only**.
577    ///
578    /// Returns `None` if the status item is not available.
579    #[cfg(target_os = "macos")]
580    pub fn ns_status_item(&self) -> Option<objc2::rc::Retained<objc2_app_kit::NSStatusItem>> {
581        self.tray.borrow().ns_status_item().cloned()
582    }
583
584    /// Get the tray icon's underlying [AppIndicator](libappindicator::AppIndicator).
585    /// **Linux/BSD AppIndicator backend only**.
586    ///
587    /// # Safety
588    ///
589    /// The returned pointer is valid as long as the `TrayIcon` is.
590    #[cfg(all(
591        unix,
592        not(target_os = "macos"),
593        feature = "libappindicator",
594        not(feature = "ksni")
595    ))]
596    pub unsafe fn app_indicator(&self) -> *const libappindicator::AppIndicator {
597        self.tray.borrow().app_indicator() as *const _
598    }
599}
600
601/// Describes a tray icon event.
602///
603/// ## Platform-specific:
604///
605/// - **Linux/BSD AppIndicator backend:** Unsupported. The event is not emitted even though the icon is
606///   shown and will still show a context menu on right click.
607/// - **Linux/BSD KSNI backend:** Emits left- and middle-click activation events. The StatusNotifier
608///   host handles right clicks itself and does not expose them to the application. The protocol
609///   does not provide the icon rectangle, so `rect` is empty.
610#[derive(Debug, Clone)]
611#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
612#[cfg_attr(feature = "serde", serde(tag = "type"))]
613#[non_exhaustive]
614pub enum TrayIconEvent {
615    /// A click happened on the tray icon.
616    #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
617    Click {
618        /// Id of the tray icon which triggered this event.
619        id: TrayIconId,
620        /// Physical Position of this event.
621        position: dpi::PhysicalPosition<f64>,
622        /// Position and size of the tray icon.
623        rect: Rect,
624        /// Mouse button that triggered this event.
625        button: MouseButton,
626        /// Mouse button state when this event was triggered.
627        button_state: MouseButtonState,
628    },
629    /// A double click happened on the tray icon. **Windows Only**
630    DoubleClick {
631        /// Id of the tray icon which triggered this event.
632        id: TrayIconId,
633        /// Physical Position of this event.
634        position: dpi::PhysicalPosition<f64>,
635        /// Position and size of the tray icon.
636        rect: Rect,
637        /// Mouse button that triggered this event.
638        button: MouseButton,
639    },
640    /// The mouse entered the tray icon region.
641    Enter {
642        /// Id of the tray icon which triggered this event.
643        id: TrayIconId,
644        /// Physical Position of this event.
645        position: dpi::PhysicalPosition<f64>,
646        /// Position and size of the tray icon.
647        rect: Rect,
648    },
649    /// The mouse moved over the tray icon region.
650    Move {
651        /// Id of the tray icon which triggered this event.
652        id: TrayIconId,
653        /// Physical Position of this event.
654        position: dpi::PhysicalPosition<f64>,
655        /// Position and size of the tray icon.
656        rect: Rect,
657    },
658    /// The mouse left the tray icon region.
659    Leave {
660        /// Id of the tray icon which triggered this event.
661        id: TrayIconId,
662        /// Physical Position of this event.
663        position: dpi::PhysicalPosition<f64>,
664        /// Position and size of the tray icon.
665        rect: Rect,
666    },
667}
668
669/// Describes the mouse button state.
670#[derive(Clone, Copy, PartialEq, Eq, Debug)]
671#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
672#[derive(Default)]
673pub enum MouseButtonState {
674    #[default]
675    Up,
676    Down,
677}
678
679/// Describes which mouse button triggered the event..
680#[derive(Clone, Copy, PartialEq, Eq, Debug)]
681#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
682#[derive(Default)]
683pub enum MouseButton {
684    #[default]
685    Left,
686    Right,
687    Middle,
688}
689
690/// Describes a rectangle including position (x - y axis) and size.
691#[derive(Debug, PartialEq, Clone, Copy)]
692#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
693pub struct Rect {
694    pub size: dpi::PhysicalSize<u32>,
695    pub position: dpi::PhysicalPosition<f64>,
696}
697
698impl Default for Rect {
699    fn default() -> Self {
700        Self {
701            size: dpi::PhysicalSize::new(0, 0),
702            position: dpi::PhysicalPosition::new(0., 0.),
703        }
704    }
705}
706
707/// A reciever that could be used to listen to tray events.
708pub type TrayIconEventReceiver = Receiver<TrayIconEvent>;
709type TrayIconEventHandler = Box<dyn Fn(TrayIconEvent) + Send + Sync + 'static>;
710
711static TRAY_CHANNEL: Lazy<(Sender<TrayIconEvent>, TrayIconEventReceiver)> = Lazy::new(unbounded);
712static TRAY_EVENT_HANDLER: OnceCell<Option<TrayIconEventHandler>> = OnceCell::new();
713
714impl TrayIconEvent {
715    /// Returns the id of the tray icon which triggered this event.
716    pub fn id(&self) -> &TrayIconId {
717        match self {
718            TrayIconEvent::Click { id, .. } => id,
719            TrayIconEvent::DoubleClick { id, .. } => id,
720            TrayIconEvent::Enter { id, .. } => id,
721            TrayIconEvent::Move { id, .. } => id,
722            TrayIconEvent::Leave { id, .. } => id,
723        }
724    }
725
726    /// Gets a reference to the event channel's [`TrayIconEventReceiver`]
727    /// which can be used to listen for tray events.
728    ///
729    /// ## Note
730    ///
731    /// This will not receive any events if [`TrayIconEvent::set_event_handler`] has been called with a `Some` value.
732    pub fn receiver<'a>() -> &'a TrayIconEventReceiver {
733        &TRAY_CHANNEL.1
734    }
735
736    /// Set a handler to be called for new events. Useful for implementing custom event sender.
737    ///
738    /// ## Note
739    ///
740    /// Calling this function with a `Some` value,
741    /// will not send new events to the channel associated with [`TrayIconEvent::receiver`]
742    pub fn set_event_handler<F: Fn(TrayIconEvent) + Send + Sync + 'static>(f: Option<F>) {
743        if let Some(f) = f {
744            let _ = TRAY_EVENT_HANDLER.set(Some(Box::new(f)));
745        } else {
746            let _ = TRAY_EVENT_HANDLER.set(None);
747        }
748    }
749
750    #[allow(unused)]
751    pub(crate) fn send(event: TrayIconEvent) {
752        if let Some(handler) = TRAY_EVENT_HANDLER.get_or_init(|| None) {
753            handler(event);
754        } else {
755            let _ = TRAY_CHANNEL.0.send(event);
756        }
757    }
758}
759
760#[cfg(test)]
761mod tests {
762
763    #[cfg(feature = "serde")]
764    #[test]
765    fn it_serializes() {
766        use super::*;
767        let event = TrayIconEvent::Click {
768            button: MouseButton::Left,
769            button_state: MouseButtonState::Down,
770            id: TrayIconId::new("id"),
771            position: dpi::PhysicalPosition::default(),
772            rect: Rect::default(),
773        };
774
775        let value = serde_json::to_value(&event).unwrap();
776        assert_eq!(
777            value,
778            serde_json::json!({
779                "type": "Click",
780                "button": "Left",
781                "buttonState": "Down",
782                "id": "id",
783                "position": {
784                    "x": 0.0,
785                    "y": 0.0,
786                },
787                "rect": {
788                    "size": {
789                        "width": 0,
790                        "height": 0,
791                    },
792                    "position": {
793                        "x": 0.0,
794                        "y": 0.0,
795                    },
796                }
797            })
798        )
799    }
800}