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}