tray_wrapper/
tray_wrapper.rs

1use crate::{
2    menu_state::MenuState,
3    server_generator::{ContinueRunning, ServerGenerator},
4    server_status::ServerStatus,
5    user_event::UserEvent,
6};
7use image::ImageError;
8use std::time::Duration;
9use take_once::TakeOnce;
10use thiserror::Error;
11use tokio::runtime::Runtime;
12use tray_icon::{BadIcon, Icon};
13use winit::{application::ApplicationHandler, event_loop::EventLoopProxy};
14
15/// This is the main entry point / handle for the wrapper
16pub struct TrayWrapper {
17    icon: Icon,
18    menu_state: Option<MenuState>,
19    runtime: Option<Runtime>,
20    event_loop_proxy: EventLoopProxy<UserEvent>,
21    server_generator: TakeOnce<ServerGenerator>,
22}
23
24impl TrayWrapper {
25    ///Construct the wrapper, its recommended you compile time load the icon which means you
26    /// can ignore image parsing errors.
27    pub fn new(
28        icon_data: &[u8],
29        event_loop_proxy: EventLoopProxy<UserEvent>,
30        server_gen: ServerGenerator,
31    ) -> Result<Self, TrayWrapperError> {
32        let image = image::load_from_memory(icon_data)?.into_rgba8();
33
34        let (width, height) = image.dimensions();
35        let rgba = image.into_raw();
36        let icon = Icon::from_rgba(rgba, width, height)?;
37        let server_generator = TakeOnce::new_with(server_gen);
38
39        Ok(TrayWrapper {
40            icon,
41            menu_state: None,
42            runtime: Some(Runtime::new()?),
43
44            event_loop_proxy,
45            server_generator,
46        })
47    }
48}
49
50// This implementation is from the winit example here: https://github.com/tauri-apps/tray-icon/blob/dev/examples/winit.rs
51impl ApplicationHandler<UserEvent> for TrayWrapper {
52    fn resumed(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {}
53
54    fn window_event(
55        &mut self,
56        _event_loop: &winit::event_loop::ActiveEventLoop,
57        _window_id: winit::window::WindowId,
58        _event: winit::event::WindowEvent,
59    ) {
60    }
61
62    fn new_events(
63        &mut self,
64        _event_loop: &winit::event_loop::ActiveEventLoop,
65        cause: winit::event::StartCause,
66    ) {
67        // We create the icon once the event loop is actually running
68        // to prevent issues like https://github.com/tauri-apps/tray-icon/issues/90
69        if winit::event::StartCause::Init == cause {
70            let Ok(mut ms) = MenuState::new(self.icon.clone()) else {
71                return _event_loop.exit();
72            };
73            ms.update_tray_icon(ServerStatus::StartUp); //The error type doesn't matter in this case
74            self.menu_state = Some(ms);
75
76            //Now its time to really start the server
77            let Some(rt) = &self.runtime else {
78                return _event_loop.exit();
79            };
80
81            let sg = self
82                .server_generator
83                .take()
84                .expect("Unable to take generator function");
85            let elp = self.event_loop_proxy.clone();
86            rt.spawn(async move {
87                let sg_fn = sg;
88                loop {
89                    let next_run = sg_fn();
90                    elp.send_event(UserEvent::ServerStatusEvent(ServerStatus::Running))
91                        .expect("Event Loop Closed!");
92                    match next_run.await {
93                        ContinueRunning::Continue => {
94                            elp.send_event(UserEvent::ServerStatusEvent(ServerStatus::Stopped(
95                                "Server Exited, will start again".to_string(),
96                            )))
97                            .expect("Event Loop Closed!");
98                            continue;
99                        }
100                        ContinueRunning::Exit => {
101                            elp.send_event(UserEvent::ServerExitEvent)
102                                .expect("Event Loop Closed!");
103                            break;
104                        }
105                        ContinueRunning::ExitWithError(e) => {
106                            elp.send_event(UserEvent::ServerStatusEvent(ServerStatus::Error(
107                                e.to_string(),
108                            )))
109                            .expect("Event Loop Closed!");
110                            break;
111                        }
112                    }
113                }
114            });
115        }
116
117        // We have to request a redraw here to have the icon actually show up.
118        // Winit only exposes a redraw method on the Window so we use core-foundation directly-ish.
119        #[cfg(target_os = "macos")]
120        {
121            use objc2_core_foundation::CFRunLoop;
122            let rl = CFRunLoop::main().unwrap();
123            CFRunLoop::wake_up(&rl);
124        }
125    }
126
127    fn user_event(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop, event: UserEvent) {
128        if let UserEvent::ServerExitEvent = event {
129            if let Some(rt) = self.runtime.take() {
130                rt.shutdown_timeout(Duration::from_secs(10));
131            }
132            _event_loop.exit();
133        }
134
135        if let Some(ms) = &self.menu_state
136            && ms.quit_matches(event)
137        {
138            if let Some(rt) = self.runtime.take() {
139                rt.shutdown_timeout(Duration::from_secs(10));
140            }
141            _event_loop.exit();
142        }
143    }
144}
145
146#[derive(Error, Debug)]
147pub enum TrayWrapperError {
148    #[error("Unable to load the icon from buffer")]
149    IconLoad(#[from] ImageError),
150    #[error("Tray Icon Bad Icon")]
151    BadIcon(#[from] BadIcon),
152    #[error("Failure to pre-create menu")]
153    MenuError(#[from] tray_icon::menu::Error),
154    #[error(transparent)]
155    RunTime(#[from] std::io::Error),
156}