tray_wrapper/
tray_wrapper.rs

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