Rewild Your Web
web browser dweb
at main 216 lines 7.0 kB view raw
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}