1use tray_icon::{
4 TrayIcon, TrayIconBuilder, TrayIconEvent,
5 menu::{Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem},
6};
7
8pub(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
25pub(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
54pub(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#[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}