Skip to main content

node_gui/
tray.rs

1//! System-tray construction, event polling, and embedded icon loading.
2
3use tray_icon::{
4    TrayIcon, TrayIconBuilder, TrayIconEvent,
5    menu::{Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem},
6};
7
8/// Holds the system tray icon and its menu item IDs.
9pub(crate) struct TrayState {
10    _tray: TrayIcon,
11    show_id: MenuId,
12    quit_id: MenuId,
13}
14
15impl std::fmt::Debug for TrayState {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        f.debug_struct("TrayState")
18            .field("_tray", &"<TrayIcon>")
19            .field("show_id", &self.show_id)
20            .field("quit_id", &self.quit_id)
21            .finish()
22    }
23}
24
25/// Create a system tray icon with Show and Quit menu items.
26pub(crate) fn create_tray() -> Option<TrayState> {
27    let icon = load_icon();
28
29    let show_item = MenuItem::new("Show", true, None);
30    let quit_item = MenuItem::new("Quit", true, None);
31
32    let show_id = show_item.id().clone();
33    let quit_id = quit_item.id().clone();
34
35    let menu = Menu::new();
36
37    menu.append_items(&[&show_item, &PredefinedMenuItem::separator(), &quit_item])
38        .ok()?;
39
40    let tray = TrayIconBuilder::new()
41        .with_menu(Box::new(menu))
42        .with_tooltip("WoW Lab Node")
43        .with_icon(icon)
44        .build()
45        .ok()?;
46
47    Some(TrayState {
48        _tray: tray,
49        show_id,
50        quit_id,
51    })
52}
53
54/// Poll for tray icon clicks and menu selections without blocking.
55pub(crate) fn poll_tray_events(tray: &TrayState) -> TrayAction {
56    if let Ok(event) = MenuEvent::receiver().try_recv() {
57        if event.id == tray.show_id {
58            return TrayAction::Show;
59        } else if event.id == tray.quit_id {
60            return TrayAction::Quit;
61        }
62    }
63
64    if let Ok(TrayIconEvent::DoubleClick { .. }) = TrayIconEvent::receiver().try_recv() {
65        return TrayAction::Show;
66    }
67
68    TrayAction::None
69}
70
71/// User action received from the system tray.
72#[derive(Debug)]
73#[non_exhaustive]
74pub(crate) enum TrayAction {
75    None,
76    Show,
77    Quit,
78}
79
80fn load_icon() -> tray_icon::Icon {
81    let icon_bytes = include_bytes!("../assets/icon.png");
82    let img = image::load_from_memory(icon_bytes)
83        .expect("Failed to load tray icon")
84        .into_rgba8();
85    let (w, h) = img.dimensions();
86
87    tray_icon::Icon::from_rgba(img.into_raw(), w, h).expect("Failed to create tray icon")
88}
89
90#[cfg(test)]
91mod tests {
92    use googletest::prelude::*;
93
94    #[gtest]
95    fn embedded_tray_icon_is_the_expected_rgba_asset() -> Result<()> {
96        let image = image::load_from_memory(include_bytes!("../assets/icon.png"))
97            .or_fail()?
98            .into_rgba8();
99
100        verify_that!(image.dimensions(), eq((180, 180)))?;
101
102        verify_that!(
103            image.get_pixel(0, 0).0,
104            elements_are![anything(), anything(), anything(), eq(0)]
105        )
106    }
107}