aboutsummaryrefslogtreecommitdiffstats
path: root/components/shared/compositing
diff options
context:
space:
mode:
Diffstat (limited to 'components/shared/compositing')
-rw-r--r--components/shared/compositing/Cargo.toml15
-rw-r--r--components/shared/compositing/display_list.rs373
-rw-r--r--components/shared/compositing/lib.rs482
-rw-r--r--components/shared/compositing/rendering_context.rs887
-rw-r--r--components/shared/compositing/tests/compositor.rs188
5 files changed, 1942 insertions, 3 deletions
diff --git a/components/shared/compositing/Cargo.toml b/components/shared/compositing/Cargo.toml
index 02488755cff..d1df90106cb 100644
--- a/components/shared/compositing/Cargo.toml
+++ b/components/shared/compositing/Cargo.toml
@@ -11,16 +11,27 @@ rust-version.workspace = true
name = "compositing_traits"
path = "lib.rs"
+[features]
+no-wgl = ["surfman/sm-angle-default"]
+
[dependencies]
base = { workspace = true }
crossbeam-channel = { workspace = true }
+dpi = { version = "0.1" }
embedder_traits = { workspace = true }
euclid = { workspace = true }
+gleam = { workspace = true }
+glow = { workspace = true }
+image = { workspace = true }
ipc-channel = { workspace = true }
log = { workspace = true }
pixels = { path = '../../pixels' }
-script_traits = { workspace = true }
+raw-window-handle = { version = "0.6" }
+serde = { workspace = true }
+servo_geometry = { path = "../../geometry" }
strum_macros = { workspace = true }
+stylo = { workspace = true }
stylo_traits = { workspace = true }
+surfman = { workspace = true, features = ["sm-x11"] }
webrender_api = { workspace = true }
-webrender_traits = { workspace = true }
+
diff --git a/components/shared/compositing/display_list.rs b/components/shared/compositing/display_list.rs
new file mode 100644
index 00000000000..2b7883c3ce1
--- /dev/null
+++ b/components/shared/compositing/display_list.rs
@@ -0,0 +1,373 @@
+/* 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/. */
+
+//! Defines data structures which are consumed by the Compositor.
+
+use base::id::ScrollTreeNodeId;
+use embedder_traits::Cursor;
+use serde::{Deserialize, Serialize};
+use style::values::specified::Overflow;
+use webrender_api::units::{LayoutSize, LayoutVector2D};
+use webrender_api::{Epoch, ExternalScrollId, PipelineId, ScrollLocation, SpatialId};
+
+/// The scroll sensitivity of a scroll node in a particular axis ie whether it can be scrolled due to
+/// input events and script events or only script events.
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
+pub enum ScrollSensitivity {
+ /// This node can be scrolled by input and script events.
+ ScriptAndInputEvents,
+ /// This node can only be scrolled by script events.
+ Script,
+ /// This node cannot be scrolled.
+ None,
+}
+
+/// Convert [Overflow] to [ScrollSensitivity].
+impl From<Overflow> for ScrollSensitivity {
+ fn from(overflow: Overflow) -> Self {
+ match overflow {
+ Overflow::Hidden => ScrollSensitivity::Script,
+ Overflow::Scroll | Overflow::Auto => ScrollSensitivity::ScriptAndInputEvents,
+ Overflow::Visible | Overflow::Clip => ScrollSensitivity::None,
+ }
+ }
+}
+
+/// The [ScrollSensitivity] of particular node in the vertical and horizontal axes.
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
+pub struct AxesScrollSensitivity {
+ pub x: ScrollSensitivity,
+ pub y: ScrollSensitivity,
+}
+
+/// Information that Servo keeps alongside WebRender display items
+/// in order to add more context to hit test results.
+#[derive(Debug, Deserialize, PartialEq, Serialize)]
+pub struct HitTestInfo {
+ /// The id of the node of this hit test item.
+ pub node: u64,
+
+ /// The cursor of this node's hit test item.
+ pub cursor: Option<Cursor>,
+
+ /// The id of the [ScrollTree] associated with this hit test item.
+ pub scroll_tree_node: ScrollTreeNodeId,
+}
+
+/// Data stored for nodes in the [ScrollTree] that actually scroll,
+/// as opposed to reference frames and sticky nodes which do not.
+#[derive(Debug, Deserialize, Serialize)]
+pub struct ScrollableNodeInfo {
+ /// The external scroll id of this node, used to track
+ /// it between successive re-layouts.
+ pub external_id: ExternalScrollId,
+
+ /// Amount that this `ScrollableNode` can scroll in both directions.
+ pub scrollable_size: LayoutSize,
+
+ /// Whether this `ScrollableNode` is sensitive to input events.
+ pub scroll_sensitivity: AxesScrollSensitivity,
+
+ /// The current offset of this scroll node.
+ pub offset: LayoutVector2D,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+/// A node in a tree of scroll nodes. This may either be a scrollable
+/// node which responds to scroll events or a non-scrollable one.
+pub struct ScrollTreeNode {
+ /// The index of the parent of this node in the tree. If this is
+ /// None then this is the root node.
+ pub parent: Option<ScrollTreeNodeId>,
+
+ /// Scrolling data which will not be None if this is a scrolling node.
+ pub scroll_info: Option<ScrollableNodeInfo>,
+}
+
+impl ScrollTreeNode {
+ /// Get the external id of this node.
+ pub fn external_id(&self) -> Option<ExternalScrollId> {
+ self.scroll_info.as_ref().map(|info| info.external_id)
+ }
+
+ /// Get the offset id of this node if it applies.
+ pub fn offset(&self) -> Option<LayoutVector2D> {
+ self.scroll_info.as_ref().map(|info| info.offset)
+ }
+
+ /// Set the offset for this node, returns false if this was a
+ /// non-scrolling node for which you cannot set the offset.
+ pub fn set_offset(&mut self, new_offset: LayoutVector2D) -> bool {
+ match self.scroll_info {
+ Some(ref mut info) => {
+ let scrollable_width = info.scrollable_size.width;
+ let scrollable_height = info.scrollable_size.height;
+
+ if scrollable_width > 0. {
+ info.offset.x = (new_offset.x).min(0.0).max(-scrollable_width);
+ }
+
+ if scrollable_height > 0. {
+ info.offset.y = (new_offset.y).min(0.0).max(-scrollable_height);
+ }
+ true
+ },
+ _ => false,
+ }
+ }
+
+ /// Scroll this node given a WebRender ScrollLocation. Returns a tuple that can
+ /// be used to scroll an individual WebRender scroll frame if the operation
+ /// actually changed an offset.
+ pub fn scroll(
+ &mut self,
+ scroll_location: ScrollLocation,
+ ) -> Option<(ExternalScrollId, LayoutVector2D)> {
+ let info = match self.scroll_info {
+ Some(ref mut data) => data,
+ None => return None,
+ };
+
+ if info.scroll_sensitivity.x != ScrollSensitivity::ScriptAndInputEvents &&
+ info.scroll_sensitivity.y != ScrollSensitivity::ScriptAndInputEvents
+ {
+ return None;
+ }
+
+ let delta = match scroll_location {
+ ScrollLocation::Delta(delta) => delta,
+ ScrollLocation::Start => {
+ if info.offset.y.round() >= 0.0 {
+ // Nothing to do on this layer.
+ return None;
+ }
+
+ info.offset.y = 0.0;
+ return Some((info.external_id, info.offset));
+ },
+ ScrollLocation::End => {
+ let end_pos = -info.scrollable_size.height;
+ if info.offset.y.round() <= end_pos {
+ // Nothing to do on this layer.
+ return None;
+ }
+
+ info.offset.y = end_pos;
+ return Some((info.external_id, info.offset));
+ },
+ };
+
+ let scrollable_width = info.scrollable_size.width;
+ let scrollable_height = info.scrollable_size.height;
+ let original_layer_scroll_offset = info.offset;
+
+ if scrollable_width > 0. &&
+ info.scroll_sensitivity.x == ScrollSensitivity::ScriptAndInputEvents
+ {
+ info.offset.x = (info.offset.x + delta.x).min(0.0).max(-scrollable_width);
+ }
+
+ if scrollable_height > 0. &&
+ info.scroll_sensitivity.y == ScrollSensitivity::ScriptAndInputEvents
+ {
+ info.offset.y = (info.offset.y + delta.y).min(0.0).max(-scrollable_height);
+ }
+
+ if info.offset != original_layer_scroll_offset {
+ Some((info.external_id, info.offset))
+ } else {
+ None
+ }
+ }
+}
+
+/// A tree of spatial nodes, which mirrors the spatial nodes in the WebRender
+/// display list, except these are used to scrolling in the compositor so that
+/// new offsets can be sent to WebRender.
+#[derive(Debug, Default, Deserialize, Serialize)]
+pub struct ScrollTree {
+ /// A list of compositor-side scroll nodes that describe the tree
+ /// of WebRender spatial nodes, used by the compositor to scroll the
+ /// contents of the display list.
+ pub nodes: Vec<ScrollTreeNode>,
+}
+
+impl ScrollTree {
+ /// Add a scroll node to this ScrollTree returning the id of the new node.
+ pub fn add_scroll_tree_node(
+ &mut self,
+ parent: Option<&ScrollTreeNodeId>,
+ spatial_id: SpatialId,
+ scroll_info: Option<ScrollableNodeInfo>,
+ ) -> ScrollTreeNodeId {
+ self.nodes.push(ScrollTreeNode {
+ parent: parent.cloned(),
+ scroll_info,
+ });
+ ScrollTreeNodeId {
+ index: self.nodes.len() - 1,
+ spatial_id,
+ }
+ }
+
+ /// Get a mutable reference to the node with the given index.
+ pub fn get_node_mut(&mut self, id: &ScrollTreeNodeId) -> &mut ScrollTreeNode {
+ &mut self.nodes[id.index]
+ }
+
+ /// Get an immutable reference to the node with the given index.
+ pub fn get_node(&mut self, id: &ScrollTreeNodeId) -> &ScrollTreeNode {
+ &self.nodes[id.index]
+ }
+
+ /// Scroll the given scroll node on this scroll tree. If the node cannot be scrolled,
+ /// because it isn't a scrollable node or it's already scrolled to the maximum scroll
+ /// extent, try to scroll an ancestor of this node. Returns the node scrolled and the
+ /// new offset if a scroll was performed, otherwise returns None.
+ pub fn scroll_node_or_ancestor(
+ &mut self,
+ scroll_node_id: &ScrollTreeNodeId,
+ scroll_location: ScrollLocation,
+ ) -> Option<(ExternalScrollId, LayoutVector2D)> {
+ let parent = {
+ let node = &mut self.get_node_mut(scroll_node_id);
+ let result = node.scroll(scroll_location);
+ if result.is_some() {
+ return result;
+ }
+ node.parent
+ };
+
+ parent.and_then(|parent| self.scroll_node_or_ancestor(&parent, scroll_location))
+ }
+
+ /// Given an [`ExternalScrollId`] and an offset, update the scroll offset of the scroll node
+ /// with the given id.
+ pub fn set_scroll_offsets_for_node_with_external_scroll_id(
+ &mut self,
+ external_scroll_id: ExternalScrollId,
+ offset: LayoutVector2D,
+ ) -> bool {
+ for node in self.nodes.iter_mut() {
+ match node.scroll_info {
+ Some(ref mut scroll_info) if scroll_info.external_id == external_scroll_id => {
+ scroll_info.offset = offset;
+ return true;
+ },
+ _ => {},
+ }
+ }
+ false
+ }
+}
+
+/// A data structure which stores compositor-side information about
+/// display lists sent to the compositor.
+#[derive(Debug, Deserialize, Serialize)]
+pub struct CompositorDisplayListInfo {
+ /// The WebRender [PipelineId] of this display list.
+ pub pipeline_id: PipelineId,
+
+ /// The size of the viewport that this display list renders into.
+ pub viewport_size: LayoutSize,
+
+ /// The size of this display list's content.
+ pub content_size: LayoutSize,
+
+ /// The epoch of the display list.
+ pub epoch: Epoch,
+
+ /// An array of `HitTestInfo` which is used to store information
+ /// to assist the compositor to take various actions (set the cursor,
+ /// scroll without layout) using a WebRender hit test result.
+ pub hit_test_info: Vec<HitTestInfo>,
+
+ /// A ScrollTree used by the compositor to scroll the contents of the
+ /// display list.
+ pub scroll_tree: ScrollTree,
+
+ /// The `ScrollTreeNodeId` of the root reference frame of this info's scroll
+ /// tree.
+ pub root_reference_frame_id: ScrollTreeNodeId,
+
+ /// The `ScrollTreeNodeId` of the topmost scrolling frame of this info's scroll
+ /// tree.
+ pub root_scroll_node_id: ScrollTreeNodeId,
+
+ /// Contentful paint i.e. whether the display list contains items of type
+ /// text, image, non-white canvas or SVG). Used by metrics.
+ /// See <https://w3c.github.io/paint-timing/#first-contentful-paint>.
+ pub is_contentful: bool,
+
+ /// Whether the first layout or a subsequent (incremental) layout triggered this
+ /// display list creation.
+ pub first_reflow: bool,
+}
+
+impl CompositorDisplayListInfo {
+ /// Create a new CompositorDisplayListInfo with the root reference frame
+ /// and scroll frame already added to the scroll tree.
+ pub fn new(
+ viewport_size: LayoutSize,
+ content_size: LayoutSize,
+ pipeline_id: PipelineId,
+ epoch: Epoch,
+ viewport_scroll_sensitivity: AxesScrollSensitivity,
+ first_reflow: bool,
+ ) -> Self {
+ let mut scroll_tree = ScrollTree::default();
+ let root_reference_frame_id = scroll_tree.add_scroll_tree_node(
+ None,
+ SpatialId::root_reference_frame(pipeline_id),
+ None,
+ );
+ let root_scroll_node_id = scroll_tree.add_scroll_tree_node(
+ Some(&root_reference_frame_id),
+ SpatialId::root_scroll_node(pipeline_id),
+ Some(ScrollableNodeInfo {
+ external_id: ExternalScrollId(0, pipeline_id),
+ scrollable_size: content_size - viewport_size,
+ scroll_sensitivity: viewport_scroll_sensitivity,
+ offset: LayoutVector2D::zero(),
+ }),
+ );
+
+ CompositorDisplayListInfo {
+ pipeline_id,
+ viewport_size,
+ content_size,
+ epoch,
+ hit_test_info: Default::default(),
+ scroll_tree,
+ root_reference_frame_id,
+ root_scroll_node_id,
+ is_contentful: false,
+ first_reflow,
+ }
+ }
+
+ /// Add or re-use a duplicate HitTestInfo entry in this `CompositorHitTestInfo`
+ /// and return the index.
+ pub fn add_hit_test_info(
+ &mut self,
+ node: u64,
+ cursor: Option<Cursor>,
+ scroll_tree_node: ScrollTreeNodeId,
+ ) -> usize {
+ let hit_test_info = HitTestInfo {
+ node,
+ cursor,
+ scroll_tree_node,
+ };
+
+ if let Some(last) = self.hit_test_info.last() {
+ if hit_test_info == *last {
+ return self.hit_test_info.len() - 1;
+ }
+ }
+
+ self.hit_test_info.push(hit_test_info);
+ self.hit_test_info.len() - 1
+ }
+}
diff --git a/components/shared/compositing/lib.rs b/components/shared/compositing/lib.rs
index 9c13afb8dc3..c231960ef50 100644
--- a/components/shared/compositing/lib.rs
+++ b/components/shared/compositing/lib.rs
@@ -18,7 +18,27 @@ use pixels::Image;
use strum_macros::IntoStaticStr;
use style_traits::CSSPixel;
use webrender_api::DocumentId;
-use webrender_traits::{CrossProcessCompositorApi, CrossProcessCompositorMessage};
+
+pub mod display_list;
+pub mod rendering_context;
+
+use core::fmt;
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex};
+
+use display_list::CompositorDisplayListInfo;
+use embedder_traits::{CompositorHitTestResult, ScreenGeometry};
+use euclid::default::Size2D as UntypedSize2D;
+use ipc_channel::ipc::{self, IpcSharedMemory};
+use serde::{Deserialize, Serialize};
+use servo_geometry::{DeviceIndependentIntRect, DeviceIndependentIntSize};
+use webrender_api::units::{DevicePoint, LayoutPoint, TexelRect};
+use webrender_api::{
+ BuiltDisplayList, BuiltDisplayListDescriptor, ExternalImage, ExternalImageData,
+ ExternalImageHandler, ExternalImageId, ExternalImageSource, ExternalScrollId,
+ FontInstanceFlags, FontInstanceKey, FontKey, HitTestFlags, ImageData, ImageDescriptor,
+ ImageKey, NativeFontHandle, PipelineId as WebRenderPipelineId,
+};
/// Sends messages to the compositor.
#[derive(Clone)]
@@ -111,3 +131,463 @@ impl Debug for CompositorMsg {
write!(formatter, "{string}")
}
}
+
+#[derive(Deserialize, Serialize)]
+pub enum CrossProcessCompositorMessage {
+ /// Inform WebRender of the existence of this pipeline.
+ SendInitialTransaction(WebRenderPipelineId),
+ /// Perform a scroll operation.
+ SendScrollNode(
+ WebViewId,
+ WebRenderPipelineId,
+ LayoutPoint,
+ ExternalScrollId,
+ ),
+ /// Inform WebRender of a new display list for the given pipeline.
+ SendDisplayList {
+ /// The [`WebViewId`] that this display list belongs to.
+ webview_id: WebViewId,
+ /// The [CompositorDisplayListInfo] that describes the display list being sent.
+ display_list_info: Box<CompositorDisplayListInfo>,
+ /// A descriptor of this display list used to construct this display list from raw data.
+ display_list_descriptor: BuiltDisplayListDescriptor,
+ /// An [ipc::IpcBytesReceiver] used to send the raw data of the display list.
+ display_list_receiver: ipc::IpcBytesReceiver,
+ },
+ /// Perform a hit test operation. The result will be returned via
+ /// the provided channel sender.
+ HitTest(
+ Option<WebRenderPipelineId>,
+ DevicePoint,
+ HitTestFlags,
+ IpcSender<Vec<CompositorHitTestResult>>,
+ ),
+ /// Create a new image key. The result will be returned via the
+ /// provided channel sender.
+ GenerateImageKey(IpcSender<ImageKey>),
+ /// Add an image with the given data and `ImageKey`.
+ AddImage(ImageKey, ImageDescriptor, SerializableImageData),
+ /// Perform a resource update operation.
+ UpdateImages(Vec<ImageUpdate>),
+
+ /// Generate a new batch of font keys which can be used to allocate
+ /// keys asynchronously.
+ GenerateFontKeys(
+ usize,
+ usize,
+ IpcSender<(Vec<FontKey>, Vec<FontInstanceKey>)>,
+ ),
+ /// Add a font with the given data and font key.
+ AddFont(FontKey, Arc<IpcSharedMemory>, u32),
+ /// Add a system font with the given font key and handle.
+ AddSystemFont(FontKey, NativeFontHandle),
+ /// Add an instance of a font with the given instance key.
+ AddFontInstance(FontInstanceKey, FontKey, f32, FontInstanceFlags),
+ /// Remove the given font resources from our WebRender instance.
+ RemoveFonts(Vec<FontKey>, Vec<FontInstanceKey>),
+
+ /// Get the client window size and position.
+ GetClientWindowRect(WebViewId, IpcSender<DeviceIndependentIntRect>),
+ /// Get the size of the screen that the client window inhabits.
+ GetScreenSize(WebViewId, IpcSender<DeviceIndependentIntSize>),
+ /// Get the available screen size (without toolbars and docks) for the screen
+ /// the client window inhabits.
+ GetAvailableScreenSize(WebViewId, IpcSender<DeviceIndependentIntSize>),
+}
+
+impl fmt::Debug for CrossProcessCompositorMessage {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::AddImage(..) => f.write_str("AddImage"),
+ Self::GenerateFontKeys(..) => f.write_str("GenerateFontKeys"),
+ Self::AddSystemFont(..) => f.write_str("AddSystemFont"),
+ Self::SendInitialTransaction(..) => f.write_str("SendInitialTransaction"),
+ Self::SendScrollNode(..) => f.write_str("SendScrollNode"),
+ Self::SendDisplayList { .. } => f.write_str("SendDisplayList"),
+ Self::HitTest(..) => f.write_str("HitTest"),
+ Self::GenerateImageKey(..) => f.write_str("GenerateImageKey"),
+ Self::UpdateImages(..) => f.write_str("UpdateImages"),
+ Self::RemoveFonts(..) => f.write_str("RemoveFonts"),
+ Self::AddFontInstance(..) => f.write_str("AddFontInstance"),
+ Self::AddFont(..) => f.write_str("AddFont"),
+ Self::GetClientWindowRect(..) => f.write_str("GetClientWindowRect"),
+ Self::GetScreenSize(..) => f.write_str("GetScreenSize"),
+ Self::GetAvailableScreenSize(..) => f.write_str("GetAvailableScreenSize"),
+ }
+ }
+}
+
+/// A mechanism to send messages from ScriptThread to the parent process' WebRender instance.
+#[derive(Clone, Deserialize, Serialize)]
+pub struct CrossProcessCompositorApi(pub IpcSender<CrossProcessCompositorMessage>);
+
+impl CrossProcessCompositorApi {
+ /// Create a new [`CrossProcessCompositorApi`] struct that does not have a listener on the other
+ /// end to use for unit testing.
+ pub fn dummy() -> Self {
+ let (sender, _) = ipc::channel().unwrap();
+ Self(sender)
+ }
+
+ /// Get the sender for this proxy.
+ pub fn sender(&self) -> &IpcSender<CrossProcessCompositorMessage> {
+ &self.0
+ }
+
+ /// Inform WebRender of the existence of this pipeline.
+ pub fn send_initial_transaction(&self, pipeline: WebRenderPipelineId) {
+ if let Err(e) = self
+ .0
+ .send(CrossProcessCompositorMessage::SendInitialTransaction(
+ pipeline,
+ ))
+ {
+ warn!("Error sending initial transaction: {}", e);
+ }
+ }
+
+ /// Perform a scroll operation.
+ pub fn send_scroll_node(
+ &self,
+ webview_id: WebViewId,
+ pipeline_id: WebRenderPipelineId,
+ point: LayoutPoint,
+ scroll_id: ExternalScrollId,
+ ) {
+ if let Err(e) = self.0.send(CrossProcessCompositorMessage::SendScrollNode(
+ webview_id,
+ pipeline_id,
+ point,
+ scroll_id,
+ )) {
+ warn!("Error sending scroll node: {}", e);
+ }
+ }
+
+ /// Inform WebRender of a new display list for the given pipeline.
+ pub fn send_display_list(
+ &self,
+ webview_id: WebViewId,
+ display_list_info: CompositorDisplayListInfo,
+ list: BuiltDisplayList,
+ ) {
+ let (display_list_data, display_list_descriptor) = list.into_data();
+ let (display_list_sender, display_list_receiver) = ipc::bytes_channel().unwrap();
+ if let Err(e) = self.0.send(CrossProcessCompositorMessage::SendDisplayList {
+ webview_id,
+ display_list_info: Box::new(display_list_info),
+ display_list_descriptor,
+ display_list_receiver,
+ }) {
+ warn!("Error sending display list: {}", e);
+ }
+
+ if let Err(error) = display_list_sender.send(&display_list_data.items_data) {
+ warn!("Error sending display list items: {}", error);
+ }
+ if let Err(error) = display_list_sender.send(&display_list_data.cache_data) {
+ warn!("Error sending display list cache data: {}", error);
+ }
+ if let Err(error) = display_list_sender.send(&display_list_data.spatial_tree) {
+ warn!("Error sending display spatial tree: {}", error);
+ }
+ }
+
+ /// Perform a hit test operation. Blocks until the operation is complete and
+ /// and a result is available.
+ pub fn hit_test(
+ &self,
+ pipeline: Option<WebRenderPipelineId>,
+ point: DevicePoint,
+ flags: HitTestFlags,
+ ) -> Vec<CompositorHitTestResult> {
+ let (sender, receiver) = ipc::channel().unwrap();
+ self.0
+ .send(CrossProcessCompositorMessage::HitTest(
+ pipeline, point, flags, sender,
+ ))
+ .expect("error sending hit test");
+ receiver.recv().expect("error receiving hit test result")
+ }
+
+ /// Create a new image key. Blocks until the key is available.
+ pub fn generate_image_key(&self) -> Option<ImageKey> {
+ let (sender, receiver) = ipc::channel().unwrap();
+ self.0
+ .send(CrossProcessCompositorMessage::GenerateImageKey(sender))
+ .ok()?;
+ receiver.recv().ok()
+ }
+
+ pub fn add_image(
+ &self,
+ key: ImageKey,
+ descriptor: ImageDescriptor,
+ data: SerializableImageData,
+ ) {
+ if let Err(e) = self.0.send(CrossProcessCompositorMessage::AddImage(
+ key, descriptor, data,
+ )) {
+ warn!("Error sending image update: {}", e);
+ }
+ }
+
+ /// Perform an image resource update operation.
+ pub fn update_images(&self, updates: Vec<ImageUpdate>) {
+ if let Err(e) = self
+ .0
+ .send(CrossProcessCompositorMessage::UpdateImages(updates))
+ {
+ warn!("error sending image updates: {}", e);
+ }
+ }
+
+ pub fn remove_unused_font_resources(
+ &self,
+ keys: Vec<FontKey>,
+ instance_keys: Vec<FontInstanceKey>,
+ ) {
+ if keys.is_empty() && instance_keys.is_empty() {
+ return;
+ }
+ let _ = self.0.send(CrossProcessCompositorMessage::RemoveFonts(
+ keys,
+ instance_keys,
+ ));
+ }
+
+ pub fn add_font_instance(
+ &self,
+ font_instance_key: FontInstanceKey,
+ font_key: FontKey,
+ size: f32,
+ flags: FontInstanceFlags,
+ ) {
+ let _x = self.0.send(CrossProcessCompositorMessage::AddFontInstance(
+ font_instance_key,
+ font_key,
+ size,
+ flags,
+ ));
+ }
+
+ pub fn add_font(&self, font_key: FontKey, data: Arc<IpcSharedMemory>, index: u32) {
+ let _ = self.0.send(CrossProcessCompositorMessage::AddFont(
+ font_key, data, index,
+ ));
+ }
+
+ pub fn add_system_font(&self, font_key: FontKey, handle: NativeFontHandle) {
+ let _ = self.0.send(CrossProcessCompositorMessage::AddSystemFont(
+ font_key, handle,
+ ));
+ }
+
+ pub fn fetch_font_keys(
+ &self,
+ number_of_font_keys: usize,
+ number_of_font_instance_keys: usize,
+ ) -> (Vec<FontKey>, Vec<FontInstanceKey>) {
+ let (sender, receiver) = ipc_channel::ipc::channel().expect("Could not create IPC channel");
+ let _ = self.0.send(CrossProcessCompositorMessage::GenerateFontKeys(
+ number_of_font_keys,
+ number_of_font_instance_keys,
+ sender,
+ ));
+ receiver.recv().unwrap()
+ }
+}
+
+/// This trait is used as a bridge between the different GL clients
+/// in Servo that handles WebRender ExternalImages and the WebRender
+/// ExternalImageHandler API.
+//
+/// This trait is used to notify lock/unlock messages and get the
+/// required info that WR needs.
+pub trait WebrenderExternalImageApi {
+ fn lock(&mut self, id: u64) -> (WebrenderImageSource, UntypedSize2D<i32>);
+ fn unlock(&mut self, id: u64);
+}
+
+pub enum WebrenderImageSource<'a> {
+ TextureHandle(u32),
+ Raw(&'a [u8]),
+}
+
+/// Type of Webrender External Image Handler.
+pub enum WebrenderImageHandlerType {
+ WebGL,
+ Media,
+ WebGPU,
+}
+
+/// List of Webrender external images to be shared among all external image
+/// consumers (WebGL, Media, WebGPU).
+/// It ensures that external image identifiers are unique.
+#[derive(Default)]
+pub struct WebrenderExternalImageRegistry {
+ /// Map of all generated external images.
+ external_images: HashMap<ExternalImageId, WebrenderImageHandlerType>,
+ /// Id generator for the next external image identifier.
+ next_image_id: u64,
+}
+
+impl WebrenderExternalImageRegistry {
+ pub fn next_id(&mut self, handler_type: WebrenderImageHandlerType) -> ExternalImageId {
+ self.next_image_id += 1;
+ let key = ExternalImageId(self.next_image_id);
+ self.external_images.insert(key, handler_type);
+ key
+ }
+
+ pub fn remove(&mut self, key: &ExternalImageId) {
+ self.external_images.remove(key);
+ }
+
+ pub fn get(&self, key: &ExternalImageId) -> Option<&WebrenderImageHandlerType> {
+ self.external_images.get(key)
+ }
+}
+
+/// WebRender External Image Handler implementation.
+pub struct WebrenderExternalImageHandlers {
+ /// WebGL handler.
+ webgl_handler: Option<Box<dyn WebrenderExternalImageApi>>,
+ /// Media player handler.
+ media_handler: Option<Box<dyn WebrenderExternalImageApi>>,
+ /// WebGPU handler.
+ webgpu_handler: Option<Box<dyn WebrenderExternalImageApi>>,
+ /// Webrender external images.
+ external_images: Arc<Mutex<WebrenderExternalImageRegistry>>,
+}
+
+impl WebrenderExternalImageHandlers {
+ pub fn new() -> (Self, Arc<Mutex<WebrenderExternalImageRegistry>>) {
+ let external_images = Arc::new(Mutex::new(WebrenderExternalImageRegistry::default()));
+ (
+ Self {
+ webgl_handler: None,
+ media_handler: None,
+ webgpu_handler: None,
+ external_images: external_images.clone(),
+ },
+ external_images,
+ )
+ }
+
+ pub fn set_handler(
+ &mut self,
+ handler: Box<dyn WebrenderExternalImageApi>,
+ handler_type: WebrenderImageHandlerType,
+ ) {
+ match handler_type {
+ WebrenderImageHandlerType::WebGL => self.webgl_handler = Some(handler),
+ WebrenderImageHandlerType::Media => self.media_handler = Some(handler),
+ WebrenderImageHandlerType::WebGPU => self.webgpu_handler = Some(handler),
+ }
+ }
+}
+
+impl ExternalImageHandler for WebrenderExternalImageHandlers {
+ /// Lock the external image. Then, WR could start to read the
+ /// image content.
+ /// The WR client should not change the image content until the
+ /// unlock() call.
+ fn lock(&mut self, key: ExternalImageId, _channel_index: u8) -> ExternalImage {
+ let external_images = self.external_images.lock().unwrap();
+ let handler_type = external_images
+ .get(&key)
+ .expect("Tried to get unknown external image");
+ match handler_type {
+ WebrenderImageHandlerType::WebGL => {
+ let (source, size) = self.webgl_handler.as_mut().unwrap().lock(key.0);
+ let texture_id = match source {
+ WebrenderImageSource::TextureHandle(b) => b,
+ _ => panic!("Wrong type"),
+ };
+ ExternalImage {
+ uv: TexelRect::new(0.0, size.height as f32, size.width as f32, 0.0),
+ source: ExternalImageSource::NativeTexture(texture_id),
+ }
+ },
+ WebrenderImageHandlerType::Media => {
+ let (source, size) = self.media_handler.as_mut().unwrap().lock(key.0);
+ let texture_id = match source {
+ WebrenderImageSource::TextureHandle(b) => b,
+ _ => panic!("Wrong type"),
+ };
+ ExternalImage {
+ uv: TexelRect::new(0.0, size.height as f32, size.width as f32, 0.0),
+ source: ExternalImageSource::NativeTexture(texture_id),
+ }
+ },
+ WebrenderImageHandlerType::WebGPU => {
+ let (source, size) = self.webgpu_handler.as_mut().unwrap().lock(key.0);
+ let buffer = match source {
+ WebrenderImageSource::Raw(b) => b,
+ _ => panic!("Wrong type"),
+ };
+ ExternalImage {
+ uv: TexelRect::new(0.0, size.height as f32, size.width as f32, 0.0),
+ source: ExternalImageSource::RawData(buffer),
+ }
+ },
+ }
+ }
+
+ /// Unlock the external image. The WR should not read the image
+ /// content after this call.
+ fn unlock(&mut self, key: ExternalImageId, _channel_index: u8) {
+ let external_images = self.external_images.lock().unwrap();
+ let handler_type = external_images
+ .get(&key)
+ .expect("Tried to get unknown external image");
+ match handler_type {
+ WebrenderImageHandlerType::WebGL => self.webgl_handler.as_mut().unwrap().unlock(key.0),
+ WebrenderImageHandlerType::Media => self.media_handler.as_mut().unwrap().unlock(key.0),
+ WebrenderImageHandlerType::WebGPU => {
+ self.webgpu_handler.as_mut().unwrap().unlock(key.0)
+ },
+ };
+ }
+}
+
+#[derive(Deserialize, Serialize)]
+/// Serializable image updates that must be performed by WebRender.
+pub enum ImageUpdate {
+ /// Register a new image.
+ AddImage(ImageKey, ImageDescriptor, SerializableImageData),
+ /// Delete a previously registered image registration.
+ DeleteImage(ImageKey),
+ /// Update an existing image registration.
+ UpdateImage(ImageKey, ImageDescriptor, SerializableImageData),
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+/// Serialized `ImageData`. It contains IPC byte channel receiver to prevent from loading bytes too
+/// slow.
+pub enum SerializableImageData {
+ /// A simple series of bytes, provided by the embedding and owned by WebRender.
+ /// The format is stored out-of-band, currently in ImageDescriptor.
+ Raw(IpcSharedMemory),
+ /// An image owned by the embedding, and referenced by WebRender. This may
+ /// take the form of a texture or a heap-allocated buffer.
+ External(ExternalImageData),
+}
+
+impl From<SerializableImageData> for ImageData {
+ fn from(value: SerializableImageData) -> Self {
+ match value {
+ SerializableImageData::Raw(shared_memory) => ImageData::new(shared_memory.to_vec()),
+ SerializableImageData::External(image) => ImageData::External(image),
+ }
+ }
+}
+
+/// 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>;
+}
diff --git a/components/shared/compositing/rendering_context.rs b/components/shared/compositing/rendering_context.rs
new file mode 100644
index 00000000000..46d72917510
--- /dev/null
+++ b/components/shared/compositing/rendering_context.rs
@@ -0,0 +1,887 @@
+/* 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/. */
+
+#![deny(unsafe_code)]
+
+use std::cell::{Cell, RefCell, RefMut};
+use std::num::NonZeroU32;
+use std::rc::Rc;
+use std::sync::Arc;
+
+use dpi::PhysicalSize;
+use euclid::default::{Rect, Size2D as UntypedSize2D};
+use euclid::{Point2D, Size2D};
+use gleam::gl::{self, Gl};
+use glow::NativeFramebuffer;
+use image::RgbaImage;
+use log::{debug, trace, warn};
+use raw_window_handle::{DisplayHandle, WindowHandle};
+pub use surfman::Error;
+use surfman::chains::{PreserveBuffer, SwapChain};
+use surfman::{
+ Adapter, Connection, Context, ContextAttributeFlags, ContextAttributes, Device, GLApi,
+ NativeContext, NativeWidget, Surface, SurfaceAccess, SurfaceInfo, SurfaceTexture, SurfaceType,
+};
+use webrender_api::units::{DeviceIntRect, DevicePixel};
+
+/// The `RenderingContext` trait defines a set of methods for managing
+/// an OpenGL or GLES rendering context.
+/// Implementors of this trait are responsible for handling the creation,
+/// management, and destruction of the rendering context and its associated
+/// resources.
+pub trait RenderingContext {
+ /// Prepare this [`RenderingContext`] to be rendered upon by Servo. For instance,
+ /// by binding a framebuffer to the current OpenGL context.
+ fn prepare_for_rendering(&self) {}
+ /// Read the contents of this [`Renderingcontext`] into an in-memory image. If the
+ /// image cannot be read (for instance, if no rendering has taken place yet), then
+ /// `None` is returned.
+ ///
+ /// In a double-buffered [`RenderingContext`] this is expected to read from the back
+ /// buffer. That means that once Servo renders to the context, this should return those
+ /// results, even before [`RenderingContext::present`] is called.
+ fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage>;
+ /// Get the current size of this [`RenderingContext`].
+ fn size(&self) -> PhysicalSize<u32>;
+ /// Get the current size of this [`RenderingContext`] as [`Size2D`].
+ fn size2d(&self) -> Size2D<u32, DevicePixel> {
+ let size = self.size();
+ Size2D::new(size.width, size.height)
+ }
+ /// Resizes the rendering surface to the given size.
+ fn resize(&self, size: PhysicalSize<u32>);
+ /// Presents the rendered frame to the screen. In a double-buffered context, this would
+ /// swap buffers.
+ fn present(&self);
+ /// Makes the context the current OpenGL context for this thread.
+ /// After calling this function, it is valid to use OpenGL rendering
+ /// commands.
+ fn make_current(&self) -> Result<(), Error>;
+ /// Returns the `gleam` version of the OpenGL or GLES API.
+ fn gleam_gl_api(&self) -> Rc<dyn gleam::gl::Gl>;
+ /// Returns the OpenGL or GLES API.
+ fn glow_gl_api(&self) -> Arc<glow::Context>;
+ /// Creates a texture from a given surface and returns the surface texture,
+ /// the OpenGL texture object, and the size of the surface. Default to `None`.
+ fn create_texture(
+ &self,
+ _surface: Surface,
+ ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> {
+ None
+ }
+ /// Destroys the texture and returns the surface. Default to `None`.
+ fn destroy_texture(&self, _surface_texture: SurfaceTexture) -> Option<Surface> {
+ None
+ }
+ /// The connection to the display server for WebGL. Default to `None`.
+ fn connection(&self) -> Option<Connection> {
+ None
+ }
+}
+
+/// A rendering context that uses the Surfman library to create and manage
+/// the OpenGL context and surface. This struct provides the default implementation
+/// of the `RenderingContext` trait, handling the creation, management, and destruction
+/// of the rendering context and its associated resources.
+///
+/// The `SurfmanRenderingContext` struct encapsulates the necessary data and methods
+/// to interact with the Surfman library, including creating surfaces, binding surfaces,
+/// resizing surfaces, presenting rendered frames, and managing the OpenGL context state.
+struct SurfmanRenderingContext {
+ gleam_gl: Rc<dyn Gl>,
+ glow_gl: Arc<glow::Context>,
+ device: RefCell<Device>,
+ context: RefCell<Context>,
+}
+
+impl Drop for SurfmanRenderingContext {
+ fn drop(&mut self) {
+ let device = &mut self.device.borrow_mut();
+ let context = &mut self.context.borrow_mut();
+ let _ = device.destroy_context(context);
+ }
+}
+
+impl SurfmanRenderingContext {
+ fn new(connection: &Connection, adapter: &Adapter) -> Result<Self, Error> {
+ let mut device = connection.create_device(adapter)?;
+
+ let flags = ContextAttributeFlags::ALPHA |
+ ContextAttributeFlags::DEPTH |
+ ContextAttributeFlags::STENCIL;
+ let gl_api = connection.gl_api();
+ let version = match &gl_api {
+ GLApi::GLES => surfman::GLVersion { major: 3, minor: 0 },
+ GLApi::GL => surfman::GLVersion { major: 3, minor: 2 },
+ };
+ let context_descriptor =
+ device.create_context_descriptor(&ContextAttributes { flags, version })?;
+ let context = device.create_context(&context_descriptor, None)?;
+
+ #[allow(unsafe_code)]
+ let gleam_gl = {
+ match gl_api {
+ GLApi::GL => unsafe {
+ gl::GlFns::load_with(|func_name| device.get_proc_address(&context, func_name))
+ },
+ GLApi::GLES => unsafe {
+ gl::GlesFns::load_with(|func_name| device.get_proc_address(&context, func_name))
+ },
+ }
+ };
+
+ #[allow(unsafe_code)]
+ let glow_gl = unsafe {
+ glow::Context::from_loader_function(|function_name| {
+ device.get_proc_address(&context, function_name)
+ })
+ };
+
+ Ok(SurfmanRenderingContext {
+ gleam_gl,
+ glow_gl: Arc::new(glow_gl),
+ device: RefCell::new(device),
+ context: RefCell::new(context),
+ })
+ }
+
+ fn create_surface(&self, surface_type: SurfaceType<NativeWidget>) -> Result<Surface, Error> {
+ let device = &mut self.device.borrow_mut();
+ let context = &self.context.borrow();
+ device.create_surface(context, SurfaceAccess::GPUOnly, surface_type)
+ }
+
+ fn bind_surface(&self, surface: Surface) -> Result<(), Error> {
+ let device = &self.device.borrow();
+ let context = &mut self.context.borrow_mut();
+ device
+ .bind_surface_to_context(context, surface)
+ .map_err(|(err, mut surface)| {
+ let _ = device.destroy_surface(context, &mut surface);
+ err
+ })?;
+ Ok(())
+ }
+
+ fn create_attached_swap_chain(&self) -> Result<SwapChain<Device>, Error> {
+ let device = &mut self.device.borrow_mut();
+ let context = &mut self.context.borrow_mut();
+ SwapChain::create_attached(device, context, SurfaceAccess::GPUOnly)
+ }
+
+ fn resize_surface(&self, size: PhysicalSize<u32>) -> Result<(), Error> {
+ let size = Size2D::new(size.width as i32, size.height as i32);
+ let device = &mut self.device.borrow_mut();
+ let context = &mut self.context.borrow_mut();
+
+ let mut surface = device.unbind_surface_from_context(context)?.unwrap();
+ device.resize_surface(context, &mut surface, size)?;
+ device
+ .bind_surface_to_context(context, surface)
+ .map_err(|(err, mut surface)| {
+ let _ = device.destroy_surface(context, &mut surface);
+ err
+ })
+ }
+
+ fn present_bound_surface(&self) -> Result<(), Error> {
+ let device = &self.device.borrow();
+ let context = &mut self.context.borrow_mut();
+
+ let mut surface = device.unbind_surface_from_context(context)?.unwrap();
+ device.present_surface(context, &mut surface)?;
+ device
+ .bind_surface_to_context(context, surface)
+ .map_err(|(err, mut surface)| {
+ let _ = device.destroy_surface(context, &mut surface);
+ err
+ })
+ }
+
+ #[allow(dead_code)]
+ fn native_context(&self) -> NativeContext {
+ let device = &self.device.borrow();
+ let context = &self.context.borrow();
+ device.native_context(context)
+ }
+
+ fn framebuffer(&self) -> Option<NativeFramebuffer> {
+ let device = &self.device.borrow();
+ let context = &self.context.borrow();
+ device
+ .context_surface_info(context)
+ .unwrap_or(None)
+ .and_then(|info| info.framebuffer_object)
+ }
+
+ fn prepare_for_rendering(&self) {
+ let framebuffer_id = self
+ .framebuffer()
+ .map_or(0, |framebuffer| framebuffer.0.into());
+ self.gleam_gl
+ .bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_id);
+ }
+
+ fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> {
+ let framebuffer_id = self
+ .framebuffer()
+ .map_or(0, |framebuffer| framebuffer.0.into());
+ Framebuffer::read_framebuffer_to_image(&self.gleam_gl, framebuffer_id, source_rectangle)
+ }
+
+ fn make_current(&self) -> Result<(), Error> {
+ let device = &self.device.borrow();
+ let context = &mut self.context.borrow();
+ device.make_context_current(context)
+ }
+
+ fn create_texture(
+ &self,
+ surface: Surface,
+ ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> {
+ let device = &self.device.borrow();
+ let context = &mut self.context.borrow_mut();
+ let SurfaceInfo {
+ id: front_buffer_id,
+ size,
+ ..
+ } = device.surface_info(&surface);
+ debug!("... getting texture for surface {:?}", front_buffer_id);
+ let surface_texture = device.create_surface_texture(context, surface).unwrap();
+ let gl_texture = device
+ .surface_texture_object(&surface_texture)
+ .map(|tex| tex.0.get())
+ .unwrap_or(0);
+ Some((surface_texture, gl_texture, size))
+ }
+
+ fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> {
+ let device = &self.device.borrow();
+ let context = &mut self.context.borrow_mut();
+ device
+ .destroy_surface_texture(context, surface_texture)
+ .map_err(|(error, _)| error)
+ .ok()
+ }
+
+ fn connection(&self) -> Option<Connection> {
+ Some(self.device.borrow().connection())
+ }
+}
+
+/// A software rendering context that uses a software OpenGL implementation to render
+/// Servo. This will generally have bad performance, but can be used in situations where
+/// it is more convenient to have consistent, but slower display output.
+///
+/// The results of the render can be accessed via [`RenderingContext::read_to_image`].
+pub struct SoftwareRenderingContext {
+ size: Cell<PhysicalSize<u32>>,
+ surfman_rendering_info: SurfmanRenderingContext,
+ swap_chain: SwapChain<Device>,
+}
+
+impl SoftwareRenderingContext {
+ pub fn new(size: PhysicalSize<u32>) -> Result<Self, Error> {
+ let connection = Connection::new()?;
+ let adapter = connection.create_software_adapter()?;
+ let surfman_rendering_info = SurfmanRenderingContext::new(&connection, &adapter)?;
+
+ let surfman_size = Size2D::new(size.width as i32, size.height as i32);
+ let surface =
+ surfman_rendering_info.create_surface(SurfaceType::Generic { size: surfman_size })?;
+ surfman_rendering_info.bind_surface(surface)?;
+ surfman_rendering_info.make_current()?;
+
+ let swap_chain = surfman_rendering_info.create_attached_swap_chain()?;
+ Ok(SoftwareRenderingContext {
+ size: Cell::new(size),
+ surfman_rendering_info,
+ swap_chain,
+ })
+ }
+}
+
+impl Drop for SoftwareRenderingContext {
+ fn drop(&mut self) {
+ let device = &mut self.surfman_rendering_info.device.borrow_mut();
+ let context = &mut self.surfman_rendering_info.context.borrow_mut();
+ let _ = self.swap_chain.destroy(device, context);
+ }
+}
+
+impl RenderingContext for SoftwareRenderingContext {
+ fn prepare_for_rendering(&self) {
+ self.surfman_rendering_info.prepare_for_rendering();
+ }
+
+ fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> {
+ self.surfman_rendering_info.read_to_image(source_rectangle)
+ }
+
+ fn size(&self) -> PhysicalSize<u32> {
+ self.size.get()
+ }
+
+ fn resize(&self, size: PhysicalSize<u32>) {
+ if self.size.get() == size {
+ return;
+ }
+
+ self.size.set(size);
+
+ let device = &mut self.surfman_rendering_info.device.borrow_mut();
+ let context = &mut self.surfman_rendering_info.context.borrow_mut();
+ let size = Size2D::new(size.width as i32, size.height as i32);
+ let _ = self.swap_chain.resize(device, context, size);
+ }
+
+ fn present(&self) {
+ let device = &mut self.surfman_rendering_info.device.borrow_mut();
+ let context = &mut self.surfman_rendering_info.context.borrow_mut();
+ let _ = self
+ .swap_chain
+ .swap_buffers(device, context, PreserveBuffer::No);
+ }
+
+ fn make_current(&self) -> Result<(), Error> {
+ self.surfman_rendering_info.make_current()
+ }
+
+ fn gleam_gl_api(&self) -> Rc<dyn gleam::gl::Gl> {
+ self.surfman_rendering_info.gleam_gl.clone()
+ }
+
+ fn glow_gl_api(&self) -> Arc<glow::Context> {
+ self.surfman_rendering_info.glow_gl.clone()
+ }
+
+ fn create_texture(
+ &self,
+ surface: Surface,
+ ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> {
+ self.surfman_rendering_info.create_texture(surface)
+ }
+
+ fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> {
+ self.surfman_rendering_info.destroy_texture(surface_texture)
+ }
+
+ fn connection(&self) -> Option<Connection> {
+ self.surfman_rendering_info.connection()
+ }
+}
+
+/// A [`RenderingContext`] that uses the `surfman` library to render to a
+/// `raw-window-handle` identified window. `surfman` will attempt to create an
+/// OpenGL context and surface for this window. This is a simple implementation
+/// of the [`RenderingContext`] crate, but by default it paints to the entire window
+/// surface.
+///
+/// If you would like to paint to only a portion of the window, consider using
+/// [`OffscreenRenderingContext`] by calling [`WindowRenderingContext::offscreen_context`].
+pub struct WindowRenderingContext {
+ size: Cell<PhysicalSize<u32>>,
+ surfman_context: SurfmanRenderingContext,
+}
+
+impl WindowRenderingContext {
+ pub fn new(
+ display_handle: DisplayHandle,
+ window_handle: WindowHandle,
+ size: PhysicalSize<u32>,
+ ) -> Result<Self, Error> {
+ let connection = Connection::from_display_handle(display_handle)?;
+ let adapter = connection.create_adapter()?;
+ let surfman_context = SurfmanRenderingContext::new(&connection, &adapter)?;
+
+ let native_widget = connection
+ .create_native_widget_from_window_handle(
+ window_handle,
+ Size2D::new(size.width as i32, size.height as i32),
+ )
+ .expect("Failed to create native widget");
+
+ let surface = surfman_context.create_surface(SurfaceType::Widget { native_widget })?;
+ surfman_context.bind_surface(surface)?;
+ surfman_context.make_current()?;
+
+ Ok(Self {
+ size: Cell::new(size),
+ surfman_context,
+ })
+ }
+
+ pub fn offscreen_context(
+ self: &Rc<Self>,
+ size: PhysicalSize<u32>,
+ ) -> OffscreenRenderingContext {
+ OffscreenRenderingContext::new(self.clone(), size)
+ }
+
+ /// Stop rendering to the window that was used to create this `WindowRenderingContext`
+ /// or last set with [`Self::set_window`].
+ ///
+ /// TODO: This should be removed once `WebView`s can replace their `RenderingContext`s.
+ pub fn take_window(&self) -> Result<(), Error> {
+ let device = self.surfman_context.device.borrow_mut();
+ let mut context = self.surfman_context.context.borrow_mut();
+ let mut surface = device.unbind_surface_from_context(&mut context)?.unwrap();
+ device.destroy_surface(&mut context, &mut surface)?;
+ Ok(())
+ }
+
+ /// Replace the window that this [`WindowRenderingContext`] renders to and give it a new
+ /// size.
+ ///
+ /// TODO: This should be removed once `WebView`s can replace their `RenderingContext`s.
+ pub fn set_window(
+ &self,
+ window_handle: WindowHandle,
+ size: PhysicalSize<u32>,
+ ) -> Result<(), Error> {
+ let mut device = self.surfman_context.device.borrow_mut();
+ let mut context = self.surfman_context.context.borrow_mut();
+
+ let native_widget = device
+ .connection()
+ .create_native_widget_from_window_handle(
+ window_handle,
+ Size2D::new(size.width as i32, size.height as i32),
+ )
+ .expect("Failed to create native widget");
+
+ let surface_access = SurfaceAccess::GPUOnly;
+ let surface_type = SurfaceType::Widget { native_widget };
+ let surface = device.create_surface(&context, surface_access, surface_type)?;
+
+ device
+ .bind_surface_to_context(&mut context, surface)
+ .map_err(|(err, mut surface)| {
+ let _ = device.destroy_surface(&mut context, &mut surface);
+ err
+ })?;
+ device.make_context_current(&context)?;
+ Ok(())
+ }
+
+ pub fn surfman_details(&self) -> (RefMut<Device>, RefMut<Context>) {
+ (
+ self.surfman_context.device.borrow_mut(),
+ self.surfman_context.context.borrow_mut(),
+ )
+ }
+}
+
+impl RenderingContext for WindowRenderingContext {
+ fn prepare_for_rendering(&self) {
+ self.surfman_context.prepare_for_rendering();
+ }
+
+ fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> {
+ self.surfman_context.read_to_image(source_rectangle)
+ }
+
+ fn size(&self) -> PhysicalSize<u32> {
+ self.size.get()
+ }
+
+ fn resize(&self, size: PhysicalSize<u32>) {
+ match self.surfman_context.resize_surface(size) {
+ Ok(..) => self.size.set(size),
+ Err(error) => warn!("Error resizing surface: {error:?}"),
+ }
+ }
+
+ fn present(&self) {
+ if let Err(error) = self.surfman_context.present_bound_surface() {
+ warn!("Error presenting surface: {error:?}");
+ }
+ }
+
+ fn make_current(&self) -> Result<(), Error> {
+ self.surfman_context.make_current()
+ }
+
+ fn gleam_gl_api(&self) -> Rc<dyn gleam::gl::Gl> {
+ self.surfman_context.gleam_gl.clone()
+ }
+
+ fn glow_gl_api(&self) -> Arc<glow::Context> {
+ self.surfman_context.glow_gl.clone()
+ }
+
+ fn create_texture(
+ &self,
+ surface: Surface,
+ ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> {
+ self.surfman_context.create_texture(surface)
+ }
+
+ fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> {
+ self.surfman_context.destroy_texture(surface_texture)
+ }
+
+ fn connection(&self) -> Option<Connection> {
+ self.surfman_context.connection()
+ }
+}
+
+struct Framebuffer {
+ gl: Rc<dyn Gl>,
+ framebuffer_id: gl::GLuint,
+ renderbuffer_id: gl::GLuint,
+ texture_id: gl::GLuint,
+}
+
+impl Framebuffer {
+ fn bind(&self) {
+ trace!("Binding FBO {}", self.framebuffer_id);
+ self.gl
+ .bind_framebuffer(gl::FRAMEBUFFER, self.framebuffer_id)
+ }
+}
+
+impl Drop for Framebuffer {
+ fn drop(&mut self) {
+ self.gl.bind_framebuffer(gl::FRAMEBUFFER, 0);
+ self.gl.delete_textures(&[self.texture_id]);
+ self.gl.delete_renderbuffers(&[self.renderbuffer_id]);
+ self.gl.delete_framebuffers(&[self.framebuffer_id]);
+ }
+}
+
+impl Framebuffer {
+ fn new(gl: Rc<dyn Gl>, size: PhysicalSize<u32>) -> Self {
+ let framebuffer_ids = gl.gen_framebuffers(1);
+ gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_ids[0]);
+
+ let texture_ids = gl.gen_textures(1);
+ gl.bind_texture(gl::TEXTURE_2D, texture_ids[0]);
+ gl.tex_image_2d(
+ gl::TEXTURE_2D,
+ 0,
+ gl::RGBA as gl::GLint,
+ size.width as gl::GLsizei,
+ size.height as gl::GLsizei,
+ 0,
+ gl::RGBA,
+ gl::UNSIGNED_BYTE,
+ None,
+ );
+ gl.tex_parameter_i(
+ gl::TEXTURE_2D,
+ gl::TEXTURE_MAG_FILTER,
+ gl::NEAREST as gl::GLint,
+ );
+ gl.tex_parameter_i(
+ gl::TEXTURE_2D,
+ gl::TEXTURE_MIN_FILTER,
+ gl::NEAREST as gl::GLint,
+ );
+
+ gl.framebuffer_texture_2d(
+ gl::FRAMEBUFFER,
+ gl::COLOR_ATTACHMENT0,
+ gl::TEXTURE_2D,
+ texture_ids[0],
+ 0,
+ );
+
+ gl.bind_texture(gl::TEXTURE_2D, 0);
+
+ let renderbuffer_ids = gl.gen_renderbuffers(1);
+ let depth_rb = renderbuffer_ids[0];
+ gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb);
+ gl.renderbuffer_storage(
+ gl::RENDERBUFFER,
+ gl::DEPTH_COMPONENT24,
+ size.width as gl::GLsizei,
+ size.height as gl::GLsizei,
+ );
+ gl.framebuffer_renderbuffer(
+ gl::FRAMEBUFFER,
+ gl::DEPTH_ATTACHMENT,
+ gl::RENDERBUFFER,
+ depth_rb,
+ );
+
+ Self {
+ gl,
+ framebuffer_id: *framebuffer_ids
+ .first()
+ .expect("Guaranteed by GL operations"),
+ renderbuffer_id: *renderbuffer_ids
+ .first()
+ .expect("Guaranteed by GL operations"),
+ texture_id: *texture_ids.first().expect("Guaranteed by GL operations"),
+ }
+ }
+
+ fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> {
+ Self::read_framebuffer_to_image(&self.gl, self.framebuffer_id, source_rectangle)
+ }
+
+ fn read_framebuffer_to_image(
+ gl: &Rc<dyn Gl>,
+ framebuffer_id: u32,
+ source_rectangle: DeviceIntRect,
+ ) -> Option<RgbaImage> {
+ gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_id);
+
+ // For some reason, OSMesa fails to render on the 3rd
+ // attempt in headless mode, under some conditions.
+ // I think this can only be some kind of synchronization
+ // bug in OSMesa, but explicitly un-binding any vertex
+ // array here seems to work around that bug.
+ // See https://github.com/servo/servo/issues/18606.
+ gl.bind_vertex_array(0);
+
+ let mut pixels = gl.read_pixels(
+ source_rectangle.min.x,
+ source_rectangle.min.y,
+ source_rectangle.width(),
+ source_rectangle.height(),
+ gl::RGBA,
+ gl::UNSIGNED_BYTE,
+ );
+ let gl_error = gl.get_error();
+ if gl_error != gl::NO_ERROR {
+ warn!("GL error code 0x{gl_error:x} set after read_pixels");
+ }
+
+ // flip image vertically (texture is upside down)
+ let source_rectangle = source_rectangle.to_usize();
+ let orig_pixels = pixels.clone();
+ let stride = source_rectangle.width() * 4;
+ for y in 0..source_rectangle.height() {
+ let dst_start = y * stride;
+ let src_start = (source_rectangle.height() - y - 1) * stride;
+ let src_slice = &orig_pixels[src_start..src_start + stride];
+ pixels[dst_start..dst_start + stride].clone_from_slice(&src_slice[..stride]);
+ }
+
+ RgbaImage::from_raw(
+ source_rectangle.width() as u32,
+ source_rectangle.height() as u32,
+ pixels,
+ )
+ }
+}
+
+pub struct OffscreenRenderingContext {
+ parent_context: Rc<WindowRenderingContext>,
+ size: Cell<PhysicalSize<u32>>,
+ framebuffer: RefCell<Framebuffer>,
+}
+
+type RenderToParentCallback = Box<dyn Fn(&glow::Context, Rect<i32>) + Send + Sync>;
+
+impl OffscreenRenderingContext {
+ fn new(parent_context: Rc<WindowRenderingContext>, size: PhysicalSize<u32>) -> Self {
+ let framebuffer = RefCell::new(Framebuffer::new(parent_context.gleam_gl_api(), size));
+ Self {
+ parent_context,
+ size: Cell::new(size),
+ framebuffer,
+ }
+ }
+
+ pub fn parent_context(&self) -> &WindowRenderingContext {
+ &self.parent_context
+ }
+
+ pub fn render_to_parent_callback(&self) -> Option<RenderToParentCallback> {
+ // Don't accept a `None` context for the source framebuffer.
+ let front_framebuffer_id =
+ NonZeroU32::new(self.framebuffer.borrow().framebuffer_id).map(NativeFramebuffer)?;
+ let parent_context_framebuffer_id = self.parent_context.surfman_context.framebuffer();
+ let size = self.size.get();
+ let size = Size2D::new(size.width as i32, size.height as i32);
+ Some(Box::new(move |gl, target_rect| {
+ Self::blit_framebuffer(
+ gl,
+ Rect::new(Point2D::origin(), size.to_i32()),
+ front_framebuffer_id,
+ target_rect,
+ parent_context_framebuffer_id,
+ );
+ }))
+ }
+
+ #[allow(unsafe_code)]
+ fn blit_framebuffer(
+ gl: &glow::Context,
+ source_rect: Rect<i32>,
+ source_framebuffer_id: NativeFramebuffer,
+ target_rect: Rect<i32>,
+ target_framebuffer_id: Option<NativeFramebuffer>,
+ ) {
+ use glow::HasContext as _;
+ unsafe {
+ gl.clear_color(0.0, 0.0, 0.0, 0.0);
+ gl.scissor(
+ target_rect.origin.x,
+ target_rect.origin.y,
+ target_rect.width(),
+ target_rect.height(),
+ );
+ gl.enable(gl::SCISSOR_TEST);
+ gl.clear(gl::COLOR_BUFFER_BIT);
+ gl.disable(gl::SCISSOR_TEST);
+
+ gl.bind_framebuffer(gl::READ_FRAMEBUFFER, Some(source_framebuffer_id));
+ gl.bind_framebuffer(gl::DRAW_FRAMEBUFFER, target_framebuffer_id);
+
+ gl.blit_framebuffer(
+ source_rect.origin.x,
+ source_rect.origin.y,
+ source_rect.origin.x + source_rect.width(),
+ source_rect.origin.y + source_rect.height(),
+ target_rect.origin.x,
+ target_rect.origin.y,
+ target_rect.origin.x + target_rect.width(),
+ target_rect.origin.y + target_rect.height(),
+ gl::COLOR_BUFFER_BIT,
+ gl::NEAREST,
+ );
+ gl.bind_framebuffer(gl::FRAMEBUFFER, target_framebuffer_id);
+ }
+ }
+}
+
+impl RenderingContext for OffscreenRenderingContext {
+ fn size(&self) -> PhysicalSize<u32> {
+ self.size.get()
+ }
+
+ fn resize(&self, new_size: PhysicalSize<u32>) {
+ let old_size = self.size.get();
+ if old_size == new_size {
+ return;
+ }
+
+ let gl = self.parent_context.gleam_gl_api();
+ let new_framebuffer = Framebuffer::new(gl.clone(), new_size);
+
+ let old_framebuffer =
+ std::mem::replace(&mut *self.framebuffer.borrow_mut(), new_framebuffer);
+ self.size.set(new_size);
+
+ let blit_size = new_size.min(old_size);
+ let rect = Rect::new(
+ Point2D::origin(),
+ Size2D::new(blit_size.width, blit_size.height),
+ )
+ .to_i32();
+
+ let Some(old_framebuffer_id) =
+ NonZeroU32::new(old_framebuffer.framebuffer_id).map(NativeFramebuffer)
+ else {
+ return;
+ };
+ let new_framebuffer_id =
+ NonZeroU32::new(self.framebuffer.borrow().framebuffer_id).map(NativeFramebuffer);
+ Self::blit_framebuffer(
+ &self.glow_gl_api(),
+ rect,
+ old_framebuffer_id,
+ rect,
+ new_framebuffer_id,
+ );
+ }
+
+ fn prepare_for_rendering(&self) {
+ self.framebuffer.borrow().bind();
+ }
+
+ fn present(&self) {}
+
+ fn make_current(&self) -> Result<(), surfman::Error> {
+ self.parent_context.make_current()
+ }
+
+ fn gleam_gl_api(&self) -> Rc<dyn gleam::gl::Gl> {
+ self.parent_context.gleam_gl_api()
+ }
+
+ fn glow_gl_api(&self) -> Arc<glow::Context> {
+ self.parent_context.glow_gl_api()
+ }
+
+ fn create_texture(
+ &self,
+ surface: Surface,
+ ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> {
+ self.parent_context.create_texture(surface)
+ }
+
+ fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> {
+ self.parent_context.destroy_texture(surface_texture)
+ }
+
+ fn connection(&self) -> Option<Connection> {
+ self.parent_context.connection()
+ }
+
+ fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> {
+ self.framebuffer.borrow().read_to_image(source_rectangle)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use dpi::PhysicalSize;
+ use euclid::{Box2D, Point2D, Size2D};
+ use gleam::gl;
+ use image::Rgba;
+ use surfman::{Connection, ContextAttributeFlags, ContextAttributes, Error, GLApi, GLVersion};
+
+ use super::Framebuffer;
+
+ #[test]
+ #[allow(unsafe_code)]
+ fn test_read_pixels() -> Result<(), Error> {
+ let connection = Connection::new()?;
+ let adapter = connection.create_software_adapter()?;
+ let mut device = connection.create_device(&adapter)?;
+ let context_descriptor = device.create_context_descriptor(&ContextAttributes {
+ version: GLVersion::new(3, 0),
+ flags: ContextAttributeFlags::empty(),
+ })?;
+ let mut context = device.create_context(&context_descriptor, None)?;
+
+ let gl = match connection.gl_api() {
+ GLApi::GL => unsafe { gl::GlFns::load_with(|s| device.get_proc_address(&context, s)) },
+ GLApi::GLES => unsafe {
+ gl::GlesFns::load_with(|s| device.get_proc_address(&context, s))
+ },
+ };
+
+ device.make_context_current(&context)?;
+
+ {
+ const SIZE: u32 = 16;
+ let framebuffer = Framebuffer::new(gl, PhysicalSize::new(SIZE, SIZE));
+ framebuffer.bind();
+ framebuffer
+ .gl
+ .clear_color(12.0 / 255.0, 34.0 / 255.0, 56.0 / 255.0, 78.0 / 255.0);
+ framebuffer.gl.clear(gl::COLOR_BUFFER_BIT);
+
+ let rect = Box2D::from_origin_and_size(Point2D::zero(), Size2D::new(SIZE, SIZE));
+ let img = framebuffer
+ .read_to_image(rect.to_i32())
+ .expect("Should have been able to read back image.");
+ assert_eq!(img.width(), SIZE);
+ assert_eq!(img.height(), SIZE);
+
+ let expected_pixel: Rgba<u8> = Rgba([12, 34, 56, 78]);
+ assert!(img.pixels().all(|&p| p == expected_pixel));
+ }
+
+ device.destroy_context(&mut context)?;
+
+ Ok(())
+ }
+}
diff --git a/components/shared/compositing/tests/compositor.rs b/components/shared/compositing/tests/compositor.rs
new file mode 100644
index 00000000000..4d2ecfd99c9
--- /dev/null
+++ b/components/shared/compositing/tests/compositor.rs
@@ -0,0 +1,188 @@
+/* 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/. */
+
+use base::id::ScrollTreeNodeId;
+use compositing_traits::display_list::{
+ AxesScrollSensitivity, ScrollSensitivity, ScrollTree, ScrollableNodeInfo,
+};
+use euclid::Size2D;
+use webrender_api::units::LayoutVector2D;
+use webrender_api::{ExternalScrollId, PipelineId, ScrollLocation, SpatialId};
+
+fn add_mock_scroll_node(tree: &mut ScrollTree) -> ScrollTreeNodeId {
+ let pipeline_id = PipelineId(0, 0);
+ let num_nodes = tree.nodes.len();
+ let parent = if num_nodes > 0 {
+ Some(ScrollTreeNodeId {
+ index: num_nodes - 1,
+ spatial_id: SpatialId::new(num_nodes - 1, pipeline_id),
+ })
+ } else {
+ None
+ };
+
+ tree.add_scroll_tree_node(
+ parent.as_ref(),
+ SpatialId::new(num_nodes, pipeline_id),
+ Some(ScrollableNodeInfo {
+ external_id: ExternalScrollId(num_nodes as u64, pipeline_id),
+ scrollable_size: Size2D::new(100.0, 100.0),
+ scroll_sensitivity: AxesScrollSensitivity {
+ x: ScrollSensitivity::ScriptAndInputEvents,
+ y: ScrollSensitivity::ScriptAndInputEvents,
+ },
+ offset: LayoutVector2D::zero(),
+ }),
+ )
+}
+
+#[test]
+fn test_scroll_tree_simple_scroll() {
+ let mut scroll_tree = ScrollTree::default();
+ let pipeline_id = PipelineId(0, 0);
+ let id = add_mock_scroll_node(&mut scroll_tree);
+
+ let (scrolled_id, offset) = scroll_tree
+ .scroll_node_or_ancestor(
+ &id,
+ ScrollLocation::Delta(LayoutVector2D::new(-20.0, -40.0)),
+ )
+ .unwrap();
+ let expected_offset = LayoutVector2D::new(-20.0, -40.0);
+ assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id));
+ assert_eq!(offset, expected_offset);
+ assert_eq!(scroll_tree.get_node(&id).offset(), Some(expected_offset));
+
+ let (scrolled_id, offset) = scroll_tree
+ .scroll_node_or_ancestor(&id, ScrollLocation::Delta(LayoutVector2D::new(20.0, 40.0)))
+ .unwrap();
+ let expected_offset = LayoutVector2D::new(0.0, 0.0);
+ assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id));
+ assert_eq!(offset, expected_offset);
+ assert_eq!(scroll_tree.get_node(&id).offset(), Some(expected_offset));
+
+ // Scroll offsets must be negative.
+ let result = scroll_tree
+ .scroll_node_or_ancestor(&id, ScrollLocation::Delta(LayoutVector2D::new(20.0, 40.0)));
+ assert!(result.is_none());
+ assert_eq!(
+ scroll_tree.get_node(&id).offset(),
+ Some(LayoutVector2D::new(0.0, 0.0))
+ );
+}
+
+#[test]
+fn test_scroll_tree_simple_scroll_chaining() {
+ let mut scroll_tree = ScrollTree::default();
+
+ let pipeline_id = PipelineId(0, 0);
+ let parent_id = add_mock_scroll_node(&mut scroll_tree);
+ let unscrollable_child_id =
+ scroll_tree.add_scroll_tree_node(Some(&parent_id), SpatialId::new(1, pipeline_id), None);
+
+ let (scrolled_id, offset) = scroll_tree
+ .scroll_node_or_ancestor(
+ &unscrollable_child_id,
+ ScrollLocation::Delta(LayoutVector2D::new(-20.0, -40.0)),
+ )
+ .unwrap();
+ let expected_offset = LayoutVector2D::new(-20.0, -40.0);
+ assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id));
+ assert_eq!(offset, expected_offset);
+ assert_eq!(
+ scroll_tree.get_node(&parent_id).offset(),
+ Some(expected_offset)
+ );
+
+ let (scrolled_id, offset) = scroll_tree
+ .scroll_node_or_ancestor(
+ &unscrollable_child_id,
+ ScrollLocation::Delta(LayoutVector2D::new(-10.0, -15.0)),
+ )
+ .unwrap();
+ let expected_offset = LayoutVector2D::new(-30.0, -55.0);
+ assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id));
+ assert_eq!(offset, expected_offset);
+ assert_eq!(
+ scroll_tree.get_node(&parent_id).offset(),
+ Some(expected_offset)
+ );
+ assert_eq!(scroll_tree.get_node(&unscrollable_child_id).offset(), None);
+}
+
+#[test]
+fn test_scroll_tree_chain_when_at_extent() {
+ let mut scroll_tree = ScrollTree::default();
+
+ let pipeline_id = PipelineId(0, 0);
+ let parent_id = add_mock_scroll_node(&mut scroll_tree);
+ let child_id = add_mock_scroll_node(&mut scroll_tree);
+
+ let (scrolled_id, offset) = scroll_tree
+ .scroll_node_or_ancestor(&child_id, ScrollLocation::End)
+ .unwrap();
+
+ let expected_offset = LayoutVector2D::new(0.0, -100.0);
+ assert_eq!(scrolled_id, ExternalScrollId(1, pipeline_id));
+ assert_eq!(offset, expected_offset);
+ assert_eq!(
+ scroll_tree.get_node(&child_id).offset(),
+ Some(expected_offset)
+ );
+
+ // The parent will have scrolled because the child is already at the extent
+ // of its scroll area in the y axis.
+ let (scrolled_id, offset) = scroll_tree
+ .scroll_node_or_ancestor(
+ &child_id,
+ ScrollLocation::Delta(LayoutVector2D::new(0.0, -10.0)),
+ )
+ .unwrap();
+ let expected_offset = LayoutVector2D::new(0.0, -10.0);
+ assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id));
+ assert_eq!(offset, expected_offset);
+ assert_eq!(
+ scroll_tree.get_node(&parent_id).offset(),
+ Some(expected_offset)
+ );
+}
+
+#[test]
+fn test_scroll_tree_chain_through_overflow_hidden() {
+ let mut scroll_tree = ScrollTree::default();
+
+ // Create a tree with a scrollable leaf, but make its `scroll_sensitivity`
+ // reflect `overflow: hidden` ie not responsive to non-script scroll events.
+ let pipeline_id = PipelineId(0, 0);
+ let parent_id = add_mock_scroll_node(&mut scroll_tree);
+ let overflow_hidden_id = add_mock_scroll_node(&mut scroll_tree);
+ scroll_tree
+ .get_node_mut(&overflow_hidden_id)
+ .scroll_info
+ .as_mut()
+ .map(|info| {
+ info.scroll_sensitivity = AxesScrollSensitivity {
+ x: ScrollSensitivity::Script,
+ y: ScrollSensitivity::Script,
+ };
+ });
+
+ let (scrolled_id, offset) = scroll_tree
+ .scroll_node_or_ancestor(
+ &overflow_hidden_id,
+ ScrollLocation::Delta(LayoutVector2D::new(-20.0, -40.0)),
+ )
+ .unwrap();
+ let expected_offset = LayoutVector2D::new(-20.0, -40.0);
+ assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id));
+ assert_eq!(offset, expected_offset);
+ assert_eq!(
+ scroll_tree.get_node(&parent_id).offset(),
+ Some(expected_offset)
+ );
+ assert_eq!(
+ scroll_tree.get_node(&overflow_hidden_id).offset(),
+ Some(LayoutVector2D::new(0.0, 0.0))
+ );
+}