libservo: Make zooming and HiDPI scaling work per-WebView (#36419)

libservo: Make zooming and HiDPI scaling work per-`WebView`

This change moves all zooming and HiDPI scaling to work per-`WebView` in
both libservo and Compositor. This means that you can pinch zoom one
`WebView` and it should now work independently of other `WebView`s.
This is accomplished by making each `WebView` in the WebRender scene
have its own scaling reference frame.

All WebViews are now expected to manage their HiDPI scaling factor and
this can be set independently of other WebViews. Perhaps in the future
this will become a Servo-wide setting.

This allows full removal of the `WindowMethods` trait from Servo.

Testing: There are not yet any tests for the WebView API, but I hope
to add those soon.

Co-authored-by: Shubham Gupta <shubham13297@gmail.com>
Signed-off-by: Martin Robinson <mrobinson@igalia.com>

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Shubham Gupta <shubham13297@gmail.com>
This commit is contained in:
Martin Robinson 2025-04-14 14:01:49 +02:00 committed by GitHub
parent f1417c4e75
commit c6dc7c83a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 415 additions and 385 deletions

View file

@ -11,16 +11,12 @@ use std::rc::Rc;
use std::time::Instant;
use std::{env, fs};
use euclid::Scale;
use log::{info, trace, warn};
use servo::compositing::windowing::WindowMethods;
use servo::config::opts::Opts;
use servo::config::prefs::Preferences;
use servo::servo_config::pref;
use servo::servo_geometry::DeviceIndependentPixel;
use servo::servo_url::ServoUrl;
use servo::user_content_manager::{UserContentManager, UserScript};
use servo::webrender_api::units::DevicePixel;
use servo::webxr::glwindow::GlWindowDiscovery;
#[cfg(target_os = "windows")]
use servo::webxr::openxr::{AppInfo, OpenXrDiscovery};
@ -139,15 +135,6 @@ impl App {
// Implements embedder methods, used by libservo and constellation.
let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery));
// 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 hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
self.0.hidpi_factor()
}
}
let mut user_content_manager = UserContentManager::new();
for script in load_userscripts(self.servoshell_preferences.userscripts_directory.as_deref())
.expect("Loading userscripts failed")
@ -160,7 +147,6 @@ impl App {
self.preferences.clone(),
window.rendering_context(),
embedder,
Rc::new(UpcastedWindow(window.clone())),
user_content_manager,
);
servo.setup_logging();
@ -358,7 +344,7 @@ impl ApplicationHandler<WakerEvent> for App {
// 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 desired_scale_factor = window.hidpi_scale_factor().get();
let effective_egui_zoom_factor = desired_scale_factor / scale_factor as f32;
info!(
@ -371,6 +357,8 @@ impl ApplicationHandler<WakerEvent> for App {
.egui_ctx
.set_zoom_factor(effective_egui_zoom_factor);
state.hidpi_scale_factor_changed();
// 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();

View file

@ -109,6 +109,7 @@ impl RunningAppState {
pub(crate) fn new_toplevel_webview(self: &Rc<Self>, url: Url) {
let webview = WebViewBuilder::new(self.servo())
.url(url)
.hidpi_scale_factor(self.inner().window.hidpi_scale_factor())
.delegate(self.clone())
.build();
@ -130,6 +131,14 @@ impl RunningAppState {
&self.servo
}
pub(crate) fn hidpi_scale_factor_changed(&self) {
let inner = self.inner();
let new_scale_factor = inner.window.hidpi_scale_factor();
for webview in inner.webviews.values() {
webview.set_hidpi_scale_factor(new_scale_factor);
}
}
pub(crate) fn save_output_image_if_necessary(&self) {
let Some(output_path) = self.servoshell_preferences.output_image_path.as_ref() else {
return;
@ -462,6 +471,7 @@ impl WebViewDelegate for RunningAppState {
parent_webview: servo::WebView,
) -> Option<servo::WebView> {
let webview = WebViewBuilder::new_auxiliary(&self.servo)
.hidpi_scale_factor(self.inner().window.hidpi_scale_factor())
.delegate(parent_webview.delegate())
.build();

View file

@ -14,7 +14,7 @@ use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vec
use keyboard_types::{Modifiers, ShortcutMatcher};
use log::{debug, info};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle};
use servo::compositing::windowing::{WebRenderDebugOption, WindowMethods};
use servo::compositing::windowing::WebRenderDebugOption;
use servo::servo_config::pref;
use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::ScrollLocation;
@ -252,7 +252,7 @@ impl Window {
/// Helper function to handle a click
fn handle_mouse(&self, webview: &WebView, button: MouseButton, action: ElementState) {
let max_pixel_dist = 10.0 * self.hidpi_factor().get();
let max_pixel_dist = 10.0 * self.hidpi_scale_factor().get();
let mouse_button = match &button {
MouseButton::Left => ServoMouseButton::Left,
MouseButton::Right => ServoMouseButton::Right,
@ -443,8 +443,11 @@ impl Window {
impl WindowPortsMethods for Window {
fn screen_geometry(&self) -> ScreenGeometry {
let hidpi_factor = self.hidpi_factor();
let toolbar_size = Size2D::new(0.0, (self.toolbar_height.get() * self.hidpi_factor()).0);
let hidpi_factor = self.hidpi_scale_factor();
let toolbar_size = Size2D::new(
0.0,
(self.toolbar_height.get() * self.hidpi_scale_factor()).0,
);
let screen_size = self.screen_size.to_f32() * hidpi_factor;
let available_screen_size = screen_size - toolbar_size;
@ -462,18 +465,18 @@ impl WindowPortsMethods for Window {
}
}
fn device_hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
fn device_hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
Scale::new(self.winit_window.scale_factor() as f32)
}
fn device_pixel_ratio_override(
&self,
) -> Option<Scale<f32, DeviceIndependentPixel, DevicePixel>> {
self.device_pixel_ratio_override.map(Scale::new)
fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
self.device_pixel_ratio_override
.map(Scale::new)
.unwrap_or_else(|| self.device_hidpi_scale_factor())
}
fn page_height(&self) -> f32 {
let dpr = self.hidpi_factor();
let dpr = self.hidpi_scale_factor();
let size = self.winit_window.inner_size();
size.height as f32 * dpr.get()
}
@ -483,7 +486,7 @@ impl WindowPortsMethods for Window {
}
fn request_resize(&self, _: &WebView, size: DeviceIntSize) -> Option<DeviceIntSize> {
let toolbar_height = self.toolbar_height() * self.hidpi_factor();
let toolbar_height = self.toolbar_height() * self.hidpi_scale_factor();
let toolbar_height = toolbar_height.get().ceil() as i32;
let total_size = PhysicalSize::new(size.width, size.height + toolbar_height);
self.winit_window
@ -587,7 +590,7 @@ impl WindowPortsMethods for Window {
},
WindowEvent::CursorMoved { position, .. } => {
let mut point = winit_position_to_euclid_point(position).to_f32();
point.y -= (self.toolbar_height() * self.hidpi_factor()).0;
point.y -= (self.toolbar_height() * self.hidpi_scale_factor()).0;
self.webview_relative_mouse_point.set(point);
webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent { point }));
@ -598,7 +601,7 @@ impl WindowPortsMethods for Window {
(dx as f64, (dy * LINE_HEIGHT) as f64, WheelMode::DeltaLine)
},
MouseScrollDelta::PixelDelta(position) => {
let scale_factor = self.device_hidpi_factor().inverse().get() as f64;
let scale_factor = self.device_hidpi_scale_factor().inverse().get() as f64;
let position = position.to_logical(scale_factor);
(position.x, position.y, WheelMode::DeltaPixel)
},
@ -728,7 +731,7 @@ impl WindowPortsMethods for Window {
// this prevents a crash in the compositor due to invalid surface size
self.winit_window.set_min_inner_size(Some(PhysicalSize::new(
1.0,
1.0 + (self.toolbar_height() * self.hidpi_factor()).0,
1.0 + (self.toolbar_height() * self.hidpi_scale_factor()).0,
)));
}
@ -761,13 +764,6 @@ impl WindowPortsMethods for Window {
}
}
impl WindowMethods for Window {
fn hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
self.device_pixel_ratio_override()
.unwrap_or_else(|| self.device_hidpi_factor())
}
}
fn winit_phase_to_touch_event_type(phase: TouchPhase) -> TouchEventType {
match phase {
TouchPhase::Started => TouchEventType::Down,

View file

@ -9,7 +9,6 @@ use std::rc::Rc;
use euclid::num::Zero;
use euclid::{Length, Scale, Size2D};
use servo::compositing::windowing::WindowMethods;
use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntSize, DevicePixel};
use servo::{RenderingContext, ScreenGeometry, SoftwareRenderingContext};
@ -94,19 +93,18 @@ impl WindowPortsMethods for Window {
Some(new_size)
}
fn device_hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
fn device_hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
Scale::new(1.0)
}
fn device_pixel_ratio_override(
&self,
) -> Option<Scale<f32, DeviceIndependentPixel, DevicePixel>> {
fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
self.device_pixel_ratio_override
.unwrap_or_else(|| self.device_hidpi_scale_factor())
}
fn page_height(&self) -> f32 {
let height = self.inner_size.get().height;
let dpr = self.hidpi_factor();
let dpr = self.hidpi_scale_factor();
height as f32 * dpr.get()
}
@ -145,10 +143,3 @@ impl WindowPortsMethods for Window {
self.rendering_context.clone()
}
}
impl WindowMethods for Window {
fn hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
self.device_pixel_ratio_override()
.unwrap_or_else(|| self.device_hidpi_factor())
}
}

View file

@ -8,7 +8,6 @@
use std::rc::Rc;
use euclid::{Length, Scale};
use servo::compositing::windowing::WindowMethods;
use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel};
use servo::{Cursor, RenderingContext, ScreenGeometry, WebView};
@ -18,13 +17,11 @@ use super::app_state::RunningAppState;
// This should vary by zoom level and maybe actual text size (focused or under cursor)
pub const LINE_HEIGHT: f32 = 38.0;
pub trait WindowPortsMethods: WindowMethods {
pub trait WindowPortsMethods {
fn id(&self) -> winit::window::WindowId;
fn screen_geometry(&self) -> ScreenGeometry;
fn device_hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
fn device_pixel_ratio_override(
&self,
) -> Option<Scale<f32, DeviceIndependentPixel, DevicePixel>>;
fn device_hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
fn page_height(&self) -> f32;
fn get_fullscreen(&self) -> bool;
fn handle_winit_event(&self, state: Rc<RunningAppState>, event: winit::event::WindowEvent);

View file

@ -83,7 +83,6 @@ pub fn init(
let window_callbacks = Rc::new(ServoWindowCallbacks::new(
callbacks,
RefCell::new(init_opts.coordinates),
init_opts.density,
));
let embedder_callbacks = Box::new(ServoEmbedderCallbacks::new(
@ -97,13 +96,13 @@ pub fn init(
preferences,
rendering_context.clone(),
embedder_callbacks,
window_callbacks.clone(),
Default::default(),
);
APP.with(|app| {
let app_state = RunningAppState::new(
init_opts.url,
init_opts.density,
rendering_context,
servo,
window_callbacks,

View file

@ -11,7 +11,7 @@ use keyboard_types::{CompositionEvent, CompositionState};
use log::{debug, error, info, warn};
use raw_window_handle::{RawWindowHandle, WindowHandle};
use servo::base::id::WebViewId;
use servo::compositing::windowing::{EmbedderMethods, WindowMethods};
use servo::compositing::windowing::EmbedderMethods;
use servo::euclid::{Point2D, Rect, Scale, Size2D, Vector2D};
use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::ScrollLocation;
@ -45,19 +45,16 @@ impl Coordinates {
pub(super) struct ServoWindowCallbacks {
host_callbacks: Box<dyn HostTrait>,
coordinates: RefCell<Coordinates>,
hidpi_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
}
impl ServoWindowCallbacks {
pub(super) fn new(
host_callbacks: Box<dyn HostTrait>,
coordinates: RefCell<Coordinates>,
hidpi_factor: f32,
) -> Self {
Self {
host_callbacks,
coordinates,
hidpi_factor: Scale::new(hidpi_factor),
}
}
}
@ -90,6 +87,9 @@ struct RunningAppStateInner {
/// Whether or not the animation state has changed. This is used to trigger
/// host callbacks indicating that animation state has changed.
animating_state_changed: Rc<Cell<bool>>,
/// The HiDPI scaling factor to use for the display of [`WebView`]s.
hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
}
struct ServoShellServoDelegate {
@ -115,7 +115,7 @@ impl ServoDelegate for ServoShellServoDelegate {
}
impl WebViewDelegate for RunningAppState {
fn screen_geometry(&self, webview: WebView) -> Option<ScreenGeometry> {
fn screen_geometry(&self, _webview: WebView) -> Option<ScreenGeometry> {
let coord = self.callbacks.coordinates.borrow();
let screen_size = DeviceIntSize::new(coord.viewport.size.width, coord.viewport.size.height);
Some(ScreenGeometry {
@ -214,6 +214,7 @@ impl WebViewDelegate for RunningAppState {
fn request_open_auxiliary_webview(&self, parent_webview: WebView) -> Option<WebView> {
let webview = WebViewBuilder::new_auxiliary(&self.servo)
.delegate(parent_webview.delegate())
.hidpi_scale_factor(self.inner().hidpi_scale_factor)
.build();
self.add(webview.clone());
Some(webview)
@ -275,6 +276,7 @@ impl WebViewDelegate for RunningAppState {
impl RunningAppState {
pub(super) fn new(
initial_url: Option<String>,
hidpi_scale_factor: f32,
rendering_context: Rc<WindowRenderingContext>,
servo: Servo,
callbacks: Rc<ServoWindowCallbacks>,
@ -303,6 +305,7 @@ impl RunningAppState {
creation_order: vec![],
focused_webview_id: None,
animating_state_changed,
hidpi_scale_factor: Scale::new(hidpi_scale_factor),
}),
});
@ -313,6 +316,7 @@ impl RunningAppState {
pub(crate) fn new_toplevel_webview(self: &Rc<Self>, url: Url) {
let webview = WebViewBuilder::new(&self.servo)
.url(url)
.hidpi_scale_factor(self.inner().hidpi_scale_factor)
.delegate(self.clone())
.build();
@ -715,9 +719,3 @@ impl EmbedderMethods for ServoEmbedderCallbacks {
}
}
}
impl WindowMethods for ServoWindowCallbacks {
fn hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
self.hidpi_factor
}
}

View file

@ -124,7 +124,6 @@ pub fn init(
let window_callbacks = Rc::new(ServoWindowCallbacks::new(
callbacks,
RefCell::new(coordinates),
options.display_density as f32,
));
let embedder_callbacks = Box::new(ServoEmbedderCallbacks::new(
@ -138,12 +137,12 @@ pub fn init(
preferences,
rendering_context.clone(),
embedder_callbacks,
window_callbacks.clone(),
Default::default(),
);
let app_state = RunningAppState::new(
Some(options.url),
options.display_density as f32,
rendering_context,
servo,
window_callbacks,