Rewild Your Web
web
browser
dweb
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}