servoshell: Port desktop servoshell to use delegate API (#35284)

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Delan Azabani <dazabani@igalia.com>
Co-authored-by: Mukilan Thiyagarajan <mukilan@igalia.com>
This commit is contained in:
Martin Robinson 2025-02-06 08:33:31 +01:00 committed by GitHub
parent 6b12499077
commit 5f08e4fa76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1109 additions and 1258 deletions

View file

@ -10,7 +10,6 @@ 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};
@ -31,10 +30,11 @@ use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::window::WindowId;
use super::app_state::AppState;
use super::events_loop::{EventsLoop, WakerEvent};
use super::minibrowser::{Minibrowser, MinibrowserEvent};
use super::webview::WebViewManager;
use super::{headed_window, headless_window};
use crate::desktop::app_state::RunningAppState;
use crate::desktop::embedder::{EmbedderCallbacks, XrDiscovery};
use crate::desktop::tracing::trace_winit_event;
use crate::desktop::window_trait::WindowPortsMethods;
@ -45,9 +45,6 @@ pub struct App {
opts: Opts,
preferences: Preferences,
servo_shell_preferences: ServoShellPreferences,
clipboard: Option<Clipboard>,
servo: Option<Servo>,
webviews: WebViewManager,
suspended: Cell<bool>,
windows: HashMap<WindowId, Rc<dyn WindowPortsMethods>>,
minibrowser: Option<Minibrowser>,
@ -55,15 +52,16 @@ pub struct App {
initial_url: ServoUrl,
t_start: Instant,
t: Instant,
state: AppState,
}
enum Present {
pub(crate) enum Present {
Deferred,
None,
}
/// Action to be taken by the caller of [`App::handle_events`].
enum PumpResult {
pub(crate) enum PumpResult {
/// The caller should shut down Servo and its related context.
Shutdown,
Continue {
@ -91,9 +89,6 @@ impl App {
opts,
preferences,
servo_shell_preferences,
clipboard: Clipboard::new().ok(),
webviews: WebViewManager::new(),
servo: None,
suspended: Cell::new(false),
windows: HashMap::new(),
minibrowser: None,
@ -101,6 +96,7 @@ impl App {
initial_url: initial_url.clone(),
t_start: t,
t,
state: AppState::Initializing,
}
}
@ -149,8 +145,6 @@ impl App {
))
};
// Create window's context
self.webviews.set_window(window.clone());
if window.winit_window().is_some() {
self.minibrowser = Some(Minibrowser::new(
&rendering_context,
@ -159,17 +153,6 @@ impl App {
));
}
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(),
&mut self.webviews,
self.servo.as_ref(),
"init",
);
window.set_toolbar_height(minibrowser.toolbar_height);
}
self.windows.insert(window.id(), window);
self.suspended.set(false);
@ -192,6 +175,15 @@ impl App {
None
};
// 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::OffscreenFbo
} else {
CompositeTarget::ContextFbo
};
// TODO: Remove this once dyn upcasting coercion stabilises
// <https://github.com/rust-lang/rust/issues/65991>
struct UpcastedWindow(Rc<dyn WindowPortsMethods>);
@ -203,116 +195,61 @@ impl App {
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::OffscreenFbo
} else {
CompositeTarget::ContextFbo
};
let servo = Servo::new(
self.opts.clone(),
self.preferences.clone(),
Rc::new(rendering_context),
embedder,
Rc::new(window),
Rc::new(UpcastedWindow(window.clone())),
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.add(webview);
let running_state = Rc::new(RunningAppState::new(
servo,
window.clone(),
self.opts.headless,
));
running_state.new_toplevel_webview(self.initial_url.clone().into_url());
self.servo = Some(servo);
if let Some(ref mut minibrowser) = self.minibrowser {
minibrowser.update(window.winit_window().unwrap(), &running_state, "init");
window.set_toolbar_height(minibrowser.toolbar_height);
}
self.state = AppState::Running(running_state);
}
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 {
// 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) && self.webviews.focused_webview_id().is_some() {
self.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 = servo.get_events();
let mut need_present = false;
let mut need_update = false;
loop {
// Consume and handle those embedder messages.
let servo_event_response = self.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.
servo.handle_events(vec![]);
if self.webviews.shutdown_requested() {
return PumpResult::Shutdown;
}
// Take any new embedder messages from Servo itself.
embedder_messages = servo.get_events();
if embedder_messages.is_empty() {
break;
}
}
let present = 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() {
let AppState::Running(state) = &self.state else {
return;
};
match state.pump_event_loop() {
PumpResult::Shutdown => {
event_loop.exit();
self.servo.take().unwrap().deinit();
if let Some(ref mut minibrowser) = self.minibrowser {
minibrowser.context.destroy();
}
state.shutdown();
self.state = AppState::ShuttingDown;
},
PumpResult::Continue { update, present } => {
if update {
if let Some(ref mut minibrowser) = self.minibrowser {
if minibrowser.update_webview_data(&mut self.webviews) {
if minibrowser.update_webview_data(state) {
// 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(),
&mut self.webviews,
self.servo.as_ref(),
state,
"update_location_in_toolbar",
);
}
@ -327,16 +264,21 @@ impl App {
if let Some(window) = window.winit_window() {
window.request_redraw();
} else {
self.servo.as_mut().unwrap().present();
state.servo().present();
}
},
Present::None => {},
}
},
}
if matches!(self.state, AppState::ShuttingDown) {
event_loop.exit();
}
}
/// Handle all servo events with headless mode. Return true if servo request to shutdown.
/// Handle all servo events with headless mode. Return true if the application should
/// continue.
pub fn handle_events_with_headless(&mut self) -> bool {
let now = Instant::now();
let event = winit::event::Event::UserEvent(WakerEvent);
@ -347,20 +289,16 @@ impl App {
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() {
// We should always be in the running state.
let AppState::Running(state) = &self.state else {
return false;
};
match state.pump_event_loop() {
PumpResult::Shutdown => {
exit = true;
self.servo.take().unwrap().deinit();
if let Some(ref mut minibrowser) = self.minibrowser {
minibrowser.context.destroy();
}
state.shutdown();
self.state = AppState::ShuttingDown;
},
PumpResult::Continue { present, .. } => {
match present {
@ -368,13 +306,14 @@ impl App {
// The compositor has painted to this frame.
trace!("PumpResult::Present::Deferred");
// In headless mode, we present directly.
self.servo.as_mut().unwrap().present();
state.servo().present();
},
Present::None => {},
}
},
}
exit
!matches!(self.state, AppState::ShuttingDown)
}
/// Takes any events generated during `egui` updates and performs their actions.
@ -382,7 +321,8 @@ impl App {
let Some(minibrowser) = self.minibrowser.as_ref() else {
return;
};
let Some(servo) = self.servo.as_ref() else {
// We should always be in the running state.
let AppState::Running(state) = &self.state else {
return;
};
@ -397,34 +337,33 @@ impl App {
warn!("failed to parse location");
break;
};
if let Some(focused_webview) = self.webviews.focused_webview() {
focused_webview.servo_webview.load(url.into_url());
if let Some(focused_webview) = state.focused_webview() {
focused_webview.load(url.into_url());
}
},
MinibrowserEvent::Back => {
if let Some(focused_webview) = self.webviews.focused_webview() {
focused_webview.servo_webview.go_back(1);
if let Some(focused_webview) = state.focused_webview() {
focused_webview.go_back(1);
}
},
MinibrowserEvent::Forward => {
if let Some(focused_webview) = self.webviews.focused_webview() {
focused_webview.servo_webview.go_forward(1);
if let Some(focused_webview) = state.focused_webview() {
focused_webview.go_forward(1);
}
},
MinibrowserEvent::Reload => {
minibrowser.update_location_dirty(false);
if let Some(focused_webview) = self.webviews.focused_webview() {
focused_webview.servo_webview.reload();
if let Some(focused_webview) = state.focused_webview() {
focused_webview.reload();
}
},
MinibrowserEvent::NewWebView => {
minibrowser.update_location_dirty(false);
let webview = servo.new_webview(Url::parse("servo:newtab").unwrap());
self.webviews.add(webview);
state.new_toplevel_webview(Url::parse("servo:newtab").unwrap());
},
MinibrowserEvent::CloseWebView(id) => {
minibrowser.update_location_dirty(false);
self.webviews.close_webview(servo, id);
state.close_webview(id);
},
}
}
@ -450,17 +389,16 @@ impl ApplicationHandler<WakerEvent> for App {
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.
let Some(ref mut servo) = self.servo else {
let AppState::Running(state) = &self.state else {
return;
};
let Some(window) = self.windows.get(&window_id) else {
return;
};
let window = window.clone();
let window = window.clone();
if event == winit::event::WindowEvent::RedrawRequested {
// We need to redraw the window for some reason.
trace!("RedrawRequested");
@ -468,16 +406,11 @@ impl ApplicationHandler<WakerEvent> for App {
// 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(),
&mut self.webviews,
Some(servo),
"RedrawRequested",
);
minibrowser.update(window.winit_window().unwrap(), state, "RedrawRequested");
minibrowser.paint(window.winit_window().unwrap());
}
servo.present();
state.servo().present();
}
// Handle the event
@ -512,8 +445,7 @@ impl ApplicationHandler<WakerEvent> for App {
if let WindowEvent::Resized(_) = event {
minibrowser.update(
window.winit_window().unwrap(),
&mut self.webviews,
Some(servo),
state,
"Sync WebView size with Window Resize event",
);
}
@ -530,7 +462,7 @@ impl ApplicationHandler<WakerEvent> for App {
}
}
if !consumed {
window.handle_winit_event(servo, &mut self.clipboard, &mut self.webviews, event);
window.handle_winit_event(state.clone(), event);
}
let animating = self.is_animating();
@ -558,12 +490,10 @@ impl ApplicationHandler<WakerEvent> for App {
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;
}
if !matches!(self.state, AppState::Running(_)) {
return;
};
let Some(window) = self.windows.values().next() else {
return;
};