tray_icon_win/
lib.rs

1//! tray-icon-win lets you create tray icons for desktop applications on Windows.
2//!
3//! This is a personal fork of [tray-icon](https://github.com/tauri-apps/tray-icon). For general use, please consider using the original.
4//!
5//! # Notes:
6//!
7//! - An event loop must be running on the thread, on Windows, a win32 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.
8//!
9//! # Examples
10//!
11//! #### Create a tray icon without a menu.
12//!
13//! ```no_run
14//! use tray_icon_win::{TrayIconBuilder, Icon};
15//!
16//! # let icon = Icon::from_rgba(Vec::new(), 0, 0).unwrap();
17//! let tray_icon = TrayIconBuilder::new()
18//!     .with_tooltip("system-tray - tray icon library!")
19//!     .with_icon(icon)
20//!     .build()
21//!     .unwrap();
22//! ```
23//!
24//! #### Create a tray icon with a menu.
25//!
26//! ```no_run
27//! use tray_icon_win::{TrayIconBuilder, menu::Menu,Icon};
28//!
29//! # let icon = Icon::from_rgba(Vec::new(), 0, 0).unwrap();
30//! let tray_menu = Menu::new();
31//! let tray_icon = TrayIconBuilder::new()
32//!     .with_menu(Box::new(tray_menu))
33//!     .with_tooltip("system-tray - tray icon library!")
34//!     .with_icon(icon)
35//!     .build()
36//!     .unwrap();
37//! ```
38//!
39//! # Processing tray events
40//!
41//! You can use [`TrayIconEvent::receiver`] to get a reference to the [`TrayIconEventReceiver`]
42//! which you can use to listen to events when a click happens on the tray icon
43//! ```no_run
44//! use tray_icon_win::TrayIconEvent;
45//!
46//! if let Ok(event) = TrayIconEvent::receiver().try_recv() {
47//!     println!("{:?}", event);
48//! }
49//! ```
50//!
51//! You can also listen for the menu events using [`MenuEvent::receiver`](crate::menu::MenuEvent::receiver) to get events for the tray context menu.
52//!
53//! ```no_run
54//! use tray_icon_win::{TrayIconEvent, menu::MenuEvent};
55//!
56//! if let Ok(event) = TrayIconEvent::receiver().try_recv() {
57//!     println!("tray event: {:?}", event);
58//! }
59//!
60//! if let Ok(event) = MenuEvent::receiver().try_recv() {
61//!     println!("menu event: {:?}", event);
62//! }
63//! ```
64//!
65//! ### Note for [winit] or [tao] users:
66//!
67//! You should use [`TrayIconEvent::set_event_handler`] and forward
68//! the tray icon events to the event loop by using [`EventLoopProxy`]
69//! so that the event loop is awakened on each tray icon event.
70//!
71//! ```no_run
72//! # use winit::event_loop::EventLoop;
73//! enum UserEvent {
74//!   TrayIconEvent(tray_icon_win::TrayIconEvent),
75//!   MenuEvent(tray_icon_win::menu::MenuEvent)
76//! }
77//!
78//! let event_loop = EventLoop::<UserEvent>::with_user_event().build().unwrap();
79//!
80//! let proxy = event_loop.create_proxy();
81//! tray_icon_win::TrayIconEvent::set_event_handler(Some(move |event| {
82//!     proxy.send_event(UserEvent::TrayIconEvent(event));
83//! }));
84//!
85//! let proxy = event_loop.create_proxy();
86//! tray_icon_win::menu::MenuEvent::set_event_handler(Some(move |event| {
87//!     proxy.send_event(UserEvent::MenuEvent(event));
88//! }));
89//! ```
90//!
91//! [`EventLoopProxy`]: https://docs.rs/winit/latest/winit/event_loop/struct.EventLoopProxy.html
92//! [winit]: https://docs.rs/winit
93//! [tao]: https://docs.rs/tao
94
95use std::{cell::RefCell, rc::Rc};
96
97use counter::Counter;
98use crossbeam_channel::{unbounded, Receiver, Sender};
99use muda_win::{MenuEvent, MenuEventHandler};
100use platform_impl::TrayIcon as PlatformTrayIcon;
101use std::sync::{LazyLock, OnceLock};
102
103mod counter;
104mod error;
105mod icon;
106mod platform_impl;
107mod tray_icon_id;
108
109pub use self::error::*;
110pub use self::icon::{BadIcon, Icon};
111pub use self::tray_icon_id::TrayIconId;
112
113/// Re-export of [muda-win](::muda_win) crate and used for tray context menu.
114pub mod menu {
115    pub use muda_win::*;
116}
117pub use muda_win::dpi;
118
119static COUNTER: Counter = Counter::new();
120
121/// Attributes to use when creating a tray icon.
122pub struct TrayIconAttributes {
123    /// Tray icon tooltip
124    pub tooltip: Option<String>,
125
126    /// Tray menu
127    pub menu: Option<Box<dyn menu::ContextMenu>>,
128
129    /// Tray menu event
130    pub menu_event: Option<Option<MenuEventHandler>>,
131
132    /// Tray icon
133    pub icon: Option<Icon>,
134
135    /// Whether to show the tray menu on left click or not, default is `true`.
136    pub menu_on_left_click: bool,
137}
138
139impl Default for TrayIconAttributes {
140    fn default() -> Self {
141        Self {
142            tooltip: None,
143            menu: None,
144            icon: None,
145            menu_on_left_click: true,
146            menu_event: None,
147        }
148    }
149}
150
151/// [`TrayIcon`] builder struct and associated methods.
152#[derive(Default)]
153pub struct TrayIconBuilder {
154    id: TrayIconId,
155    attrs: TrayIconAttributes,
156}
157
158impl TrayIconBuilder {
159    /// Creates a new [`TrayIconBuilder`] with default [`TrayIconAttributes`].
160    ///
161    /// See [`TrayIcon::new`] for more info.
162    pub fn new() -> Self {
163        Self {
164            id: TrayIconId(COUNTER.next().to_string()),
165            attrs: TrayIconAttributes::default(),
166        }
167    }
168
169    /// Sets the unique id to build the tray icon with.
170    pub fn with_id<I: Into<TrayIconId>>(mut self, id: I) -> Self {
171        self.id = id.into();
172        self
173    }
174
175    /// Set the a menu for this tray icon.
176    pub fn with_menu(mut self, menu: Box<dyn menu::ContextMenu>) -> Self {
177        self.attrs.menu = Some(menu);
178        self
179    }
180
181    /// Set an icon for this tray icon.
182    pub fn with_icon(mut self, icon: Icon) -> Self {
183        self.attrs.icon = Some(icon);
184        self
185    }
186
187    /// Set a tooltip for this tray icon.
188    pub fn with_tooltip<S: AsRef<str>>(mut self, s: S) -> Self {
189        self.attrs.tooltip = Some(s.as_ref().to_string());
190        self
191    }
192
193    /// Whether to show the tray menu on left click or not, default is `true`. **macOS only**.
194    pub fn with_menu_on_left_click(mut self, enable: bool) -> Self {
195        self.attrs.menu_on_left_click = enable;
196        self
197    }
198
199    /// Access the unique id that will be assigned to the tray icon
200    /// this builder will create.
201    pub fn id(&self) -> &TrayIconId {
202        &self.id
203    }
204
205    /// Builds and adds a new [`TrayIcon`] to the system tray.
206    pub fn build(self) -> Result<TrayIcon> {
207        TrayIcon::with_id(self.id, self.attrs)
208    }
209
210    pub fn on_menu_event<F>(mut self, event: F) -> Self
211    where
212        F: Fn(MenuEvent) + Send + Sync + 'static,
213    {
214        let event = MenuEvent::set_event_handler(Some(event));
215
216        self.attrs.menu_event = event;
217
218        self
219    }
220}
221
222// Tray icon struct and associated methods.
223///
224/// This type is reference-counted and the icon is removed when the last instance is dropped.
225#[derive(Clone, Debug)]
226pub struct TrayIcon {
227    id: TrayIconId,
228    tray: Rc<RefCell<PlatformTrayIcon>>,
229}
230
231impl TrayIcon {
232    /// Builds and adds a new tray icon to the system tray.
233    pub fn new(attrs: TrayIconAttributes) -> Result<Self> {
234        let id = TrayIconId(COUNTER.next().to_string());
235        Ok(Self {
236            tray: Rc::new(RefCell::new(PlatformTrayIcon::new(id.clone(), attrs)?)),
237            id,
238        })
239    }
240
241    /// Builds and adds a new tray icon to the system tray with the specified Id.
242    ///
243    /// See [`TrayIcon::new`] for more info.
244    pub fn with_id<I: Into<TrayIconId>>(id: I, attrs: TrayIconAttributes) -> Result<Self> {
245        let id = id.into();
246        Ok(Self {
247            tray: Rc::new(RefCell::new(PlatformTrayIcon::new(id.clone(), attrs)?)),
248            id,
249        })
250    }
251
252    /// Returns the id associated with this tray icon.
253    pub fn id(&self) -> &TrayIconId {
254        &self.id
255    }
256
257    /// Set new tray icon. If `None` is provided, it will remove the icon.
258    pub fn set_icon(&self, icon: Option<Icon>) -> Result<()> {
259        self.tray.borrow_mut().set_icon(icon)
260    }
261
262    /// Set new tray menu.
263    pub fn set_menu(&self, menu: Option<Box<dyn menu::ContextMenu>>) {
264        self.tray.borrow_mut().set_menu(menu)
265    }
266
267    /// Sets the tooltip for this tray icon.
268    pub fn set_tooltip<S: AsRef<str>>(&self, tooltip: Option<S>) -> Result<()> {
269        self.tray.borrow_mut().set_tooltip(tooltip)
270    }
271
272    /// Show or hide this tray icon
273    pub fn set_visible(&self, visible: bool) -> Result<()> {
274        self.tray.borrow_mut().set_visible(visible)
275    }
276
277    /// Disable or enable showing the tray menu on left click.
278    pub fn set_show_menu_on_left_click(&self, enable: bool) {
279        self.tray.borrow_mut().set_show_menu_on_left_click(enable);
280    }
281
282    /// Get tray icon rect.
283    pub fn rect(&self) -> Option<Rect> {
284        self.tray.borrow().rect()
285    }
286}
287
288/// Describes a tray icon event.
289#[derive(Debug, Clone)]
290#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
291#[cfg_attr(feature = "serde", serde(tag = "type"))]
292#[non_exhaustive]
293pub enum TrayIconEvent {
294    /// A click happened on the tray icon.
295    #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
296    Click {
297        /// Id of the tray icon which triggered this event.
298        id: TrayIconId,
299        /// Physical Position of this event.
300        position: dpi::PhysicalPosition<f64>,
301        /// Position and size of the tray icon.
302        rect: Rect,
303        /// Mouse button that triggered this event.
304        button: MouseButton,
305        /// Mouse button state when this event was triggered.
306        button_state: MouseButtonState,
307    },
308    /// A double click happened on the tray icon. **Windows Only**
309    DoubleClick {
310        /// Id of the tray icon which triggered this event.
311        id: TrayIconId,
312        /// Physical Position of this event.
313        position: dpi::PhysicalPosition<f64>,
314        /// Position and size of the tray icon.
315        rect: Rect,
316        /// Mouse button that triggered this event.
317        button: MouseButton,
318    },
319    /// The mouse entered the tray icon region.
320    Enter {
321        /// Id of the tray icon which triggered this event.
322        id: TrayIconId,
323        /// Physical Position of this event.
324        position: dpi::PhysicalPosition<f64>,
325        /// Position and size of the tray icon.
326        rect: Rect,
327    },
328    /// The mouse moved over the tray icon region.
329    Move {
330        /// Id of the tray icon which triggered this event.
331        id: TrayIconId,
332        /// Physical Position of this event.
333        position: dpi::PhysicalPosition<f64>,
334        /// Position and size of the tray icon.
335        rect: Rect,
336    },
337    /// The mouse left the tray icon region.
338    Leave {
339        /// Id of the tray icon which triggered this event.
340        id: TrayIconId,
341        /// Physical Position of this event.
342        position: dpi::PhysicalPosition<f64>,
343        /// Position and size of the tray icon.
344        rect: Rect,
345    },
346}
347
348/// Describes the mouse button state.
349#[derive(Clone, Copy, PartialEq, Eq, Debug)]
350#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
351pub enum MouseButtonState {
352    Up,
353    Down,
354}
355
356impl Default for MouseButtonState {
357    fn default() -> Self {
358        Self::Up
359    }
360}
361
362/// Describes which mouse button triggered the event..
363#[derive(Clone, Copy, PartialEq, Eq, Debug)]
364#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
365pub enum MouseButton {
366    Left,
367    Right,
368    Middle,
369}
370
371impl Default for MouseButton {
372    fn default() -> Self {
373        Self::Left
374    }
375}
376
377/// Describes a rectangle including position (x - y axis) and size.
378#[derive(Debug, PartialEq, Clone, Copy)]
379#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
380pub struct Rect {
381    pub size: dpi::PhysicalSize<u32>,
382    pub position: dpi::PhysicalPosition<f64>,
383}
384
385impl Default for Rect {
386    fn default() -> Self {
387        Self {
388            size: dpi::PhysicalSize::new(0, 0),
389            position: dpi::PhysicalPosition::new(0., 0.),
390        }
391    }
392}
393
394/// A reciever that could be used to listen to tray events.
395pub type TrayIconEventReceiver = Receiver<TrayIconEvent>;
396type TrayIconEventHandler = Box<dyn Fn(TrayIconEvent) + Send + Sync + 'static>;
397
398static TRAY_CHANNEL: LazyLock<(Sender<TrayIconEvent>, TrayIconEventReceiver)> =
399    LazyLock::new(unbounded);
400static TRAY_EVENT_HANDLER: OnceLock<Option<TrayIconEventHandler>> = OnceLock::new();
401
402impl TrayIconEvent {
403    /// Returns the id of the tray icon which triggered this event.
404    pub fn id(&self) -> &TrayIconId {
405        match self {
406            TrayIconEvent::Click { id, .. } => id,
407            TrayIconEvent::DoubleClick { id, .. } => id,
408            TrayIconEvent::Enter { id, .. } => id,
409            TrayIconEvent::Move { id, .. } => id,
410            TrayIconEvent::Leave { id, .. } => id,
411        }
412    }
413
414    /// Gets a reference to the event channel's [`TrayIconEventReceiver`]
415    /// which can be used to listen for tray events.
416    ///
417    /// ## Note
418    ///
419    /// This will not receive any events if [`TrayIconEvent::set_event_handler`] has been called with a `Some` value.
420    pub fn receiver<'a>() -> &'a TrayIconEventReceiver {
421        &TRAY_CHANNEL.1
422    }
423
424    /// Set a handler to be called for new events. Useful for implementing custom event sender.
425    ///
426    /// ## Note
427    ///
428    /// Calling this function with a `Some` value,
429    /// will not send new events to the channel associated with [`TrayIconEvent::receiver`]
430    pub fn set_event_handler<F: Fn(TrayIconEvent) + Send + Sync + 'static>(f: Option<F>) {
431        if let Some(f) = f {
432            let _ = TRAY_EVENT_HANDLER.set(Some(Box::new(f)));
433        } else {
434            let _ = TRAY_EVENT_HANDLER.set(None);
435        }
436    }
437
438    #[allow(unused)]
439    pub(crate) fn send(event: TrayIconEvent) {
440        if let Some(handler) = TRAY_EVENT_HANDLER.get_or_init(|| None) {
441            handler(event);
442        } else {
443            let _ = TRAY_CHANNEL.0.send(event);
444        }
445    }
446}
447
448#[cfg(test)]
449mod tests {
450
451    #[cfg(feature = "serde")]
452    #[test]
453    fn it_serializes() {
454        use super::*;
455        let event = TrayIconEvent::Click {
456            button: MouseButton::Left,
457            button_state: MouseButtonState::Down,
458            id: TrayIconId::new("id"),
459            position: dpi::PhysicalPosition::default(),
460            rect: Rect::default(),
461        };
462
463        let value = serde_jsonc2::to_value(&event).unwrap();
464        assert_eq!(
465            value,
466            serde_jsonc2::jsonc!({
467                "type": "Click",
468                "button": "Left",
469                "buttonState": "Down",
470                "id": "id",
471                "position": {
472                    "x": 0.0,
473                    "y": 0.0,
474                },
475                "rect": {
476                    "size": {
477                        "width": 0,
478                        "height": 0,
479                    },
480                    "position": {
481                        "x": 0.0,
482                        "y": 0.0,
483                    },
484                }
485            })
486        )
487    }
488}