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}