servo/ports/servoshell/desktop/app.rs
Martin Robinson 5466c27f6f
Finish the integration of webxr into the Cargo workspace (#35229)
- Run `cargo fmt` on `webxr` and `webxr-api`
- Fix clippy warnings in the existing `webxr` code
- Integrate the new crates into the workspace
- Expose `webxr` via the libservo API rather than requiring embedders to
  depend on it explicitly.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
2025-01-31 16:41:57 +00:00

624 lines
23 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Application entry point, runs the event loop.
use std::cell::Cell;
use std::collections::HashMap;
use std::rc::Rc;
use std::time::Instant;
use std::{env, fs};
use arboard::Clipboard;
use log::{info, trace, warn};
use raw_window_handle::HasDisplayHandle;
use servo::compositing::windowing::{AnimationState, WindowMethods};
use servo::compositing::CompositeTarget;
use servo::config::opts::Opts;
use servo::config::prefs::Preferences;
use servo::servo_config::pref;
use servo::servo_url::ServoUrl;
use servo::webrender_traits::SurfmanRenderingContext;
use servo::webxr::glwindow::GlWindowDiscovery;
#[cfg(target_os = "windows")]
use servo::webxr::openxr::{AppInfo, OpenXrDiscovery};
use servo::{EventLoopWaker, Servo};
use surfman::Connection;
use url::Url;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::window::WindowId;
use super::events_loop::{EventsLoop, WakerEvent};
use super::minibrowser::{Minibrowser, MinibrowserEvent};
use super::webview::WebViewManager;
use super::{headed_window, headless_window};
use crate::desktop::embedder::{EmbedderCallbacks, XrDiscovery};
use crate::desktop::tracing::trace_winit_event;
use crate::desktop::window_trait::WindowPortsMethods;
use crate::parser::{get_default_url, location_bar_input_to_url};
use crate::prefs::ServoShellPreferences;
pub struct App {
opts: Opts,
preferences: Preferences,
servo_shell_preferences: ServoShellPreferences,
clipboard: Option<Clipboard>,
servo: Option<Servo>,
webviews: Option<WebViewManager>,
suspended: Cell<bool>,
windows: HashMap<WindowId, Rc<dyn WindowPortsMethods>>,
minibrowser: Option<Minibrowser>,
waker: Box<dyn EventLoopWaker>,
initial_url: ServoUrl,
t_start: Instant,
t: Instant,
}
enum Present {
Immediate,
Deferred,
None,
}
/// Action to be taken by the caller of [`App::handle_events`].
enum PumpResult {
/// The caller should shut down Servo and its related context.
Shutdown,
Continue {
update: bool,
present: Present,
},
}
impl App {
pub fn new(
opts: Opts,
preferences: Preferences,
servo_shell_preferences: ServoShellPreferences,
events_loop: &EventsLoop,
) -> Self {
let initial_url = get_default_url(
servo_shell_preferences.url.as_deref(),
env::current_dir().unwrap(),
|path| fs::metadata(path).is_ok(),
&servo_shell_preferences,
);
let t = Instant::now();
App {
opts,
preferences,
servo_shell_preferences,
clipboard: Clipboard::new().ok(),
webviews: None,
servo: None,
suspended: Cell::new(false),
windows: HashMap::new(),
minibrowser: None,
waker: events_loop.create_event_loop_waker(),
initial_url: initial_url.clone(),
t_start: t,
t,
}
}
/// Initialize Application once event loop start running.
pub fn init(&mut self, event_loop: Option<&ActiveEventLoop>) {
// Create rendering context
let rendering_context = if self.opts.headless {
let connection = Connection::new().expect("Failed to create connection");
let adapter = connection
.create_software_adapter()
.expect("Failed to create adapter");
SurfmanRenderingContext::create(
&connection,
&adapter,
Some(self.opts.initial_window_size.to_untyped().to_i32()),
)
.expect("Failed to create WR surfman")
} else {
let display_handle = event_loop
.unwrap()
.display_handle()
.expect("could not get display handle from window");
let connection = Connection::from_display_handle(display_handle)
.expect("Failed to create connection");
let adapter = connection
.create_adapter()
.expect("Failed to create adapter");
SurfmanRenderingContext::create(&connection, &adapter, None)
.expect("Failed to create WR surfman")
};
let window = if self.opts.headless {
headless_window::Window::new(
self.opts.initial_window_size,
self.servo_shell_preferences.device_pixel_ratio_override,
self.opts.screen_size_override,
)
} else {
Rc::new(headed_window::Window::new(
&self.opts,
&rendering_context,
self.opts.initial_window_size,
event_loop.unwrap(),
self.servo_shell_preferences.no_native_titlebar,
self.servo_shell_preferences.device_pixel_ratio_override,
))
};
// Create window's context
self.webviews = Some(WebViewManager::new(window.clone()));
if window.winit_window().is_some() {
self.minibrowser = Some(Minibrowser::new(
&rendering_context,
event_loop.unwrap(),
self.initial_url.clone(),
));
}
if let Some(ref mut minibrowser) = self.minibrowser {
// Servo is not yet initialised, so there is no `servo_framebuffer_id`.
minibrowser.update(
window.winit_window().unwrap(),
self.webviews.as_mut().unwrap(),
self.servo.as_mut(),
"init",
);
window.set_toolbar_height(minibrowser.toolbar_height);
}
self.windows.insert(window.id(), window);
self.suspended.set(false);
let (_, window) = self.windows.iter().next().unwrap();
let xr_discovery = if pref!(dom_webxr_openxr_enabled) && !self.opts.headless {
#[cfg(target_os = "windows")]
let openxr = {
let app_info = AppInfo::new("Servoshell", 0, "Servo", 0);
Some(XrDiscovery::OpenXr(OpenXrDiscovery::new(None, app_info)))
};
#[cfg(not(target_os = "windows"))]
let openxr = None;
openxr
} else if pref!(dom_webxr_glwindow_enabled) && !self.opts.headless {
let window = window.new_glwindow(event_loop.unwrap());
Some(XrDiscovery::GlWindow(GlWindowDiscovery::new(window)))
} else {
None
};
// TODO: Remove this once dyn upcasting coercion stabilises
// <https://github.com/rust-lang/rust/issues/65991>
struct UpcastedWindow(Rc<dyn WindowPortsMethods>);
impl WindowMethods for UpcastedWindow {
fn get_coordinates(&self) -> servo::compositing::windowing::EmbedderCoordinates {
self.0.get_coordinates()
}
fn set_animation_state(&self, state: AnimationState) {
self.0.set_animation_state(state);
}
}
let window = UpcastedWindow(window.clone());
// Implements embedder methods, used by libservo and constellation.
let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery));
let composite_target = if self.minibrowser.is_some() {
CompositeTarget::Fbo
} else {
CompositeTarget::Window
};
let servo = Servo::new(
self.opts.clone(),
self.preferences.clone(),
Rc::new(rendering_context),
embedder,
Rc::new(window),
self.servo_shell_preferences.user_agent.clone(),
composite_target,
);
servo.setup_logging();
let webview = servo.new_webview(self.initial_url.clone().into_url());
self.webviews.as_mut().unwrap().add(webview);
self.servo = Some(servo);
}
pub fn is_animating(&self) -> bool {
self.windows.iter().any(|(_, window)| window.is_animating())
}
/// Spins the Servo event loop, and (for now) handles a few other tasks:
/// - Notifying Servo about incoming gamepad events
/// - Receiving updates from Servo
/// - Performing updates in the compositor, such as queued pinch zoom events
///
/// In the future, these tasks may be decoupled.
fn handle_events(&mut self) -> PumpResult {
let webviews = self.webviews.as_mut().unwrap();
// If the Gamepad API is enabled, handle gamepad events from GilRs.
// Checking for focused_webview_id should ensure we'll have a valid browsing context.
if pref!(dom_gamepad_enabled) && webviews.focused_webview_id().is_some() {
webviews.handle_gamepad_events();
}
// Take any new embedder messages from Servo.
let servo = self.servo.as_mut().expect("Servo should be running.");
let mut embedder_messages: Vec<_> = servo.get_events().collect();
let mut need_resize = false;
let mut need_present = false;
let mut need_update = false;
loop {
// Consume and handle those embedder messages.
let servo_event_response = webviews.handle_servo_events(
servo,
&mut self.clipboard,
&self.opts,
embedder_messages,
);
need_present |= servo_event_response.need_present;
need_update |= servo_event_response.need_update;
// Runs the compositor, and receives and collects embedder messages from various Servo components.
need_resize |= servo.handle_events(vec![]);
if webviews.shutdown_requested() {
return PumpResult::Shutdown;
}
// Take any new embedder messages from Servo itself.
embedder_messages = servo.get_events().collect();
if embedder_messages.is_empty() {
break;
}
}
let present = if need_resize {
Present::Immediate
} else if need_present {
Present::Deferred
} else {
Present::None
};
PumpResult::Continue {
update: need_update,
present,
}
}
/// Handle events with winit contexts
pub fn handle_events_with_winit(
&mut self,
event_loop: &ActiveEventLoop,
window: Rc<dyn WindowPortsMethods>,
) {
match self.handle_events() {
PumpResult::Shutdown => {
event_loop.exit();
self.servo.take().unwrap().deinit();
if let Some(ref mut minibrowser) = self.minibrowser {
minibrowser.context.destroy();
}
},
PumpResult::Continue { update, present } => {
if update {
if let Some(ref mut minibrowser) = self.minibrowser {
let webviews = self.webviews.as_mut().unwrap();
if minibrowser.update_webview_data(webviews) {
// Update the minibrowser immediately. While we could update by requesting a
// redraw, doing so would delay the location update by two frames.
minibrowser.update(
window.winit_window().unwrap(),
webviews,
self.servo.as_mut(),
"update_location_in_toolbar",
);
}
}
}
match present {
Present::Immediate => {
// The window was resized.
trace!("PumpResult::Present::Immediate");
// If we had resized any of the viewports in response to this, we would need to
// call Servo::repaint_synchronously. At the moment we dont, so there wont be
// any paint scheduled, and calling it would hang the compositor forever.
if let Some(ref mut minibrowser) = self.minibrowser {
minibrowser.update(
window.winit_window().unwrap(),
self.webviews.as_mut().unwrap(),
self.servo.as_mut(),
"PumpResult::Present::Immediate",
);
minibrowser.paint(window.winit_window().unwrap());
}
self.servo.as_mut().unwrap().present();
},
Present::Deferred => {
// The compositor has painted to this frame.
trace!("PumpResult::Present::Deferred");
// Request a winit redraw event, so we can paint the minibrowser and present.
// Otherwise, it's in headless mode and we present directly.
if let Some(window) = window.winit_window() {
window.request_redraw();
} else {
self.servo.as_mut().unwrap().present();
}
},
Present::None => {},
}
},
}
}
/// Handle all servo events with headless mode. Return true if servo request to shutdown.
pub fn handle_events_with_headless(&mut self) -> bool {
let now = Instant::now();
let event = winit::event::Event::UserEvent(WakerEvent);
trace_winit_event!(
event,
"@{:?} (+{:?}) {event:?}",
now - self.t_start,
now - self.t
);
self.t = now;
// If self.servo is None here, it means that we're in the process of shutting down,
// let's ignore events.
if self.servo.is_none() {
return false;
}
let mut exit = false;
match self.handle_events() {
PumpResult::Shutdown => {
exit = true;
self.servo.take().unwrap().deinit();
if let Some(ref mut minibrowser) = self.minibrowser {
minibrowser.context.destroy();
}
},
PumpResult::Continue { present, .. } => {
match present {
Present::Immediate => {
// The window was resized.
trace!("PumpResult::Present::Immediate");
self.servo.as_mut().unwrap().present();
},
Present::Deferred => {
// The compositor has painted to this frame.
trace!("PumpResult::Present::Deferred");
// In headless mode, we present directly.
self.servo.as_mut().unwrap().present();
},
Present::None => {},
}
},
}
exit
}
/// Takes any events generated during `egui` updates and performs their actions.
fn handle_servoshell_ui_events(&mut self) {
let Some(webviews) = self.webviews.as_mut() else {
return;
};
let Some(minibrowser) = self.minibrowser.as_ref() else {
return;
};
let Some(servo) = self.servo.as_ref() else {
return;
};
for event in minibrowser.take_events() {
match event {
MinibrowserEvent::Go(location) => {
minibrowser.update_location_dirty(false);
let Some(url) = location_bar_input_to_url(
&location.clone(),
&self.servo_shell_preferences.searchpage,
) else {
warn!("failed to parse location");
break;
};
if let Some(focused_webview) = webviews.focused_webview() {
focused_webview.servo_webview.load(url.into_url());
}
},
MinibrowserEvent::Back => {
if let Some(focused_webview) = webviews.focused_webview() {
focused_webview.servo_webview.go_back(1);
}
},
MinibrowserEvent::Forward => {
if let Some(focused_webview) = webviews.focused_webview() {
focused_webview.servo_webview.go_forward(1);
}
},
MinibrowserEvent::Reload => {
minibrowser.update_location_dirty(false);
if let Some(focused_webview) = webviews.focused_webview() {
focused_webview.servo_webview.reload();
}
},
MinibrowserEvent::NewWebView => {
minibrowser.update_location_dirty(false);
let webview = servo.new_webview(Url::parse("servo:newtab").unwrap());
webviews.add(webview);
},
MinibrowserEvent::CloseWebView(id) => {
minibrowser.update_location_dirty(false);
webviews.close_webview(servo, id);
},
}
}
}
}
impl ApplicationHandler<WakerEvent> for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.init(Some(event_loop));
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
let now = Instant::now();
trace_winit_event!(
event,
"@{:?} (+{:?}) {event:?}",
now - self.t_start,
now - self.t
);
self.t = now;
// If self.servo is None here, it means that we're in the process of shutting down,
// let's ignore events.
if self.servo.is_none() {
return;
}
let Some(window) = self.windows.get(&window_id) else {
return;
};
let window = window.clone();
if event == winit::event::WindowEvent::RedrawRequested {
// We need to redraw the window for some reason.
trace!("RedrawRequested");
// WARNING: do not defer painting or presenting to some later tick of the event
// loop or servoshell may become unresponsive! (servo#30312)
if let Some(ref mut minibrowser) = self.minibrowser {
minibrowser.update(
window.winit_window().unwrap(),
self.webviews.as_mut().unwrap(),
self.servo.as_mut(),
"RedrawRequested",
);
minibrowser.paint(window.winit_window().unwrap());
}
self.servo.as_mut().unwrap().present();
}
// Handle the event
let mut consumed = false;
if let Some(ref mut minibrowser) = self.minibrowser {
match event {
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
// Intercept any ScaleFactorChanged events away from EguiGlow::on_window_event, so
// we can use our own logic for calculating the scale factor and set eguis
// scale factor to that value manually.
let desired_scale_factor = window.hidpi_factor().get();
let effective_egui_zoom_factor = desired_scale_factor / scale_factor as f32;
info!(
"window scale factor changed to {}, setting egui zoom factor to {}",
scale_factor, effective_egui_zoom_factor
);
minibrowser
.context
.egui_ctx
.set_zoom_factor(effective_egui_zoom_factor);
// Request a winit redraw event, so we can recomposite, update and paint
// the minibrowser, and present the new frame.
window.winit_window().unwrap().request_redraw();
},
ref event => {
let response =
minibrowser.on_window_event(window.winit_window().unwrap(), event);
// Update minibrowser if there's resize event to sync up with window.
if let WindowEvent::Resized(_) = event {
minibrowser.update(
window.winit_window().unwrap(),
self.webviews.as_mut().unwrap(),
self.servo.as_mut(),
"Sync WebView size with Window Resize event",
);
}
if response.repaint && *event != winit::event::WindowEvent::RedrawRequested {
// Request a winit redraw event, so we can recomposite, update and paint
// the minibrowser, and present the new frame.
window.winit_window().unwrap().request_redraw();
}
// TODO how do we handle the tab key? (see doc for consumed)
// Note that servo doesnt yet support tabbing through links and inputs
consumed = response.consumed;
},
}
}
if !consumed {
if let (Some(servo), Some(webviews)) = (self.servo.as_ref(), self.webviews.as_mut()) {
window.handle_winit_event(servo, &mut self.clipboard, webviews, event);
}
}
let animating = self.is_animating();
// Block until the window gets an event
if !animating || self.suspended.get() {
event_loop.set_control_flow(ControlFlow::Wait);
} else {
event_loop.set_control_flow(ControlFlow::Poll);
}
// Consume and handle any events from the servoshell UI.
self.handle_servoshell_ui_events();
self.handle_events_with_winit(event_loop, window);
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: WakerEvent) {
let now = Instant::now();
let event = winit::event::Event::UserEvent(event);
trace_winit_event!(
event,
"@{:?} (+{:?}) {event:?}",
now - self.t_start,
now - self.t
);
self.t = now;
// If self.servo is None here, it means that we're in the process of shutting down,
// let's ignore events.
if self.servo.is_none() {
return;
}
let Some(window) = self.windows.values().next() else {
return;
};
let window = window.clone();
let animating = self.is_animating();
// Block until the window gets an event
if !animating || self.suspended.get() {
event_loop.set_control_flow(ControlFlow::Wait);
} else {
event_loop.set_control_flow(ControlFlow::Poll);
}
// Consume and handle any events from the Minibrowser.
self.handle_servoshell_ui_events();
self.handle_events_with_winit(event_loop, window);
}
fn suspended(&mut self, _: &ActiveEventLoop) {
self.suspended.set(true);
}
}