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}