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

@ -9,17 +9,21 @@ use std::rc::Rc;
use base::id::{PipelineId, WebViewId};
use compositing_traits::{RendererWebView, SendableFrameTree};
use constellation_traits::{EmbedderToConstellationMessage, ScrollState};
use constellation_traits::{EmbedderToConstellationMessage, ScrollState, WindowSizeType};
use embedder_traits::{
AnimationState, CompositorHitTestResult, InputEvent, MouseButton, MouseButtonAction,
MouseButtonEvent, MouseMoveEvent, ShutdownState, TouchEvent, TouchEventResult, TouchEventType,
TouchId,
TouchId, ViewportDetails,
};
use euclid::{Point2D, Scale, Vector2D};
use euclid::{Box2D, Point2D, Scale, Size2D, Vector2D};
use fnv::FnvHashSet;
use log::{debug, warn};
use servo_geometry::DeviceIndependentPixel;
use style_traits::{CSSPixel, PinchZoomFactor};
use webrender::Transaction;
use webrender_api::units::{DeviceIntPoint, DevicePoint, DeviceRect, LayoutVector2D};
use webrender_api::units::{
DeviceIntPoint, DeviceIntRect, DevicePixel, DevicePoint, DeviceRect, LayoutVector2D,
};
use webrender_api::{
ExternalScrollId, HitTestFlags, RenderReasons, SampledScrollOffset, ScrollLocation,
};
@ -28,6 +32,10 @@ use crate::IOCompositor;
use crate::compositor::{PipelineDetails, ServoRenderer};
use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState};
// Default viewport constraints
const MAX_ZOOM: f32 = 8.0;
const MIN_ZOOM: f32 = 0.1;
#[derive(Clone, Copy)]
struct ScrollEvent {
/// Scroll by this offset, or to Start or End
@ -65,6 +73,16 @@ pub(crate) struct WebView {
pending_scroll_zoom_events: Vec<ScrollZoomEvent>,
/// Touch input state machine
touch_handler: TouchHandler,
/// "Desktop-style" zoom that resizes the viewport to fit the window.
pub page_zoom: Scale<f32, CSSPixel, DeviceIndependentPixel>,
/// "Mobile-style" zoom that does not reflow the page.
viewport_zoom: PinchZoomFactor,
/// Viewport zoom constraints provided by @viewport.
min_viewport_zoom: Option<PinchZoomFactor>,
max_viewport_zoom: Option<PinchZoomFactor>,
/// The HiDPI scale factor for the `WebView` associated with this renderer. This is controlled
/// by the embedding layer.
hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
}
impl Drop for WebView {
@ -78,19 +96,26 @@ impl Drop for WebView {
impl WebView {
pub(crate) fn new(
renderer_webview: Box<dyn RendererWebView>,
rect: DeviceRect,
global: Rc<RefCell<ServoRenderer>>,
renderer_webview: Box<dyn RendererWebView>,
viewport_details: ViewportDetails,
) -> Self {
let hidpi_scale_factor = viewport_details.hidpi_scale_factor;
let size = viewport_details.size * viewport_details.hidpi_scale_factor;
Self {
id: renderer_webview.id(),
renderer_webview,
root_pipeline_id: None,
rect,
rect: DeviceRect::from_origin_and_size(DevicePoint::origin(), size),
pipelines: Default::default(),
touch_handler: TouchHandler::new(),
global,
pending_scroll_zoom_events: Default::default(),
page_zoom: Scale::new(1.0),
viewport_zoom: PinchZoomFactor::new(1.0),
min_viewport_zoom: Some(PinchZoomFactor::new(1.0)),
max_viewport_zoom: None,
hidpi_scale_factor: Scale::new(hidpi_scale_factor.0),
}
}
@ -265,7 +290,7 @@ impl WebView {
}
/// On a Window refresh tick (e.g. vsync)
pub fn on_vsync(&mut self) {
pub(crate) fn on_vsync(&mut self) {
if let Some(fling_action) = self.touch_handler.on_vsync() {
self.on_scroll_window_event(
ScrollLocation::Delta(fling_action.delta),
@ -300,7 +325,7 @@ impl WebView {
}
}
pub fn notify_input_event(&mut self, event: InputEvent) {
pub(crate) fn notify_input_event(&mut self, event: InputEvent) {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return;
}
@ -365,7 +390,7 @@ impl WebView {
}
}
pub fn on_touch_event(&mut self, event: TouchEvent) {
pub(crate) fn on_touch_event(&mut self, event: TouchEvent) {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return;
}
@ -644,7 +669,7 @@ impl WebView {
}));
}
pub fn notify_scroll_event(
pub(crate) fn notify_scroll_event(
&mut self,
scroll_location: ScrollLocation,
cursor: DeviceIntPoint,
@ -727,13 +752,12 @@ impl WebView {
}
}
let zoom_changed = compositor
.set_pinch_zoom_level(compositor.pinch_zoom_level().get() * combined_magnification);
let zoom_changed =
self.set_pinch_zoom_level(self.pinch_zoom_level().get() * combined_magnification);
let scroll_result = combined_scroll_event.and_then(|combined_event| {
self.scroll_node_at_device_point(
combined_event.cursor.to_f32(),
combined_event.scroll_location,
compositor,
)
});
if !zoom_changed && scroll_result.is_none() {
@ -769,11 +793,10 @@ impl WebView {
&mut self,
cursor: DevicePoint,
scroll_location: ScrollLocation,
compositor: &mut IOCompositor,
) -> Option<(PipelineId, ExternalScrollId, LayoutVector2D)> {
let scroll_location = match scroll_location {
ScrollLocation::Delta(delta) => {
let device_pixels_per_page = compositor.device_pixels_per_page_pixel();
let device_pixels_per_page = self.device_pixels_per_page_pixel();
let scaled_delta = (Vector2D::from_untyped(delta.to_untyped()) /
device_pixels_per_page)
.to_untyped();
@ -818,8 +841,39 @@ impl WebView {
None
}
pub(crate) fn pinch_zoom_level(&self) -> Scale<f32, DevicePixel, DevicePixel> {
Scale::new(self.viewport_zoom.get())
}
fn set_pinch_zoom_level(&mut self, mut zoom: f32) -> bool {
if let Some(min) = self.min_viewport_zoom {
zoom = f32::max(min.get(), zoom);
}
if let Some(max) = self.max_viewport_zoom {
zoom = f32::min(max.get(), zoom);
}
let old_zoom = std::mem::replace(&mut self.viewport_zoom, PinchZoomFactor::new(zoom));
old_zoom != self.viewport_zoom
}
pub(crate) fn set_page_zoom(&mut self, magnification: f32) {
self.page_zoom =
Scale::new((self.page_zoom.get() * magnification).clamp(MIN_ZOOM, MAX_ZOOM));
}
pub(crate) fn device_pixels_per_page_pixel(&self) -> Scale<f32, CSSPixel, DevicePixel> {
self.page_zoom * self.hidpi_scale_factor * self.pinch_zoom_level()
}
pub(crate) fn device_pixels_per_page_pixel_not_including_pinch_zoom(
&self,
) -> Scale<f32, CSSPixel, DevicePixel> {
self.page_zoom * self.hidpi_scale_factor
}
/// Simulate a pinch zoom
pub fn set_pinch_zoom(&mut self, magnification: f32) {
pub(crate) fn set_pinch_zoom(&mut self, magnification: f32) {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return;
}
@ -828,6 +882,71 @@ impl WebView {
self.pending_scroll_zoom_events
.push(ScrollZoomEvent::PinchZoom(magnification));
}
fn send_window_size_message(&self) {
// The device pixel ratio used by the style system should include the scale from page pixels
// to device pixels, but not including any pinch zoom.
let device_pixel_ratio = self.device_pixels_per_page_pixel_not_including_pinch_zoom();
let initial_viewport = self.rect.size().to_f32() / device_pixel_ratio;
let msg = EmbedderToConstellationMessage::ChangeViewportDetails(
self.id,
ViewportDetails {
hidpi_scale_factor: device_pixel_ratio,
size: initial_viewport,
},
WindowSizeType::Resize,
);
if let Err(e) = self.global.borrow().constellation_sender.send(msg) {
warn!("Sending window resize to constellation failed ({:?}).", e);
}
}
/// Set the `hidpi_scale_factor` for this renderer, returning `true` if the value actually changed.
pub(crate) fn set_hidpi_scale_factor(
&mut self,
new_scale: Scale<f32, DeviceIndependentPixel, DevicePixel>,
) -> bool {
let old_scale_factor = std::mem::replace(&mut self.hidpi_scale_factor, new_scale);
if self.hidpi_scale_factor == old_scale_factor {
return false;
}
self.send_window_size_message();
true
}
/// Set the `rect` for this renderer, returning `true` if the value actually changed.
pub(crate) fn set_rect(&mut self, new_rect: DeviceRect) -> bool {
let old_rect = std::mem::replace(&mut self.rect, new_rect);
if old_rect.size() != self.rect.size() {
self.send_window_size_message();
}
old_rect != self.rect
}
pub(crate) fn client_window_rect(
&self,
rendering_context_size: Size2D<u32, DevicePixel>,
) -> Box2D<i32, DeviceIndependentPixel> {
let screen_geometry = self.renderer_webview.screen_geometry().unwrap_or_default();
let rect = DeviceIntRect::from_origin_and_size(
screen_geometry.offset,
rendering_context_size.to_i32(),
)
.to_f32() /
self.hidpi_scale_factor;
rect.to_i32()
}
pub(crate) fn screen_size(&self) -> Size2D<i32, DeviceIndependentPixel> {
let screen_geometry = self.renderer_webview.screen_geometry().unwrap_or_default();
(screen_geometry.size.to_f32() / self.hidpi_scale_factor).to_i32()
}
pub(crate) fn available_screen_size(&self) -> Size2D<i32, DeviceIndependentPixel> {
let screen_geometry = self.renderer_webview.screen_geometry().unwrap_or_default();
(screen_geometry.available_size.to_f32() / self.hidpi_scale_factor).to_i32()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]