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