libservo: Start moving WindowMethods to WebViewDelegate (#36223)

`WindowMethods` is used by the embedding layer to get information from
the embedder. This change moves the functionality for getting screen
size and `WebView` offsets to `WebViewDelegate`.

This is important because `WebView`s might be on different screens or
have different offsets on the screen itself, so it makes sense for this
to be per-`WebView` and not global to the embedder. HiDPI and animation
state functionality will move to the embedder in subsequent changes.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>

<!-- Please describe your changes on the following line: -->


---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by
`[X]` when the step is complete, and replace `___` with appropriate
data: -->
- [x] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [x] These changes do not require tests because they just modify the
`WebView` API surface a bit.

<!-- Also, please make sure that "Allow edits from maintainers" checkbox
is checked, so that we can help you if you get stuck somewhere along the
way.-->

<!-- Pull requests that do not address these steps are welcome, but they
will require additional verification as part of the review process. -->

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-04-02 13:17:24 +02:00 committed by GitHub
parent 520a7f7bc5
commit b925c31424
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 235 additions and 183 deletions

View file

@ -26,7 +26,8 @@ use constellation_traits::{
use crossbeam_channel::Sender;
use dpi::PhysicalSize;
use embedder_traits::{
Cursor, InputEvent, MouseButtonEvent, MouseMoveEvent, ShutdownState, TouchEventType,
Cursor, InputEvent, MouseButtonEvent, MouseMoveEvent, ScreenGeometry, ShutdownState,
TouchEventType,
};
use euclid::{Box2D, Point2D, Rect, Scale, Size2D, Transform3D};
use fnv::FnvHashMap;
@ -54,11 +55,11 @@ use webrender_api::{
};
use webrender_traits::display_list::{HitTestInfo, ScrollTree};
use webrender_traits::rendering_context::RenderingContext;
use webrender_traits::{CrossProcessCompositorMessage, ImageUpdate};
use webrender_traits::{CrossProcessCompositorMessage, ImageUpdate, RendererWebView};
use crate::InitialCompositorState;
use crate::webview::{UnknownWebView, WebView, WebViewManager};
use crate::windowing::{self, EmbedderCoordinates, WebRenderDebugOption, WindowMethods};
use crate::windowing::{self, WebRenderDebugOption, WindowMethods};
#[derive(Debug, PartialEq)]
enum UnableToComposite {
@ -167,8 +168,9 @@ pub struct IOCompositor {
/// The surfman instance that webrender targets
rendering_context: Rc<dyn RenderingContext>,
/// The coordinates of the native window, its view and the screen.
embedder_coordinates: EmbedderCoordinates,
/// The HighDPI factor of the native window, its view and the screen.
/// TODO: Eventually this should be a property of the `WebView`.
hidpi_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
/// The number of frames pending to receive from WebRender.
pending_frames: usize,
@ -442,7 +444,7 @@ impl IOCompositor {
cursor_pos: DevicePoint::new(0.0, 0.0),
})),
webviews: WebViewManager::default(),
embedder_coordinates: window.get_coordinates(),
hidpi_factor: window.hidpi_factor(),
window,
needs_repaint: Cell::default(),
page_zoom: Scale::new(1.0),
@ -895,27 +897,46 @@ impl IOCompositor {
.collect();
let _ = result_sender.send((font_keys, font_instance_keys));
},
CrossProcessCompositorMessage::GetClientWindowRect(req) => {
if let Err(e) = req.send(self.embedder_coordinates.window_rect) {
warn!("Sending response to get client window failed ({:?}).", e);
CrossProcessCompositorMessage::GetClientWindowRect(webview_id, response_sender) => {
let screen_geometry = self.webview_screen_geometry(webview_id);
let rect = DeviceIntRect::from_origin_and_size(
screen_geometry.offset,
self.rendering_context.size2d().to_i32(),
)
.to_f32() /
self.hidpi_factor;
if let Err(error) = response_sender.send(rect.to_i32()) {
warn!("Sending response to get client window failed ({error:?}).");
}
},
CrossProcessCompositorMessage::GetScreenSize(req) => {
if let Err(e) = req.send(self.embedder_coordinates.screen_size) {
warn!("Sending response to get screen size failed ({:?}).", e);
CrossProcessCompositorMessage::GetScreenSize(webview_id, response_sender) => {
let screen_geometry = self.webview_screen_geometry(webview_id);
let screen_size = screen_geometry.size.to_f32() / self.hidpi_factor;
if let Err(error) = response_sender.send(screen_size.to_i32()) {
warn!("Sending response to get screen size failed ({error:?}).");
}
},
CrossProcessCompositorMessage::GetAvailableScreenSize(req) => {
if let Err(e) = req.send(self.embedder_coordinates.available_screen_size) {
warn!(
"Sending response to get screen avail size failed ({:?}).",
e
);
CrossProcessCompositorMessage::GetAvailableScreenSize(webview_id, response_sender) => {
let screen_geometry = self.webview_screen_geometry(webview_id);
let available_screen_size =
screen_geometry.available_size.to_f32() / self.hidpi_factor;
if let Err(error) = response_sender.send(available_screen_size.to_i32()) {
warn!("Sending response to get screen size failed ({error:?}).");
}
},
}
}
fn webview_screen_geometry(&self, webview_id: WebViewId) -> ScreenGeometry {
self.webviews
.get(webview_id)
.and_then(|webview| webview.renderer_webview.screen_geometry())
.unwrap_or_default()
}
/// Handle messages sent to the compositor during the shutdown process. In general,
/// the things the compositor can do in this state are limited. It's very important to
/// answer any synchronous messages though as other threads might be waiting on the
@ -961,25 +982,27 @@ impl IOCompositor {
let _ = result_sender.send((font_keys, font_instance_keys));
},
CompositorMsg::CrossProcess(CrossProcessCompositorMessage::GetClientWindowRect(
req,
_,
response_sender,
)) => {
if let Err(e) = req.send(self.embedder_coordinates.window_rect) {
warn!("Sending response to get client window failed ({:?}).", e);
if let Err(error) = response_sender.send(Default::default()) {
warn!("Sending response to get client window failed ({error:?}).");
}
},
CompositorMsg::CrossProcess(CrossProcessCompositorMessage::GetScreenSize(req)) => {
if let Err(e) = req.send(self.embedder_coordinates.screen_size) {
warn!("Sending response to get screen size failed ({:?}).", e);
CompositorMsg::CrossProcess(CrossProcessCompositorMessage::GetScreenSize(
_,
response_sender,
)) => {
if let Err(error) = response_sender.send(Default::default()) {
warn!("Sending response to get client window failed ({error:?}).");
}
},
CompositorMsg::CrossProcess(CrossProcessCompositorMessage::GetAvailableScreenSize(
req,
_,
response_sender,
)) => {
if let Err(e) = req.send(self.embedder_coordinates.available_screen_size) {
warn!(
"Sending response to get screen avail size failed ({:?}).",
e
);
if let Err(error) = response_sender.send(Default::default()) {
warn!("Sending response to get client window failed ({error:?}).");
}
},
CompositorMsg::NewWebRenderFrameReady(..) => {
@ -1102,10 +1125,10 @@ impl IOCompositor {
}
}
pub fn add_webview(&mut self, webview_id: WebViewId) {
pub fn add_webview(&mut self, webview: Box<dyn RendererWebView>) {
let size = self.rendering_context.size2d().to_f32();
self.webviews.entry(webview_id).or_insert(WebView::new(
webview_id,
self.webviews.entry(webview.id()).or_insert(WebView::new(
webview,
Box2D::from_origin_and_size(Point2D::origin(), size),
self.global.clone(),
));
@ -1239,20 +1262,14 @@ impl IOCompositor {
}
}
pub fn on_embedder_window_moved(&mut self) {
self.embedder_coordinates = self.window.get_coordinates();
}
pub fn resize_rendering_context(&mut self, new_size: PhysicalSize<u32>) -> bool {
if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown {
return false;
}
let old_hidpi_factor = self.embedder_coordinates.hidpi_factor;
self.embedder_coordinates = self.window.get_coordinates();
if self.embedder_coordinates.hidpi_factor == old_hidpi_factor &&
self.rendering_context.size() == new_size
{
let old_hidpi_factor = self.hidpi_factor;
self.hidpi_factor = self.window.hidpi_factor();
if self.hidpi_factor == old_hidpi_factor && self.rendering_context.size() == new_size {
return false;
}
@ -1303,10 +1320,6 @@ impl IOCompositor {
self.window.set_animation_state(animation_state);
}
fn hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
self.embedder_coordinates.hidpi_factor
}
pub(crate) fn device_pixels_per_page_pixel(&self) -> Scale<f32, CSSPixel, DevicePixel> {
self.device_pixels_per_page_pixel_not_including_page_zoom() * self.pinch_zoom_level()
}
@ -1314,7 +1327,7 @@ impl IOCompositor {
fn device_pixels_per_page_pixel_not_including_page_zoom(
&self,
) -> Scale<f32, CSSPixel, DevicePixel> {
self.page_zoom * self.hidpi_factor()
self.page_zoom * self.hidpi_factor
}
pub fn on_zoom_reset_window_event(&mut self) {

View file

@ -23,6 +23,7 @@ use webrender_api::units::{DeviceIntPoint, DevicePoint, DeviceRect, LayoutVector
use webrender_api::{
ExternalScrollId, HitTestFlags, RenderReasons, SampledScrollOffset, ScrollLocation,
};
use webrender_traits::RendererWebView;
use crate::IOCompositor;
use crate::compositor::{PipelineDetails, ServoRenderer};
@ -50,6 +51,10 @@ enum ScrollZoomEvent {
pub(crate) struct WebView {
/// The [`WebViewId`] of the `WebView` associated with this [`WebViewDetails`].
pub id: WebViewId,
/// The renderer's view of the embedding layer `WebView` as a trait implementation,
/// so that the renderer doesn't need to depend on the embedding layer. This avoids
/// a dependency cycle.
pub renderer_webview: Box<dyn RendererWebView>,
/// The root [`PipelineId`] of the currently displayed page in this WebView.
pub root_pipeline_id: Option<PipelineId>,
pub rect: DeviceRect,
@ -73,9 +78,14 @@ impl Drop for WebView {
}
impl WebView {
pub(crate) fn new(id: WebViewId, rect: DeviceRect, global: Rc<RefCell<ServoRenderer>>) -> Self {
pub(crate) fn new(
renderer_webview: Box<dyn RendererWebView>,
rect: DeviceRect,
global: Rc<RefCell<ServoRenderer>>,
) -> Self {
Self {
id,
id: renderer_webview.id(),
renderer_webview,
root_pipeline_id: None,
rect,
pipelines: Default::default(),

View file

@ -9,7 +9,7 @@ use std::fmt::Debug;
use embedder_traits::{EventLoopWaker, MouseButton};
use euclid::Scale;
use net::protocols::ProtocolRegistry;
use servo_geometry::{DeviceIndependentIntRect, DeviceIndependentIntSize, DeviceIndependentPixel};
use servo_geometry::DeviceIndependentPixel;
use webrender_api::units::{DevicePixel, DevicePoint};
#[derive(Clone)]
@ -37,12 +37,14 @@ pub enum AnimationState {
// for creating the GL context, making it current, buffer
// swapping, etc. Really that should all be done by surfman.
pub trait WindowMethods {
/// Get the coordinates of the native window, the screen and the framebuffer.
fn get_coordinates(&self) -> EmbedderCoordinates;
/// Get the HighDPI factor of the native window, the screen and the framebuffer.
/// TODO(martin): Move this to `RendererWebView` when possible.
fn hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
/// Set whether the application is currently animating.
/// Typically, when animations are active, the window
/// will want to avoid blocking on UI events, and just
/// run the event loop at the vsync interval.
/// TODO(martin): Move this to `RendererWebView` when possible.
fn set_animation_state(&self, _state: AnimationState);
}
@ -65,15 +67,3 @@ pub trait EmbedderMethods {
ProtocolRegistry::default()
}
}
#[derive(Clone, Copy, Debug)]
pub struct EmbedderCoordinates {
/// The pixel density of the display.
pub hidpi_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
/// Size of the screen.
pub screen_size: DeviceIndependentIntSize,
/// Size of the available screen space (screen without toolbars and docks).
pub available_screen_size: DeviceIndependentIntSize,
/// Position and size of the native window.
pub window_rect: DeviceIndependentIntRect,
}

View file

@ -35,28 +35,34 @@ impl Screen {
}
fn screen_size(&self) -> Size2D<u32, CSSPixel> {
let (send, recv) =
let (sender, receiver) =
ipc::channel::<DeviceIndependentIntSize>(self.global().time_profiler_chan().clone())
.unwrap();
self.window
.compositor_api()
.sender()
.send(CrossProcessCompositorMessage::GetScreenSize(send))
.send(CrossProcessCompositorMessage::GetScreenSize(
self.window.webview_id(),
sender,
))
.unwrap();
let size = recv.recv().unwrap_or(Size2D::zero()).to_u32();
let size = receiver.recv().unwrap_or(Size2D::zero()).to_u32();
Size2D::new(size.width, size.height)
}
fn screen_avail_size(&self) -> Size2D<u32, CSSPixel> {
let (send, recv) =
let (sender, receiver) =
ipc::channel::<DeviceIndependentIntSize>(self.global().time_profiler_chan().clone())
.unwrap();
self.window
.compositor_api()
.sender()
.send(CrossProcessCompositorMessage::GetAvailableScreenSize(send))
.send(CrossProcessCompositorMessage::GetAvailableScreenSize(
self.window.webview_id(),
sender,
))
.unwrap();
let size = recv.recv().unwrap_or(Size2D::zero()).to_u32();
let size = receiver.recv().unwrap_or(Size2D::zero()).to_u32();
Size2D::new(size.width, size.height)
}
}

View file

@ -1859,13 +1859,15 @@ impl Window {
fn client_window(&self) -> (Size2D<u32, CSSPixel>, Point2D<i32, CSSPixel>) {
let timer_profile_chan = self.global().time_profiler_chan().clone();
let (send, recv) =
let (sender, receiver) =
ProfiledIpc::channel::<DeviceIndependentIntRect>(timer_profile_chan).unwrap();
let _ = self
.compositor_api
.sender()
.send(webrender_traits::CrossProcessCompositorMessage::GetClientWindowRect(send));
let rect = recv.recv().unwrap_or_default();
let _ = self.compositor_api.sender().send(
webrender_traits::CrossProcessCompositorMessage::GetClientWindowRect(
self.webview_id(),
sender,
),
);
let rect = receiver.recv().unwrap_or_default();
(
Size2D::new(rect.size().width as u32, rect.size().height as u32),
Point2D::new(rect.min.x, rect.min.y),

View file

@ -1,6 +1,5 @@
/* 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/. */
* 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/. */
use std::cell::{Cell, RefCell};
use std::error::Error;
@ -13,7 +12,7 @@ use servo_geometry::DeviceIndependentPixel;
use tracing::warn;
use url::Url;
use webrender_api::ScrollLocation;
use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DevicePixel, LayoutVector2D};
use webrender_api::units::{DeviceIntPoint, DevicePixel, LayoutVector2D};
use winit::application::ApplicationHandler;
use winit::dpi::{PhysicalPosition, PhysicalSize};
use winit::event::{MouseScrollDelta, WindowEvent};
@ -250,26 +249,8 @@ impl WindowDelegate {
}
impl WindowMethods for WindowDelegate {
fn get_coordinates(&self) -> compositing::windowing::EmbedderCoordinates {
let monitor = self
.window
.current_monitor()
.or_else(|| self.window.available_monitors().nth(0))
.expect("Failed to get winit monitor");
let scale =
Scale::<f64, DeviceIndependentPixel, DevicePixel>::new(self.window.scale_factor());
let window_size = winit_size_to_euclid_size(self.window.outer_size()).to_i32();
let window_origin = self.window.outer_position().unwrap_or_default();
let window_origin = winit_position_to_euclid_point(window_origin).to_i32();
let window_rect = DeviceIntRect::from_origin_and_size(window_origin, window_size);
compositing::windowing::EmbedderCoordinates {
hidpi_factor: Scale::new(self.window.scale_factor() as f32),
screen_size: (winit_size_to_euclid_size(monitor.size()).to_f64() / scale).to_i32(),
available_screen_size: (winit_size_to_euclid_size(monitor.size()).to_f64() / scale)
.to_i32(),
window_rect: (window_rect.to_f64() / scale).to_i32(),
}
fn hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
Scale::new(self.window.scale_factor() as f32)
}
fn set_animation_state(&self, state: compositing::windowing::AnimationState) {

View file

@ -321,8 +321,7 @@ impl Servo {
None
};
let coordinates: compositing::windowing::EmbedderCoordinates = window.get_coordinates();
let device_pixel_ratio = coordinates.hidpi_factor.get();
let device_pixel_ratio = window.hidpi_factor().get();
let viewport_size = rendering_context.size2d();
let (mut webrender, webrender_api_sender) = {

View file

@ -13,11 +13,12 @@ use compositing::windowing::WebRenderDebugOption;
use constellation_traits::{ConstellationMsg, TraversalDirection};
use dpi::PhysicalSize;
use embedder_traits::{
Cursor, InputEvent, LoadStatus, MediaSessionActionType, Theme, TouchEventType,
Cursor, InputEvent, LoadStatus, MediaSessionActionType, ScreenGeometry, Theme, TouchEventType,
};
use url::Url;
use webrender_api::ScrollLocation;
use webrender_api::units::{DeviceIntPoint, DeviceRect};
use webrender_traits::RendererWebView;
use crate::ConstellationProxy;
use crate::clipboard_delegate::{ClipboardDelegate, DefaultClipboardDelegate};
@ -96,11 +97,10 @@ impl WebView {
compositor: Rc<RefCell<IOCompositor>>,
) -> Self {
let id = WebViewId::new();
compositor.borrow_mut().add_webview(id);
Self(Rc::new(RefCell::new(WebViewInner {
let webview = Self(Rc::new(RefCell::new(WebViewInner {
id,
constellation_proxy: constellation_proxy.clone(),
compositor,
compositor: compositor.clone(),
delegate: Rc::new(DefaultWebViewDelegate),
clipboard_delegate: Rc::new(DefaultClipboardDelegate),
rect: DeviceRect::zero(),
@ -111,7 +111,16 @@ impl WebView {
favicon_url: None,
focused: false,
cursor: Cursor::Pointer,
})))
})));
compositor
.borrow_mut()
.add_webview(Box::new(ServoRendererWebView {
weak_handle: webview.weak_handle(),
id,
}));
webview
}
fn inner(&self) -> Ref<'_, WebViewInner> {
@ -382,13 +391,6 @@ impl WebView {
.resize_rendering_context(new_size);
}
pub fn notify_embedder_window_moved(&self) {
self.inner()
.compositor
.borrow_mut()
.on_embedder_window_moved();
}
pub fn set_zoom(&self, new_zoom: f32) {
self.inner()
.compositor
@ -452,3 +454,21 @@ impl WebView {
self.inner().compositor.borrow_mut().render()
}
}
/// A structure used to expose a view of the [`WebView`] to the Servo
/// renderer, without having the Servo renderer depend on the embedding layer.
struct ServoRendererWebView {
id: WebViewId,
weak_handle: Weak<RefCell<WebViewInner>>,
}
impl RendererWebView for ServoRendererWebView {
fn screen_geometry(&self) -> Option<ScreenGeometry> {
let webview = WebView::from_weak_handle(&self.weak_handle)?;
webview.delegate().screen_geometry(webview)
}
fn id(&self) -> WebViewId {
self.id
}
}

View file

@ -9,7 +9,7 @@ use constellation_traits::ConstellationMsg;
use embedder_traits::{
AllowOrDeny, AuthenticationResponse, ContextMenuResult, Cursor, FilterPattern,
GamepadHapticEffectType, InputMethodType, LoadStatus, MediaSessionEvent, Notification,
PermissionFeature, SimpleDialog, WebResourceRequest, WebResourceResponse,
PermissionFeature, ScreenGeometry, SimpleDialog, WebResourceRequest, WebResourceResponse,
WebResourceResponseMsg,
};
use ipc_channel::ipc::IpcSender;
@ -297,6 +297,12 @@ impl Drop for InterceptedWebResourceLoad {
}
pub trait WebViewDelegate {
/// Get the [`ScreenGeometry`] for this [`WebView`]. If this is unimplemented or returns `None`
/// the screen will have the size of the [`WebView`]'s `RenderingContext` and `WebView` will be
/// considered to be positioned at the screen's origin.
fn screen_geometry(&self, _webview: WebView) -> Option<ScreenGeometry> {
None
}
/// The URL of the currently loaded page in this [`WebView`] has changed. The new
/// URL can accessed via [`WebView::url`].
fn notify_url_changed(&self, _webview: WebView, _url: Url) {}

View file

@ -636,3 +636,22 @@ pub struct NotificationAction {
/// Icon's raw image data and metadata.
pub icon_resource: Option<Arc<Image>>,
}
/// Information about a `WebView`'s screen geometry and offset. This is used
/// for the [Screen](https://drafts.csswg.org/cssom-view/#the-screen-interface)
/// CSSOM APIs and `window.screenLeft` / `window.screenTop`.
#[derive(Clone, Copy, Debug, Default)]
pub struct ScreenGeometry {
/// The size of the screen in device pixels. This will be converted to
/// CSS pixels based on the pixel scaling of the `WebView`.
pub size: DeviceIntSize,
/// The available size of the screen in device pixels. This size is the size
/// available for web content on the screen, and should be `size` minus any system
/// toolbars, docks, and interface elements of the browser. This will be converted to
/// CSS pixels based on the pixel scaling of the `WebView`.
pub available_size: DeviceIntSize,
/// The offset of the `WebView` in device pixels for the purposes of the `window.screenLeft`
/// and `window.screenTop` APIs. This will be converted to CSS pixels based on the pixel scaling
/// of the `WebView`.
pub offset: DeviceIntPoint,
}

View file

@ -14,6 +14,7 @@ use std::sync::{Arc, Mutex};
use base::id::WebViewId;
use constellation_traits::CompositorHitTestResult;
use display_list::CompositorDisplayListInfo;
use embedder_traits::ScreenGeometry;
use euclid::default::Size2D as UntypedSize2D;
use ipc_channel::ipc::{self, IpcSender, IpcSharedMemory};
use log::warn;
@ -82,12 +83,12 @@ pub enum CrossProcessCompositorMessage {
RemoveFonts(Vec<FontKey>, Vec<FontInstanceKey>),
/// Get the client window size and position.
GetClientWindowRect(IpcSender<DeviceIndependentIntRect>),
GetClientWindowRect(WebViewId, IpcSender<DeviceIndependentIntRect>),
/// Get the size of the screen that the client window inhabits.
GetScreenSize(IpcSender<DeviceIndependentIntSize>),
GetScreenSize(WebViewId, IpcSender<DeviceIndependentIntSize>),
/// Get the available screen size (without toolbars and docks) for the screen
/// the client window inhabits.
GetAvailableScreenSize(IpcSender<DeviceIndependentIntSize>),
GetAvailableScreenSize(WebViewId, IpcSender<DeviceIndependentIntSize>),
}
impl fmt::Debug for CrossProcessCompositorMessage {
@ -478,3 +479,11 @@ impl From<SerializableImageData> for ImageData {
}
}
}
/// A trait that exposes the embedding layer's `WebView` to the Servo renderer.
/// This is to prevent a dependency cycle between the renderer and the embedding
/// layer.
pub trait RendererWebView {
fn id(&self) -> WebViewId;
fn screen_geometry(&self) -> Option<ScreenGeometry>;
}