Rewild Your Web
web
browser
dweb
1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3//! System tray icon and menu management.
4
5#[cfg(target_os = "linux")]
6use std::sync::mpsc;
7
8use image::ImageReader;
9use log::warn;
10#[cfg(not(target_os = "linux"))]
11use tray_icon::TrayIcon;
12use tray_icon::menu::{Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem};
13use tray_icon::{Icon, TrayIconBuilder};
14use winit::event_loop::EventLoopProxy;
15
16use crate::WakerEvent;
17
18/// Identifiers for tray menu items.
19#[derive(Clone, Debug)]
20pub(crate) struct TrayMenuIds {
21 pub show_all: MenuId,
22 pub hide_all: MenuId,
23 pub search: MenuId,
24 pub settings: MenuId,
25 pub quit: MenuId,
26}
27
28/// Manages the system tray icon and its associated menu.
29pub(crate) struct Tray {
30 /// On non-Linux platforms, we hold the TrayIcon directly.
31 /// On Linux, the TrayIcon lives in a separate GTK thread.
32 #[cfg(not(target_os = "linux"))]
33 _icon: TrayIcon,
34 #[cfg(target_os = "linux")]
35 _gtk_thread: std::thread::JoinHandle<()>,
36 menu_ids: TrayMenuIds,
37}
38
39impl Tray {
40 /// Creates a new system tray icon with menu.
41 /// Returns None if the tray icon cannot be created (e.g., on GNOME without
42 /// the AppIndicator extension installed).
43 #[cfg(not(target_os = "linux"))]
44 pub fn new(event_loop_proxy: EventLoopProxy<WakerEvent>) -> Option<Self> {
45 let (menu_ids, menu) = create_menu();
46 let icon = load_icon()?;
47
48 let tray_icon = match TrayIconBuilder::new()
49 .with_menu(Box::new(menu))
50 .with_icon(icon)
51 .with_tooltip("Browser.HTML")
52 .build()
53 {
54 Ok(icon) => icon,
55 Err(e) => {
56 warn!(
57 "Failed to create tray icon: {e}. The app will exit when all windows are closed."
58 );
59 return None;
60 },
61 };
62
63 // Set up menu event forwarding to the winit event loop
64 MenuEvent::set_event_handler(Some(move |event: MenuEvent| {
65 let _ = event_loop_proxy.send_event(WakerEvent::TrayMenu(event.id));
66 }));
67
68 Some(Self {
69 _icon: tray_icon,
70 menu_ids,
71 })
72 }
73
74 /// Creates a new system tray icon with menu on Linux.
75 /// On Linux, GTK must run in its own thread since winit doesn't use GTK.
76 #[cfg(target_os = "linux")]
77 pub fn new(event_loop_proxy: EventLoopProxy<WakerEvent>) -> Option<Self> {
78 // Create menu IDs first so we can return them
79 let (menu_ids, _) = create_menu();
80 let menu_ids_clone = menu_ids.clone();
81
82 // Use a channel to communicate success/failure from the GTK thread
83 let (tx, rx) = mpsc::channel();
84
85 let gtk_thread = std::thread::spawn(move || {
86 if gtk::init().is_err() {
87 let _ = tx.send(false);
88 return;
89 }
90
91 let (_, menu) = create_menu_with_ids(&menu_ids_clone);
92 let Some(icon) = load_icon() else {
93 let _ = tx.send(false);
94 return;
95 };
96
97 let tray_icon = match TrayIconBuilder::new()
98 .with_menu(Box::new(menu))
99 .with_icon(icon)
100 .with_tooltip("Browser.HTML")
101 .build()
102 {
103 Ok(icon) => icon,
104 Err(e) => {
105 warn!(
106 "Failed to create tray icon: {e}. The app will exit when all windows are closed."
107 );
108 let _ = tx.send(false);
109 return;
110 },
111 };
112
113 // Set up menu event forwarding to the winit event loop
114 MenuEvent::set_event_handler(Some(move |event: MenuEvent| {
115 let _ = event_loop_proxy.send_event(WakerEvent::TrayMenu(event.id));
116 }));
117
118 // Signal success
119 let _ = tx.send(true);
120
121 // Keep the tray icon alive by preventing it from being dropped
122 let _tray_icon = tray_icon;
123
124 // Run the GTK main loop
125 gtk::main();
126 });
127
128 // Wait for the GTK thread to report success or failure
129 match rx.recv() {
130 Ok(true) => Some(Self {
131 _gtk_thread: gtk_thread,
132 menu_ids,
133 }),
134 _ => {
135 warn!(
136 "Failed to initialize tray icon on Linux. The app will exit when all windows are closed."
137 );
138 None
139 },
140 }
141 }
142
143 /// Returns the menu IDs for matching against menu events.
144 pub fn menu_ids(&self) -> &TrayMenuIds {
145 &self.menu_ids
146 }
147}
148
149/// Create the menu and return the menu IDs along with the menu.
150fn create_menu() -> (TrayMenuIds, Menu) {
151 let show_all = MenuItem::new("Show all windows", true, None);
152 let hide_all = MenuItem::new("Hide all windows", true, None);
153 let search = MenuItem::new("Search", true, None);
154 let settings = MenuItem::new("Settings", true, None);
155 let quit = MenuItem::new("Quit", true, None);
156
157 let menu_ids = TrayMenuIds {
158 show_all: show_all.id().clone(),
159 hide_all: hide_all.id().clone(),
160 search: search.id().clone(),
161 settings: settings.id().clone(),
162 quit: quit.id().clone(),
163 };
164
165 let menu = Menu::new();
166 #[cfg(not(target_os = "linux"))]
167 {
168 let _ = menu.append(&show_all);
169 let _ = menu.append(&hide_all);
170 }
171 let _ = menu.append(&search);
172 let _ = menu.append(&settings);
173 let _ = menu.append(&PredefinedMenuItem::separator());
174 let _ = menu.append(&quit);
175
176 (menu_ids, menu)
177}
178
179/// Create a menu using pre-existing menu IDs (for Linux where IDs must match).
180#[cfg(target_os = "linux")]
181fn create_menu_with_ids(ids: &TrayMenuIds) -> (TrayMenuIds, Menu) {
182 #[cfg(not(target_os = "linux"))]
183 {
184 let show_all = MenuItem::with_id(ids.show_all.clone(), "Show all windows", true, None);
185 let hide_all = MenuItem::with_id(ids.hide_all.clone(), "Hide all windows", true, None);
186 }
187 let search = MenuItem::with_id(ids.search.clone(), "Search", true, None);
188 let settings = MenuItem::with_id(ids.settings.clone(), "Settings", true, None);
189 let quit = MenuItem::with_id(ids.quit.clone(), "Quit", true, None);
190
191 let menu = Menu::new();
192 #[cfg(not(target_os = "linux"))]
193 {
194 let _ = menu.append(&show_all);
195 let _ = menu.append(&hide_all);
196 }
197 let _ = menu.append(&search);
198 let _ = menu.append(&settings);
199 let _ = menu.append(&PredefinedMenuItem::separator());
200 let _ = menu.append(&quit);
201
202 (ids.clone(), menu)
203}
204
205/// Load the tray icon from embedded bytes.
206fn load_icon() -> Option<Icon> {
207 let file_path = crate::resources::resources_dir_path()
208 .join("browserhtml")
209 .join("system")
210 .join("logo.png");
211
212 let img = ImageReader::open(file_path).ok()?.decode().ok()?;
213 let rgba = img.to_rgba8();
214
215 Icon::from_rgba(rgba.to_vec(), img.width(), img.height()).ok()
216}