Rewild Your Web
web browser dweb
at main 843 lines 30 kB view raw
1// SPDX-License-Identifier: AGPL-3.0-or-later 2 3mod browser_window; 4#[cfg(feature = "global-hotkeys")] 5mod hotkeys; 6mod keyutils; 7mod prefs; 8mod protocols; 9mod resources; 10mod touch_event_simulator; 11#[cfg(feature = "status-tray")] 12mod tray; 13mod vhost; 14 15use std::cell::{Cell, RefCell}; 16use std::collections::HashMap; 17use std::error::Error; 18use std::path::PathBuf; 19use std::rc::Rc; 20 21use chrono::{DateTime, Local}; 22use euclid::Scale; 23#[cfg(feature = "global-hotkeys")] 24use hotkeys::HotkeyManager; 25use log::{error, info, warn}; 26use protocols::resource::ResourceProtocolHandler; 27use servo::prefs::get_embedder_pref; 28use servo::protocol_handler::ProtocolRegistry; 29use servo::{ 30 AllowOrDenyRequest, BrowsingContextId, ConsoleLogLevel, CreateNewWebViewRequest, Cursor, 31 EmbedderControl, EmbedderControlId, InputEventId, InputEventResult, Notification, Opts, 32 PipelineId, PrefValue, Preferences, RenderingContext, Servo, ServoBuilder, ServoDelegate, 33 ServoError, WebView, WebViewBuilder, WebViewDelegate, WebViewId, WindowRenderingContext, 34}; 35#[cfg(feature = "status-tray")] 36use tray::Tray; 37#[cfg(feature = "status-tray")] 38use tray_icon::menu::MenuId; 39use url::Url; 40use winit::application::ApplicationHandler; 41use winit::event::WindowEvent; 42use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}; 43#[cfg(target_os = "macos")] 44use winit::platform::macos::WindowAttributesExtMacOS; 45use winit::raw_window_handle::{HasDisplayHandle, HasWindowHandle}; 46use winit::window::{CursorIcon, WindowAttributes, WindowLevel}; 47 48use crate::browser_window::{BrowserWindow, BrowserWindowId, UserInterfaceCommand}; 49use crate::prefs::enable_experimental_prefs; 50 51/// Returns the profile name, or "default" if none is set. 52fn profile_name() -> String { 53 std::env::var("BROWSERHTML_PROFILE").unwrap_or_else(|_| "default".into()) 54} 55 56/// Get the configuration directory for browserhtml. 57/// Returns None if the directory cannot be determined. 58fn config_dir() -> Option<PathBuf> { 59 #[cfg(target_os = "macos")] 60 { 61 dirs::data_dir().map(|d| d.join("BrowserHTML").join(profile_name())) 62 } 63 #[cfg(not(target_os = "macos"))] 64 { 65 dirs::config_dir().map(|d| d.join("browserhtml").join(profile_name())) 66 } 67} 68 69fn main() -> Result<(), Box<dyn Error>> { 70 rustls::crypto::aws_lc_rs::default_provider() 71 .install_default() 72 .expect("Failed to install crypto provider"); 73 crate::resources::init(); 74 75 // Load browserhtml preferences from config directory 76 if let Some(prefs_path) = config_dir().map(|d| d.join("prefs.json")) { 77 if let Err(e) = prefs::load(&prefs_path) { 78 log::warn!("Failed to load browserhtml preferences: {}", e); 79 } 80 } 81 82 // Check if this is the main process or a content process. 83 // content processes get a "--content-process /tmp/.tmpTwE4hh/socket" parameter. 84 let args: Vec<_> = std::env::args().collect(); 85 if let Some(token) = args 86 .windows(2) 87 .find(|arg| arg[0] == "--content-process") 88 .map(|arg| arg[1].clone()) 89 { 90 servo::run_content_process(token); 91 return Ok(()); 92 } 93 94 let event_loop = EventLoop::with_user_event() 95 .build() 96 .expect("Failed to create EventLoop"); 97 let mut app = App::new(&event_loop); 98 Ok(event_loop.run_app(&mut app)?) 99} 100 101/// Global application state that holds all windows and the shared Servo instance. 102struct AppState { 103 servo: Servo, 104 windows: RefCell<HashMap<BrowserWindowId, Rc<BrowserWindow>>>, 105 focused_window: RefCell<Option<Rc<BrowserWindow>>>, 106 #[cfg(feature = "status-tray")] 107 tray: Option<Tray>, 108 exit_requested: Cell<bool>, 109 #[cfg(feature = "global-hotkeys")] 110 hotkey_manager: Option<HotkeyManager>, 111} 112 113impl AppState { 114 /// Look up a window by its ID. 115 fn window(&self, id: BrowserWindowId) -> Option<Rc<BrowserWindow>> { 116 self.windows.borrow().get(&id).cloned() 117 } 118 119 /// Explicitly shut down Servo. This is needed because there's a reference 120 /// cycle between AppState and Servo (via the delegate), which prevents 121 /// the normal Drop-based cleanup from running. We need to: 122 /// 1. Clear all windows (which hold WebViews that reference Servo) 123 /// 2. Set a dummy delegate to break the AppState <-> Servo cycle 124 fn shutdown(&self) { 125 // Clear focused_window reference 126 *self.focused_window.borrow_mut() = None; 127 // Clear windows - this drops WebViews which hold Servo references 128 self.windows.borrow_mut().clear(); 129 // Break the delegate cycle 130 self.servo.set_delegate(Rc::new(ShutdownDelegate)); 131 } 132 133 /// Find the window containing the given WebView. 134 fn window_for_webview_id(&self, webview_id: WebViewId) -> Option<Rc<BrowserWindow>> { 135 for window in self.windows.borrow().values() { 136 if window.contains_webview(webview_id) { 137 return Some(window.clone()); 138 } 139 } 140 None 141 } 142 143 /// Open a new browser window. 144 fn open_window( 145 self: &Rc<Self>, 146 event_loop: &ActiveEventLoop, 147 url: Url, 148 features: Option<String>, 149 ) -> Rc<BrowserWindow> { 150 let mut with_decorations = true; 151 let mut with_resizable = true; 152 let mut with_window_level = WindowLevel::Normal; 153 154 for feature in features.unwrap_or_default().split(',') { 155 if feature == "noresize" { 156 with_resizable = false; 157 } else if feature == "notitle" { 158 with_decorations = false; 159 } else if feature == "ontop" { 160 with_window_level = WindowLevel::AlwaysOnTop; 161 } else if feature == "onbottom" { 162 with_window_level = WindowLevel::AlwaysOnBottom; 163 } 164 } 165 166 let display_handle = event_loop 167 .display_handle() 168 .expect("Failed to get display handle"); 169 170 // Build window attributes 171 let mut window_attributes = WindowAttributes::default(); 172 window_attributes = window_attributes 173 .with_transparent(true) 174 .with_decorations(with_decorations) 175 .with_resizable(with_resizable) 176 .with_window_level(with_window_level); 177 178 // On macOS, disable shadow for frameless windows to avoid double border effect 179 #[cfg(target_os = "macos")] 180 if !with_decorations { 181 window_attributes = window_attributes.with_has_shadow(false); 182 } 183 184 // In Mobile simulation mode, start with a smaller window size 185 let simulate_touch = matches!( 186 get_embedder_pref("browserhtml.mobile_simulation"), 187 Some(PrefValue::Bool(true)) 188 ); 189 190 if simulate_touch { 191 window_attributes = window_attributes.with_inner_size(winit::dpi::PhysicalSize { 192 width: 650, 193 height: 1000, 194 }); 195 } 196 197 let window = event_loop 198 .create_window(window_attributes) 199 .expect("Failed to create winit Window"); 200 let window_handle = window.window_handle().expect("Failed to get window handle"); 201 202 let rendering_context = Rc::new( 203 WindowRenderingContext::new(display_handle, window_handle, window.inner_size()) 204 .expect("Could not create RenderingContext for window."), 205 ); 206 207 let _ = rendering_context.make_current(); 208 209 // Check if mobile simulation is enabled 210 let simulate_touch = matches!( 211 get_embedder_pref("browserhtml.mobile_simulation"), 212 Some(PrefValue::Bool(true)) 213 ); 214 215 let browser_window = Rc::new(BrowserWindow::new( 216 window, 217 rendering_context.clone(), 218 simulate_touch, 219 )); 220 221 let id = browser_window.id(); 222 self.windows.borrow_mut().insert(id, browser_window.clone()); 223 224 // Create the initial WebView for this window 225 let webview = WebViewBuilder::new(&self.servo, rendering_context) 226 .url(url) 227 .hidpi_scale_factor(Scale::new(browser_window.window.scale_factor() as f32)) 228 .delegate(self.clone()) 229 .build(); 230 231 let webview_id = webview.id(); 232 browser_window 233 .webviews 234 .borrow_mut() 235 .insert(webview_id, webview); 236 237 // Set this as the system webview (the browserhtml shell UI) 238 browser_window.system_webview_id.set(Some(webview_id)); 239 240 browser_window 241 } 242 243 /// Close a window by its ID. 244 fn close_window(&self, id: BrowserWindowId) { 245 self.windows.borrow_mut().remove(&id); 246 // Clear focused_window if it was this one 247 if self 248 .focused_window 249 .borrow() 250 .as_ref() 251 .is_some_and(|w| w.id() == id) 252 { 253 *self.focused_window.borrow_mut() = None; 254 } 255 } 256 257 /// Process pending commands for all windows. 258 fn process_pending_commands(self: &Rc<Self>, event_loop: &ActiveEventLoop) { 259 // Collect commands from all windows first to avoid borrow conflicts 260 let commands: Vec<_> = self 261 .windows 262 .borrow() 263 .values() 264 .flat_map(|w| w.take_pending_commands()) 265 .collect(); 266 267 for command in commands { 268 match command { 269 UserInterfaceCommand::NewWindow(maybe_url, maybe_features) => { 270 let url = maybe_url.unwrap_or( 271 Url::parse("http://system.localhost:8888/index.html") 272 .expect("Invalid default URL"), 273 ); 274 self.open_window(event_loop, url, maybe_features); 275 }, 276 } 277 } 278 } 279 280 /// Handle a tray menu event. 281 #[cfg(feature = "status-tray")] 282 fn handle_tray_menu_event(self: &Rc<Self>, event_loop: &ActiveEventLoop, menu_id: &MenuId) { 283 let Some(tray) = &self.tray else { 284 return; 285 }; 286 let ids = tray.menu_ids(); 287 288 if menu_id == &ids.show_all { 289 self.show_all_windows(); 290 } else if menu_id == &ids.hide_all { 291 self.hide_all_windows(); 292 } else if menu_id == &ids.search { 293 self.open_search_window(event_loop); 294 } else if menu_id == &ids.settings { 295 self.open_settings_window(event_loop); 296 } else if menu_id == &ids.quit { 297 // TODO: Make sure to close all windows first. 298 self.exit_requested.set(true); 299 } 300 } 301 302 /// Show and focus all windows. 303 #[cfg(feature = "status-tray")] 304 fn show_all_windows(&self) { 305 for window in self.windows.borrow().values() { 306 window.window.set_visible(true); 307 window.window.focus_window(); 308 } 309 } 310 311 /// Hide all windows. 312 #[cfg(feature = "status-tray")] 313 fn hide_all_windows(&self) { 314 for window in self.windows.borrow().values() { 315 println!("hiding window {:?}", window.window.id()); 316 window.window.set_visible(false); 317 } 318 } 319 320 /// Open a new search window, centered on screen. 321 /// TODO: switch to a real search window. 322 #[cfg(any(feature = "status-tray", feature = "global-hotkeys"))] 323 fn open_search_window(self: &Rc<Self>, event_loop: &ActiveEventLoop) { 324 let url = Url::parse("http://system.localhost:8888/search.html").expect("Invalid URL"); 325 let window = self.open_window(event_loop, url, Some("notitle,ontop".into())); 326 327 // Try to center the window on the primary monitor 328 if let Some(monitor) = event_loop 329 .primary_monitor() 330 .or_else(|| event_loop.available_monitors().next()) 331 { 332 let screen_size = monitor.size(); 333 let window_size = window.window.outer_size(); 334 let x = (screen_size.width.saturating_sub(window_size.width)) / 2; 335 let y = (screen_size.height.saturating_sub(window_size.height)) / 2; 336 let position = winit::dpi::PhysicalPosition::new( 337 x as i32 + monitor.position().x, 338 y as i32 + monitor.position().y, 339 ); 340 println!("Moving window ({window_size:?}) to {position:?}"); 341 window.window.set_outer_position(position); 342 } 343 } 344 345 /// Open the settings window. 346 #[cfg(feature = "status-tray")] 347 fn open_settings_window(self: &Rc<Self>, event_loop: &ActiveEventLoop) { 348 let url = Url::parse("http://system.localhost:8888/index.html?open=http%3A%2F%2Fsettings.localhost%3A8888%2Findex.html") 349 .expect("Invalid URL"); 350 self.open_window(event_loop, url, None); 351 } 352 353 fn window_if_system_webview(&self, webview: &WebView) -> Option<Rc<BrowserWindow>> { 354 let Some(ref window) = self.window_for_webview_id(webview.id()) else { 355 return None; 356 }; 357 358 let Some(system_webview_id) = window.system_webview_id.get() else { 359 return None; 360 }; 361 362 if webview.id() != system_webview_id { 363 return None; 364 } 365 366 Some(window.clone()) 367 } 368} 369 370impl WebViewDelegate for AppState { 371 fn notify_new_frame_ready(&self, webview: WebView) { 372 if let Some(window) = self.window_for_webview_id(webview.id()) { 373 window.window.request_redraw(); 374 } 375 } 376 377 fn show_console_message(&self, _webview: WebView, level: ConsoleLogLevel, message: String) { 378 let current_local: DateTime<Local> = Local::now(); 379 let time = current_local.format("%T%.3f"); 380 println!("[{time}][{level:?}] {message}"); 381 } 382 383 fn request_create_embedded(&self, parent_webview: WebView, request: CreateNewWebViewRequest) { 384 let Some(window) = self.window_for_webview_id(parent_webview.id()) else { 385 return; 386 }; 387 388 // Create the embedded webview using the same rendering context as the parent. 389 // This is required for embedded webviews to be composited within the parent's scene. 390 let webview = request 391 .builder(window.rendering_context.clone()) 392 .hidpi_scale_factor(Scale::new(window.window.scale_factor() as f32)) 393 .delegate(parent_webview.delegate()) 394 .build(); 395 396 // Store the webview so it doesn't get dropped 397 window.webviews.borrow_mut().insert(webview.id(), webview); 398 } 399 400 fn notify_cursor_changed(&self, webview: WebView, cursor: Cursor) { 401 let Some(window) = self.window_for_webview_id(webview.id()) else { 402 return; 403 }; 404 405 let winit_cursor = match cursor { 406 Cursor::Default => CursorIcon::Default, 407 Cursor::Pointer => CursorIcon::Pointer, 408 Cursor::ContextMenu => CursorIcon::ContextMenu, 409 Cursor::Help => CursorIcon::Help, 410 Cursor::Progress => CursorIcon::Progress, 411 Cursor::Wait => CursorIcon::Wait, 412 Cursor::Cell => CursorIcon::Cell, 413 Cursor::Crosshair => CursorIcon::Crosshair, 414 Cursor::Text => CursorIcon::Text, 415 Cursor::VerticalText => CursorIcon::VerticalText, 416 Cursor::Alias => CursorIcon::Alias, 417 Cursor::Copy => CursorIcon::Copy, 418 Cursor::Move => CursorIcon::Move, 419 Cursor::NoDrop => CursorIcon::NoDrop, 420 Cursor::NotAllowed => CursorIcon::NotAllowed, 421 Cursor::Grab => CursorIcon::Grab, 422 Cursor::Grabbing => CursorIcon::Grabbing, 423 Cursor::EResize => CursorIcon::EResize, 424 Cursor::NResize => CursorIcon::NResize, 425 Cursor::NeResize => CursorIcon::NeResize, 426 Cursor::NwResize => CursorIcon::NwResize, 427 Cursor::SResize => CursorIcon::SResize, 428 Cursor::SeResize => CursorIcon::SeResize, 429 Cursor::SwResize => CursorIcon::SwResize, 430 Cursor::WResize => CursorIcon::WResize, 431 Cursor::EwResize => CursorIcon::EwResize, 432 Cursor::NsResize => CursorIcon::NsResize, 433 Cursor::NeswResize => CursorIcon::NeswResize, 434 Cursor::NwseResize => CursorIcon::NwseResize, 435 Cursor::ColResize => CursorIcon::ColResize, 436 Cursor::RowResize => CursorIcon::RowResize, 437 Cursor::AllScroll => CursorIcon::AllScroll, 438 Cursor::ZoomIn => CursorIcon::ZoomIn, 439 Cursor::ZoomOut => CursorIcon::ZoomOut, 440 Cursor::None => { 441 window.window.set_cursor_visible(false); 442 return; 443 }, 444 }; 445 window.window.set_cursor(winit_cursor); 446 window.window.set_cursor_visible(true); 447 } 448 449 fn notify_focus_changed(&self, webview: WebView, focused: bool) { 450 if let Some(window) = self.window_for_webview_id(webview.id()) { 451 if focused { 452 window.focused_webview_id.set(Some(webview.id())); 453 } else if window.focused_webview_id.get() == Some(webview.id()) { 454 // Only clear if this webview was the focused one 455 window.focused_webview_id.set(None); 456 } 457 } 458 } 459 460 fn notify_closed(&self, webview: WebView) { 461 if let Some(window) = self.window_for_webview_id(webview.id()) { 462 window.webviews.borrow_mut().remove(&webview.id()); 463 } 464 } 465 466 fn notify_embedded_webview_created( 467 &self, 468 parent_webview: WebView, 469 new_webview_id: WebViewId, 470 new_browsing_context_id: BrowsingContextId, 471 new_pipeline_id: PipelineId, 472 url: Url, 473 ) { 474 // Call JavaScript API on the parent (browserhtml shell) to create a new tab 475 // that adopts the pre-created webview using the provided IDs. 476 // The IDs are serialized as strings since their internal structure is opaque. 477 let script = format!( 478 "window.servo?.adoptNewWebView({}, {}, {}, {})", 479 serde_json::to_string(url.as_str()).unwrap_or_else(|_| "null".to_string()), 480 serde_json::to_string(&new_webview_id.to_string()) 481 .unwrap_or_else(|_| "null".to_string()), 482 serde_json::to_string(&new_browsing_context_id.to_string()) 483 .unwrap_or_else(|_| "null".to_string()), 484 serde_json::to_string(&new_pipeline_id.to_string()) 485 .unwrap_or_else(|_| "null".to_string()) 486 ); 487 parent_webview.evaluate_javascript(&script, move |result| { 488 if let Err(error) = result { 489 error!("Failed to adopt new WebView: {error:?}"); 490 } else { 491 info!("adoptNewWebView() succeeded"); 492 } 493 }); 494 } 495 496 fn notify_page_title_changed(&self, webview: WebView, title: Option<String>) { 497 let Some(window) = self.window_if_system_webview(&webview) else { 498 return; 499 }; 500 501 let Some(title) = title else { 502 return; 503 }; 504 505 if title.is_empty() { 506 return; 507 } 508 509 window.window.set_title(&title); 510 } 511 512 fn notify_input_event_handled( 513 &self, 514 webview: WebView, 515 event_id: InputEventId, 516 result: InputEventResult, 517 ) { 518 if let Some(window) = self.window_for_webview_id(webview.id()) { 519 window.handle_keyboard_event_result(event_id, result); 520 } 521 } 522 523 fn show_notification(&self, webview: WebView, notification: Notification) { 524 // For browserhtml, notifications from embedded webviews are routed through 525 // the constellation and arrive as iframe events (embednotificationshow). 526 // This delegate method is only called for non-embedded webviews, which 527 // don't exist in browserhtml's architecture. 528 log::debug!( 529 "Notification from non-embedded webview {:?}: {:?}", 530 webview.id(), 531 notification.title 532 ); 533 } 534 535 fn show_embedder_control(&self, webview: WebView, embedder_control: EmbedderControl) { 536 if self.window_if_system_webview(&webview).is_none() { 537 return; 538 }; 539 540 let EmbedderControl::InputMethod(ime) = embedder_control else { 541 warn!("Not an input field, ignoring"); 542 return; 543 }; 544 545 let script = format!( 546 "window.servo?.openKeyboard({}, {}, {})", 547 serde_json::to_string(&ime.input_method_type()).unwrap_or_else(|_| "null".to_string()), 548 serde_json::to_string(&ime.text()).unwrap_or_else(|_| "null".to_string()), 549 serde_json::to_string(&ime.insertion_point().unwrap_or(0)) 550 .unwrap_or_else(|_| "null".to_string()) 551 ); 552 webview.evaluate_javascript(&script, move |result| { 553 if let Err(error) = result { 554 error!("Failed to openKeyboard: {error:?}"); 555 } else { 556 info!("openKeyboard() succeeded"); 557 } 558 }); 559 } 560 561 fn hide_embedder_control(&self, webview: WebView, control_id: EmbedderControlId) { 562 if self.window_if_system_webview(&webview).is_none() { 563 return; 564 }; 565 566 let script = format!( 567 "window.servo?.closeEmbedderControl({})", 568 serde_json::to_string(&control_id).unwrap_or_else(|_| "null".to_string()) 569 ); 570 webview.evaluate_javascript(&script, move |result| { 571 if let Err(error) = result { 572 error!("Failed to closeEmbedderControl: {error:?}"); 573 } else { 574 info!("closeEmbedderControl() succeeded"); 575 } 576 }); 577 } 578} 579 580impl ServoDelegate for AppState { 581 fn notify_devtools_server_started(&self, port: u16, _token: String) { 582 info!("Devtools Server running on port {port}"); 583 } 584 585 fn request_devtools_connection(&self, request: AllowOrDenyRequest) { 586 request.allow(); 587 } 588 589 fn notify_error(&self, error: ServoError) { 590 error!("Saw Servo error: {error:?}!"); 591 } 592 593 fn show_console_message(&self, level: ConsoleLogLevel, message: String) { 594 let current_local: DateTime<Local> = Local::now(); 595 let time = current_local.format("%T%.3f"); 596 println!("[{time}][{level:?}] {message}"); 597 } 598 599 fn request_open_new_os_window(&self, url: Url, features: &str) { 600 if let Some(window) = self.focused_window.borrow().as_ref() { 601 let features = if features.is_empty() { 602 None 603 } else { 604 Some(features.to_owned()) 605 }; 606 window.queue_command(UserInterfaceCommand::NewWindow(Some(url), features)); 607 } 608 } 609 610 fn request_close_current_os_window(&self) { 611 let Some(id) = self.focused_window.borrow().as_ref().map(|w| w.id()) else { 612 return; 613 }; 614 self.close_window(id); 615 } 616 617 fn request_exit_application(&self) { 618 self.exit_requested.set(true); 619 } 620 621 fn request_start_window_drag(&self, _webview_id: WebViewId) { 622 let Some(id) = self.focused_window.borrow().as_ref().map(|w| w.id()) else { 623 return; 624 }; 625 if let Some(window) = self.window(id) { 626 let _ = window.window.drag_window(); 627 } 628 } 629 630 fn request_start_window_resize(&self, _webview_id: WebViewId) { 631 let Some(id) = self.focused_window.borrow().as_ref().map(|w| w.id()) else { 632 return; 633 }; 634 if let Some(window) = self.window(id) { 635 let _ = window 636 .window 637 .drag_resize_window(winit::window::ResizeDirection::NorthWest); 638 } 639 } 640} 641 642/// A minimal ServoDelegate used to break the reference cycle between 643/// AppState and Servo during shutdown. This allows Servo's Drop to run. 644struct ShutdownDelegate; 645 646impl ServoDelegate for ShutdownDelegate {} 647 648enum App { 649 Initial(Waker), 650 Running(Rc<AppState>), 651} 652 653impl App { 654 fn new(event_loop: &EventLoop<WakerEvent>) -> Self { 655 Self::Initial(Waker::new(event_loop)) 656 } 657} 658 659impl ApplicationHandler<WakerEvent> for App { 660 fn resumed(&mut self, event_loop: &ActiveEventLoop) { 661 if let Self::Initial(waker) = self { 662 let mut protocol_registry = ProtocolRegistry::default(); 663 let _ = protocol_registry.register("resource", ResourceProtocolHandler::default()); 664 665 let opts = Opts { 666 multiprocess: true, 667 config_dir: config_dir(), 668 ..Default::default() 669 }; 670 671 let preferences = Preferences { 672 viewport_meta_enabled: true, 673 devtools_server_enabled: true, 674 devtools_server_port: 6222, 675 shell_background_color_rgba: [0.0, 0.0, 0.0, 0.0], 676 ..Default::default() 677 }; 678 679 let servo = ServoBuilder::default() 680 .opts(opts) 681 .preferences(preferences) 682 .protocol_registry(protocol_registry) 683 .event_loop_waker(Box::new(waker.clone())) 684 .build(); 685 686 vhost::start_vhost(8888); 687 688 // Create the tray icon before creating the app state. 689 // This may return None on systems that don't support tray icons 690 // (e.g., GNOME without AppIndicator extension). 691 #[cfg(feature = "status-tray")] 692 let tray = Tray::new(waker.proxy()); 693 694 // Create the global hotkey manager. 695 // This may return None if hotkeys cannot be registered 696 // (e.g., permission denied on macOS, or hotkey already in use). 697 #[cfg(feature = "global-hotkeys")] 698 let hotkey_manager = HotkeyManager::new(waker.proxy()); 699 700 let app_state = Rc::new(AppState { 701 servo: servo.clone(), 702 windows: Default::default(), 703 focused_window: Default::default(), 704 #[cfg(feature = "status-tray")] 705 tray, 706 exit_requested: Cell::new(false), 707 #[cfg(feature = "global-hotkeys")] 708 hotkey_manager, 709 }); 710 711 let state2 = Rc::clone(&app_state); 712 servo.set_delegate(state2); 713 servo.setup_logging(); 714 enable_experimental_prefs(&servo); 715 716 let url = Url::parse( 717 &std::env::args() 718 .nth(1) 719 .unwrap_or("http://system.localhost:8888/index.html".to_owned()), 720 ) 721 .expect("Invalid url"); 722 723 // Create the initial window 724 app_state.open_window(event_loop, url, None); 725 726 *self = Self::Running(app_state); 727 } 728 } 729 730 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: WakerEvent) { 731 if let Self::Running(app_state) = self { 732 match event { 733 WakerEvent::Wake => { 734 app_state.servo.spin_event_loop(); 735 }, 736 #[cfg(feature = "status-tray")] 737 WakerEvent::TrayMenu(menu_id) => { 738 app_state.handle_tray_menu_event(event_loop, &menu_id); 739 }, 740 #[cfg(feature = "global-hotkeys")] 741 WakerEvent::GlobalHotkey(id) => { 742 if let Some(ref hm) = app_state.hotkey_manager { 743 if hm.is_search_hotkey(id) { 744 app_state.open_search_window(event_loop); 745 } 746 } 747 }, 748 } 749 app_state.process_pending_commands(event_loop); 750 751 // Check if quit was requested 752 if app_state.exit_requested.get() { 753 app_state.shutdown(); 754 event_loop.exit(); 755 } 756 } 757 } 758 759 fn window_event( 760 &mut self, 761 event_loop: &ActiveEventLoop, 762 window_id: winit::window::WindowId, 763 event: WindowEvent, 764 ) { 765 let Self::Running(app_state) = self else { 766 return; 767 }; 768 769 app_state.servo.spin_event_loop(); 770 771 let Some(browser_window) = app_state.window(window_id.into()) else { 772 return; 773 }; 774 775 match event { 776 WindowEvent::CloseRequested => { 777 app_state.close_window(browser_window.id()); 778 // Exit if there are no more windows and no tray icon keeping the app alive 779 #[cfg(feature = "status-tray")] 780 let should_exit = app_state.windows.borrow().is_empty() && app_state.tray.is_none(); 781 #[cfg(not(feature = "status-tray"))] 782 let should_exit = app_state.windows.borrow().is_empty(); 783 if should_exit { 784 app_state.shutdown(); 785 event_loop.exit(); 786 } 787 }, 788 WindowEvent::Focused(focused) => { 789 if focused { 790 *app_state.focused_window.borrow_mut() = Some(browser_window.clone()); 791 } 792 }, 793 _ => browser_window.window_event(event), 794 } 795 796 app_state.process_pending_commands(event_loop); 797 } 798 799 fn exiting(&mut self, _event_loop: &ActiveEventLoop) { 800 if let Self::Running(app_state) = self { 801 app_state.shutdown(); 802 } 803 } 804} 805 806#[derive(Clone)] 807struct Waker(EventLoopProxy<WakerEvent>); 808 809/// Events that can wake the event loop. 810#[derive(Debug)] 811pub enum WakerEvent { 812 /// Servo has work to do. 813 Wake, 814 /// A tray menu item was clicked. 815 #[cfg(feature = "status-tray")] 816 TrayMenu(MenuId), 817 /// A global hotkey was pressed. 818 #[cfg(feature = "global-hotkeys")] 819 GlobalHotkey(u32), 820} 821 822impl Waker { 823 fn new(event_loop: &EventLoop<WakerEvent>) -> Self { 824 Self(event_loop.create_proxy()) 825 } 826 827 #[cfg(any(feature = "status-tray", feature = "global-hotkeys"))] 828 fn proxy(&self) -> EventLoopProxy<WakerEvent> { 829 self.0.clone() 830 } 831} 832 833impl embedder_traits::EventLoopWaker for Waker { 834 fn clone_box(&self) -> Box<dyn embedder_traits::EventLoopWaker> { 835 Box::new(Self(self.0.clone())) 836 } 837 838 fn wake(&self) { 839 if let Err(error) = self.0.send_event(WakerEvent::Wake) { 840 warn!("Failed to wake event loop: {error}"); 841 } 842 } 843}