aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock2
-rw-r--r--components/compositing/Cargo.toml1
-rw-r--r--components/compositing/compositor.rs89
-rw-r--r--components/layout/display_list/webrender_helpers.rs115
-rw-r--r--components/layout_2020/display_list/mod.rs26
-rw-r--r--components/layout_2020/display_list/stacking_context.rs93
-rw-r--r--components/layout_thread/lib.rs12
-rw-r--r--components/script_traits/Cargo.toml3
-rw-r--r--components/script_traits/compositor.rs252
-rw-r--r--components/script_traits/lib.rs4
-rw-r--r--components/script_traits/tests/compositor.rs181
-rw-r--r--python/servo/testing_commands.py1
12 files changed, 677 insertions, 102 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 450263d7a8e..482eac66071 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -854,6 +854,7 @@ dependencies = [
"crossbeam-channel 0.4.4",
"embedder_traits",
"euclid",
+ "fnv",
"gfx_traits",
"gleam",
"image 0.24.6",
@@ -5231,6 +5232,7 @@ dependencies = [
"servo_atoms",
"servo_url",
"smallvec",
+ "std_test_override",
"style_traits",
"time 0.1.45",
"uuid",
diff --git a/components/compositing/Cargo.toml b/components/compositing/Cargo.toml
index d67623f9ffc..92b86235471 100644
--- a/components/compositing/Cargo.toml
+++ b/components/compositing/Cargo.toml
@@ -20,6 +20,7 @@ canvas = { path = "../canvas" }
crossbeam-channel = { workspace = true }
embedder_traits = { path = "../embedder_traits" }
euclid = { workspace = true }
+fnv = { workspace = true }
gfx_traits = { path = "../gfx_traits" }
gleam = { workspace = true, optional = true }
image = { workspace = true }
diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs
index b453547365f..b1ac1dfd3c9 100644
--- a/components/compositing/compositor.rs
+++ b/components/compositing/compositor.rs
@@ -17,6 +17,7 @@ use canvas::canvas_paint_thread::ImageUpdate;
use crossbeam_channel::Sender;
use embedder_traits::Cursor;
use euclid::{Point2D, Rect, Scale, Vector2D};
+use fnv::{FnvHashMap, FnvHashSet};
use gfx_traits::{Epoch, FontData};
#[cfg(feature = "gl")]
use image::{DynamicImage, ImageFormat};
@@ -31,7 +32,7 @@ use net_traits::image_cache::CorsStatus;
#[cfg(feature = "gl")]
use pixels::PixelFormat;
use profile_traits::time::{self as profile_time, profile, ProfilerCategory};
-use script_traits::compositor::HitTestInfo;
+use script_traits::compositor::{HitTestInfo, ScrollTree};
use script_traits::CompositorEvent::{MouseButtonEvent, MouseMoveEvent, TouchEvent, WheelEvent};
use script_traits::{
AnimationState, AnimationTickType, CompositorHitTestResult, LayoutControlMsg, MouseButton,
@@ -49,9 +50,9 @@ use style_traits::viewport::ViewportConstraints;
use style_traits::{CSSPixel, DevicePixel, PinchZoomFactor};
use time::{now, precise_time_ns, precise_time_s};
use webrender_api::units::{
- DeviceIntPoint, DeviceIntSize, DevicePoint, LayoutVector2D, WorldPoint,
+ DeviceIntPoint, DeviceIntSize, DevicePoint, LayoutPoint, LayoutVector2D, WorldPoint,
};
-use webrender_api::{self, HitTestFlags, ScrollLocation};
+use webrender_api::{self, ExternalScrollId, HitTestFlags, ScrollClamping, ScrollLocation};
use webrender_surfman::WebrenderSurfman;
#[derive(Debug, PartialEq)]
@@ -269,6 +270,10 @@ struct PipelineDetails {
/// Hit test items for this pipeline. This is used to map WebRender hit test
/// information to the full information necessary for Servo.
hit_test_items: Vec<HitTestInfo>,
+
+ /// The compositor-side [ScrollTree]. This is used to allow finding and scrolling
+ /// nodes in the compositor before forwarding new offsets to WebRender.
+ scroll_tree: ScrollTree,
}
impl PipelineDetails {
@@ -279,6 +284,30 @@ impl PipelineDetails {
animation_callbacks_running: false,
visible: true,
hit_test_items: Vec::new(),
+ scroll_tree: ScrollTree::default(),
+ }
+ }
+
+ fn install_new_scroll_tree(&mut self, new_scroll_tree: ScrollTree) {
+ let old_scroll_offsets: FnvHashMap<ExternalScrollId, LayoutVector2D> = self
+ .scroll_tree
+ .nodes
+ .drain(..)
+ .filter_map(|node| match (node.external_id(), node.offset()) {
+ (Some(external_id), Some(offset)) => Some((external_id, offset)),
+ _ => None,
+ })
+ .collect();
+
+ self.scroll_tree = new_scroll_tree;
+ for node in self.scroll_tree.nodes.iter_mut() {
+ match node.external_id() {
+ Some(external_id) => match old_scroll_offsets.get(&external_id) {
+ Some(new_offset) => node.set_offset(*new_offset),
+ None => continue,
+ },
+ _ => continue,
+ };
}
}
}
@@ -647,6 +676,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
let details = self.pipeline_details(PipelineId::from_webrender(pipeline));
details.hit_test_items = compositor_display_list_info.hit_test_info;
+ details.install_new_scroll_tree(compositor_display_list_info.scroll_tree);
let mut txn = webrender_api::Transaction::new();
txn.set_display_list(
@@ -850,10 +880,38 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
.send_transaction(self.webrender_document, txn);
self.create_pipeline_details_for_frame_tree(&frame_tree);
+ self.reset_scroll_tree_for_unattached_pipelines(&frame_tree);
self.frame_tree_id.next();
}
+ fn reset_scroll_tree_for_unattached_pipelines(&mut self, frame_tree: &SendableFrameTree) {
+ // TODO(mrobinson): Eventually this can selectively preserve the scroll trees
+ // state for some unattached pipelines in order to preserve scroll position when
+ // navigating backward and forward.
+ fn collect_pipelines(
+ pipelines: &mut FnvHashSet<PipelineId>,
+ frame_tree: &SendableFrameTree,
+ ) {
+ pipelines.insert(frame_tree.pipeline.id);
+ for kid in &frame_tree.children {
+ collect_pipelines(pipelines, kid);
+ }
+ }
+
+ let mut attached_pipelines: FnvHashSet<PipelineId> = FnvHashSet::default();
+ collect_pipelines(&mut attached_pipelines, frame_tree);
+
+ self.pipeline_details
+ .iter_mut()
+ .filter(|(id, _)| !attached_pipelines.contains(id))
+ .for_each(|(_, details)| {
+ details.scroll_tree.nodes.iter_mut().for_each(|node| {
+ node.set_offset(LayoutVector2D::zero());
+ })
+ })
+ }
+
fn create_pipeline_details_for_frame_tree(&mut self, frame_tree: &SendableFrameTree) {
self.pipeline_details(frame_tree.pipeline.id).pipeline = Some(frame_tree.pipeline.clone());
@@ -1005,6 +1063,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
point_relative_to_item: item.point_relative_to_item.to_untyped(),
node: UntrustedNodeAddress(info.node as *const c_void),
cursor: info.cursor,
+ scroll_tree_node: info.scroll_tree_node,
})
})
.collect()
@@ -1220,7 +1279,29 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
let cursor = (combined_event.cursor.to_f32() / self.scale).to_untyped();
let cursor = WorldPoint::from_untyped(cursor);
let mut txn = webrender_api::Transaction::new();
- txn.scroll(scroll_location, cursor);
+
+ let result = match self.hit_test_at_point(cursor) {
+ Some(result) => result,
+ None => return,
+ };
+
+ if let Some(details) = self.pipeline_details.get_mut(&result.pipeline_id) {
+ match details
+ .scroll_tree
+ .scroll_node_or_ancestor(&result.scroll_tree_node, scroll_location)
+ {
+ Some((external_id, offset)) => {
+ let scroll_origin = LayoutPoint::new(-offset.x, -offset.y);
+ txn.scroll_node_with_id(
+ scroll_origin,
+ external_id,
+ ScrollClamping::NoClamping,
+ );
+ },
+ None => {},
+ }
+ }
+
if combined_event.magnification != 1.0 {
let old_zoom = self.pinch_zoom_level();
self.set_pinch_zoom_level(old_zoom * combined_event.magnification);
diff --git a/components/layout/display_list/webrender_helpers.rs b/components/layout/display_list/webrender_helpers.rs
index c32f4ba87eb..a34905be136 100644
--- a/components/layout/display_list/webrender_helpers.rs
+++ b/components/layout/display_list/webrender_helpers.rs
@@ -10,8 +10,8 @@
use crate::display_list::items::{BaseDisplayItem, ClipScrollNode, ClipScrollNodeType, ClipType};
use crate::display_list::items::{DisplayItem, DisplayList, StackingContextType};
use msg::constellation_msg::PipelineId;
-use script_traits::compositor::CompositorDisplayListInfo;
-use webrender_api::units::{LayoutPoint, LayoutVector2D};
+use script_traits::compositor::{CompositorDisplayListInfo, ScrollTreeNodeId, ScrollableNodeInfo};
+use webrender_api::units::{LayoutPoint, LayoutSize, LayoutVector2D};
use webrender_api::{
self, ClipId, CommonItemProperties, DisplayItem as WrDisplayItem, DisplayListBuilder,
PrimitiveFlags, PropertyBinding, PushStackingContextDisplayItem, RasterSpace,
@@ -20,23 +20,25 @@ use webrender_api::{
struct ClipScrollState {
clip_ids: Vec<Option<ClipId>>,
- spatial_ids: Vec<Option<SpatialId>>,
- active_clip_id: ClipId,
- active_spatial_id: SpatialId,
+ scroll_node_ids: Vec<Option<ScrollTreeNodeId>>,
compositor_info: CompositorDisplayListInfo,
}
impl ClipScrollState {
- fn new(size: usize, pipeline_id: webrender_api::PipelineId) -> Self {
- let root_clip_id = ClipId::root(pipeline_id);
- let root_scroll_node_id = SpatialId::root_scroll_node(pipeline_id);
- let root_reference_frame_id = SpatialId::root_reference_frame(pipeline_id);
+ fn new(
+ size: usize,
+ content_size: LayoutSize,
+ viewport_size: LayoutSize,
+ pipeline_id: webrender_api::PipelineId,
+ ) -> Self {
let mut state = ClipScrollState {
clip_ids: vec![None; size],
- spatial_ids: vec![None; size],
- active_clip_id: root_clip_id,
- active_spatial_id: root_scroll_node_id,
- compositor_info: CompositorDisplayListInfo::default(),
+ scroll_node_ids: vec![None; size],
+ compositor_info: CompositorDisplayListInfo::new(
+ viewport_size,
+ content_size,
+ pipeline_id,
+ ),
};
// We need to register the WebRender root reference frame and root scroll node ids
@@ -44,9 +46,10 @@ impl ClipScrollState {
// automatically. We also follow the "old" WebRender API for clip/scroll for now,
// hence both arrays are initialized based on FIRST_SPATIAL_NODE_INDEX, while
// FIRST_CLIP_NODE_INDEX is not taken into account.
- state.spatial_ids[0] = Some(root_reference_frame_id);
- state.spatial_ids[1] = Some(root_scroll_node_id);
+ state.scroll_node_ids[0] = Some(state.compositor_info.root_reference_frame_id);
+ state.scroll_node_ids[1] = Some(state.compositor_info.root_scroll_node_id);
+ let root_clip_id = ClipId::root(pipeline_id);
state.add_clip_node_mapping(0, root_clip_id);
state.add_clip_node_mapping(1, root_clip_id);
@@ -58,20 +61,37 @@ impl ClipScrollState {
}
fn webrender_spatial_id_for_index(&mut self, index: usize) -> SpatialId {
- self.spatial_ids[index]
+ self.scroll_node_ids[index]
.expect("Tried to use WebRender parent SpatialId before it was defined.")
+ .spatial_id
}
fn add_clip_node_mapping(&mut self, index: usize, webrender_id: ClipId) {
self.clip_ids[index] = Some(webrender_id);
}
- fn register_spatial_node(&mut self, index: usize, webrender_id: SpatialId) {
- self.spatial_ids[index] = Some(webrender_id);
+ fn scroll_node_id_from_index(&self, index: usize) -> ScrollTreeNodeId {
+ self.scroll_node_ids[index]
+ .expect("Tried to use WebRender parent SpatialId before it was defined.")
+ }
+
+ fn register_spatial_node(
+ &mut self,
+ index: usize,
+ spatial_id: SpatialId,
+ parent_index: Option<usize>,
+ scroll_info: Option<ScrollableNodeInfo>,
+ ) {
+ let parent_scroll_node_id = parent_index.map(|index| self.scroll_node_id_from_index(index));
+ self.scroll_node_ids[index] = Some(self.compositor_info.scroll_tree.add_scroll_tree_node(
+ parent_scroll_node_id.as_ref(),
+ spatial_id,
+ scroll_info,
+ ));
}
fn add_spatial_node_mapping_to_parent_index(&mut self, index: usize, parent_index: usize) {
- self.spatial_ids[index] = self.spatial_ids[parent_index];
+ self.scroll_node_ids[index] = self.scroll_node_ids[parent_index];
}
}
@@ -85,9 +105,15 @@ impl DisplayList {
pub fn convert_to_webrender(
&mut self,
pipeline_id: PipelineId,
+ viewport_size: LayoutSize,
) -> (DisplayListBuilder, CompositorDisplayListInfo, IsContentful) {
let webrender_pipeline = pipeline_id.to_webrender();
- let mut state = ClipScrollState::new(self.clip_scroll_nodes.len(), webrender_pipeline);
+ let mut state = ClipScrollState::new(
+ self.clip_scroll_nodes.len(),
+ self.bounds().size,
+ viewport_size,
+ webrender_pipeline,
+ );
let mut builder = DisplayListBuilder::with_capacity(
webrender_pipeline,
@@ -122,33 +148,29 @@ impl DisplayItem {
trace!("converting {:?}", clip_and_scroll_indices);
let current_scrolling_index = clip_and_scroll_indices.scrolling.to_index();
- let cur_spatial_id = state.webrender_spatial_id_for_index(current_scrolling_index);
- if cur_spatial_id != state.active_spatial_id {
- state.active_spatial_id = cur_spatial_id;
- }
+ let current_scroll_node_id = state.scroll_node_id_from_index(current_scrolling_index);
let internal_clip_id = clip_and_scroll_indices
.clipping
.unwrap_or(clip_and_scroll_indices.scrolling);
- let cur_clip_id = state.webrender_clip_id_for_index(internal_clip_id.to_index());
- if cur_clip_id != state.active_clip_id {
- state.active_clip_id = cur_clip_id;
- }
+ let current_clip_id = state.webrender_clip_id_for_index(internal_clip_id.to_index());
let mut build_common_item_properties = |base: &BaseDisplayItem| {
let tag = match base.metadata.cursor {
Some(cursor) => {
- let hit_test_index = state
- .compositor_info
- .add_hit_test_info(base.metadata.node.0 as u64, Some(cursor));
+ let hit_test_index = state.compositor_info.add_hit_test_info(
+ base.metadata.node.0 as u64,
+ Some(cursor),
+ current_scroll_node_id,
+ );
Some((hit_test_index as u64, 0u16))
},
None => None,
};
CommonItemProperties {
clip_rect: base.clip_rect,
- spatial_id: state.active_spatial_id,
- clip_id: state.active_clip_id,
+ spatial_id: current_scroll_node_id.spatial_id,
+ clip_id: current_clip_id,
// TODO(gw): Make use of the WR backface visibility functionality.
flags: PrimitiveFlags::default(),
hit_info: tag,
@@ -265,20 +287,25 @@ impl DisplayItem {
let new_spatial_id = builder.push_reference_frame(
stacking_context.bounds.origin,
- state.active_spatial_id,
+ current_scroll_node_id.spatial_id,
stacking_context.transform_style,
PropertyBinding::Value(transform),
ref_frame,
);
let index = frame_index.to_index();
- state.add_clip_node_mapping(index, cur_clip_id);
- state.register_spatial_node(index, new_spatial_id);
+ state.add_clip_node_mapping(index, current_clip_id);
+ state.register_spatial_node(
+ index,
+ new_spatial_id,
+ Some(current_scrolling_index),
+ None,
+ );
bounds.origin = LayoutPoint::zero();
new_spatial_id
} else {
- state.active_spatial_id
+ current_scroll_node_id.spatial_id
};
if !stacking_context.filters.is_empty() {
@@ -355,8 +382,18 @@ impl DisplayItem {
LayoutVector2D::zero(),
);
- state.register_spatial_node(index, space_clip_info.spatial_id);
state.add_clip_node_mapping(index, space_clip_info.clip_id);
+ state.register_spatial_node(
+ index,
+ space_clip_info.spatial_id,
+ Some(parent_index),
+ Some(ScrollableNodeInfo {
+ external_id,
+ scrollable_size: node.content_rect.size - item_rect.size,
+ scroll_sensitivity,
+ offset: LayoutVector2D::zero(),
+ }),
+ );
},
ClipScrollNodeType::StickyFrame(ref sticky_data) => {
// TODO: Add define_sticky_frame_with_parent to WebRender.
@@ -370,7 +407,7 @@ impl DisplayItem {
);
state.add_clip_node_mapping(index, parent_clip_id);
- state.register_spatial_node(index, id);
+ state.register_spatial_node(index, id, Some(current_scrolling_index), None);
},
ClipScrollNodeType::Placeholder => {
unreachable!("Found DefineClipScrollNode for Placeholder type node.");
diff --git a/components/layout_2020/display_list/mod.rs b/components/layout_2020/display_list/mod.rs
index bd4a237fb59..1e097730bd7 100644
--- a/components/layout_2020/display_list/mod.rs
+++ b/components/layout_2020/display_list/mod.rs
@@ -18,7 +18,7 @@ use gfx::text::glyph::GlyphStore;
use mitochondria::OnceCell;
use msg::constellation_msg::BrowsingContextId;
use net_traits::image_cache::UsePlaceholder;
-use script_traits::compositor::CompositorDisplayListInfo;
+use script_traits::compositor::{CompositorDisplayListInfo, ScrollTreeNodeId};
use std::sync::Arc;
use style::computed_values::text_decoration_style::T as ComputedTextDecorationStyle;
use style::dom::OpaqueNode;
@@ -66,27 +66,28 @@ pub struct DisplayList {
impl DisplayList {
/// Create a new [DisplayList] given the dimensions of the layout and the WebRender
/// pipeline id.
- ///
- /// TODO(mrobinson): `_viewport_size` will eventually be used in the creation
- /// of the compositor-side scroll tree.
pub fn new(
- _viewport_size: units::LayoutSize,
+ viewport_size: units::LayoutSize,
content_size: units::LayoutSize,
pipeline_id: wr::PipelineId,
) -> Self {
Self {
wr: wr::DisplayListBuilder::new(pipeline_id, content_size),
- compositor_info: CompositorDisplayListInfo::default(),
+ compositor_info: CompositorDisplayListInfo::new(
+ viewport_size,
+ content_size,
+ pipeline_id,
+ ),
}
}
}
pub(crate) struct DisplayListBuilder<'a> {
- /// The current [wr::SpatialId] for this [DisplayListBuilder]. This allows
- /// only passing the builder instead passing the containing
+ /// The current [ScrollTreeNodeId] for this [DisplayListBuilder]. This
+ /// allows only passing the builder instead passing the containing
/// [stacking_context::StackingContextFragment] as an argument to display
/// list building functions.
- current_spatial_id: wr::SpatialId,
+ current_scroll_node_id: ScrollTreeNodeId,
/// The current [wr::ClipId] for this [DisplayListBuilder]. This allows
/// only passing the builder instead passing the containing
@@ -125,7 +126,7 @@ impl DisplayList {
root_stacking_context: &StackingContext,
) -> (FnvHashMap<BrowsingContextId, Size2D<f32, CSSPixel>>, bool) {
let mut builder = DisplayListBuilder {
- current_spatial_id: wr::SpatialId::root_scroll_node(self.wr.pipeline_id),
+ current_scroll_node_id: self.compositor_info.root_scroll_node_id,
current_clip_id: wr::ClipId::root(self.wr.pipeline_id),
element_for_canvas_background: fragment_tree.canvas_background.from_element,
is_contentful: false,
@@ -153,7 +154,7 @@ impl<'a> DisplayListBuilder<'a> {
// for fragments that paint their entire border rectangle.
wr::CommonItemProperties {
clip_rect,
- spatial_id: self.current_spatial_id,
+ spatial_id: self.current_scroll_node_id.spatial_id,
clip_id: self.current_clip_id,
hit_info: None,
flags: style.get_webrender_primitive_flags(),
@@ -176,6 +177,7 @@ impl<'a> DisplayListBuilder<'a> {
let hit_test_index = self.display_list.compositor_info.add_hit_test_info(
tag?.node.0 as u64,
Some(cursor(inherited_ui.cursor.keyword, auto_cursor)),
+ self.current_scroll_node_id,
);
Some((hit_test_index as u64, 0u16))
}
@@ -866,7 +868,7 @@ fn clip_for_radii(
None
} else {
let parent_space_and_clip = wr::SpaceAndClipInfo {
- spatial_id: builder.current_spatial_id,
+ spatial_id: builder.current_scroll_node_id.spatial_id,
clip_id: builder.current_clip_id,
};
Some(builder.wr().define_clip_rounded_rect(
diff --git a/components/layout_2020/display_list/stacking_context.rs b/components/layout_2020/display_list/stacking_context.rs
index e88c08c6c1a..d4995d8cfc3 100644
--- a/components/layout_2020/display_list/stacking_context.rs
+++ b/components/layout_2020/display_list/stacking_context.rs
@@ -12,6 +12,7 @@ use crate::geom::PhysicalRect;
use crate::style_ext::ComputedValuesExt;
use crate::FragmentTree;
use euclid::default::Rect;
+use script_traits::compositor::{ScrollTreeNodeId, ScrollableNodeInfo};
use servo_arc::Arc as ServoArc;
use std::cmp::Ordering;
use std::mem;
@@ -32,7 +33,7 @@ use webrender_api::units::{LayoutPoint, LayoutRect, LayoutTransform, LayoutVecto
pub(crate) struct ContainingBlock {
/// The SpatialId of the spatial node that contains the children
/// of this containing block.
- spatial_id: wr::SpatialId,
+ scroll_node_id: ScrollTreeNodeId,
/// The WebRender ClipId to use for this children of this containing
/// block.
@@ -45,11 +46,11 @@ pub(crate) struct ContainingBlock {
impl ContainingBlock {
pub(crate) fn new(
rect: &PhysicalRect<Length>,
- spatial_id: wr::SpatialId,
+ scroll_node_id: ScrollTreeNodeId,
clip_id: wr::ClipId,
) -> Self {
ContainingBlock {
- spatial_id,
+ scroll_node_id,
clip_id,
rect: *rect,
}
@@ -77,12 +78,12 @@ impl DisplayList {
pub fn build_stacking_context_tree(&mut self, fragment_tree: &FragmentTree) -> StackingContext {
let cb_for_non_fixed_descendants = ContainingBlock::new(
&fragment_tree.initial_containing_block,
- wr::SpatialId::root_scroll_node(self.wr.pipeline_id),
+ self.compositor_info.root_scroll_node_id,
wr::ClipId::root(self.wr.pipeline_id),
);
let cb_for_fixed_descendants = ContainingBlock::new(
&fragment_tree.initial_containing_block,
- wr::SpatialId::root_reference_frame(self.wr.pipeline_id),
+ self.compositor_info.root_reference_frame_id,
wr::ClipId::root(self.wr.pipeline_id),
);
@@ -115,13 +116,23 @@ impl DisplayList {
fn push_reference_frame(
&mut self,
origin: LayoutPoint,
- parent_spatial_id: &wr::SpatialId,
+ parent_scroll_node_id: &ScrollTreeNodeId,
transform_style: wr::TransformStyle,
transform: wr::PropertyBinding<LayoutTransform>,
kind: wr::ReferenceFrameKind,
- ) -> wr::SpatialId {
- self.wr
- .push_reference_frame(origin, *parent_spatial_id, transform_style, transform, kind)
+ ) -> ScrollTreeNodeId {
+ let new_spatial_id = self.wr.push_reference_frame(
+ origin,
+ parent_scroll_node_id.spatial_id,
+ transform_style,
+ transform,
+ kind,
+ );
+ self.compositor_info.scroll_tree.add_scroll_tree_node(
+ Some(parent_scroll_node_id),
+ new_spatial_id,
+ None,
+ )
}
fn pop_reference_frame(&mut self) {
@@ -130,31 +141,41 @@ impl DisplayList {
fn define_scroll_frame(
&mut self,
- parent_spatial_id: &wr::SpatialId,
+ parent_scroll_node_id: &ScrollTreeNodeId,
parent_clip_id: &wr::ClipId,
- external_id: Option<wr::ExternalScrollId>,
+ external_id: wr::ExternalScrollId,
content_rect: LayoutRect,
clip_rect: LayoutRect,
scroll_sensitivity: wr::ScrollSensitivity,
external_scroll_offset: LayoutVector2D,
- ) -> (wr::SpatialId, wr::ClipId) {
+ ) -> (ScrollTreeNodeId, wr::ClipId) {
let new_space_and_clip = self.wr.define_scroll_frame(
&wr::SpaceAndClipInfo {
- spatial_id: *parent_spatial_id,
+ spatial_id: parent_scroll_node_id.spatial_id,
clip_id: *parent_clip_id,
},
- external_id,
+ Some(external_id),
content_rect,
clip_rect,
scroll_sensitivity,
external_scroll_offset,
);
- (new_space_and_clip.spatial_id, new_space_and_clip.clip_id)
+ let new_scroll_node_id = self.compositor_info.scroll_tree.add_scroll_tree_node(
+ Some(&parent_scroll_node_id),
+ new_space_and_clip.spatial_id,
+ Some(ScrollableNodeInfo {
+ external_id,
+ scrollable_size: content_rect.size - clip_rect.size,
+ scroll_sensitivity,
+ offset: LayoutVector2D::zero(),
+ }),
+ );
+ (new_scroll_node_id, new_space_and_clip.clip_id)
}
}
pub(crate) struct StackingContextFragment {
- spatial_id: wr::SpatialId,
+ scroll_node_id: ScrollTreeNodeId,
clip_id: wr::ClipId,
section: StackingContextSection,
containing_block: PhysicalRect<Length>,
@@ -163,7 +184,7 @@ pub(crate) struct StackingContextFragment {
impl StackingContextFragment {
fn build_display_list(&self, builder: &mut DisplayListBuilder) {
- builder.current_spatial_id = self.spatial_id;
+ builder.current_scroll_node_id = self.scroll_node_id;
builder.current_clip_id = self.clip_id;
self.fragment
.borrow()
@@ -398,7 +419,7 @@ impl StackingContext {
// The root element may have a CSS transform, and we want the canvas’
// background image to be transformed. To do so, take its `SpatialId`
// (but not its `ClipId`)
- builder.current_spatial_id = first_stacking_context_fragment.spatial_id;
+ builder.current_scroll_node_id = first_stacking_context_fragment.scroll_node_id;
// Now we need express the painting area rectangle in the local coordinate system,
// which differs from the top-level coordinate system based on…
@@ -559,7 +580,7 @@ impl Fragment {
Fragment::Text(_) | Fragment::Image(_) | Fragment::IFrame(_) => {
stacking_context.fragments.push(StackingContextFragment {
section: StackingContextSection::Content,
- spatial_id: containing_block.spatial_id,
+ scroll_node_id: containing_block.scroll_node_id,
clip_id: containing_block.clip_id,
containing_block: containing_block.rect,
fragment: fragment_ref.clone(),
@@ -650,7 +671,7 @@ impl BoxFragment {
let new_spatial_id = display_list.push_reference_frame(
reference_frame_data.origin.to_webrender(),
- &containing_block.spatial_id,
+ &containing_block.scroll_node_id,
self.style.get_box().transform_style.to_webrender(),
wr::PropertyBinding::Value(reference_frame_data.transform),
reference_frame_data.kind,
@@ -712,7 +733,7 @@ impl BoxFragment {
};
let mut child_stacking_context = StackingContext::new(
- containing_block.spatial_id,
+ containing_block.scroll_node_id.spatial_id,
self.style.clone(),
context_type,
);
@@ -749,11 +770,11 @@ impl BoxFragment {
containing_block_info: &ContainingBlockInfo,
stacking_context: &mut StackingContext,
) {
- let mut new_spatial_id = containing_block.spatial_id;
+ let mut new_scroll_node_id = containing_block.scroll_node_id;
let mut new_clip_id = containing_block.clip_id;
if let Some(clip_id) = self.build_clip_frame_if_necessary(
display_list,
- &new_spatial_id,
+ &new_scroll_node_id,
&new_clip_id,
&containing_block.rect,
) {
@@ -761,7 +782,7 @@ impl BoxFragment {
}
stacking_context.fragments.push(StackingContextFragment {
- spatial_id: new_spatial_id,
+ scroll_node_id: new_scroll_node_id,
clip_id: new_clip_id,
section: self.get_stacking_context_section(),
containing_block: containing_block.rect,
@@ -769,7 +790,7 @@ impl BoxFragment {
});
if self.style.get_outline().outline_width.px() > 0.0 {
stacking_context.fragments.push(StackingContextFragment {
- spatial_id: new_spatial_id,
+ scroll_node_id: new_scroll_node_id,
clip_id: new_clip_id,
section: StackingContextSection::Outline,
containing_block: containing_block.rect,
@@ -779,13 +800,13 @@ impl BoxFragment {
// We want to build the scroll frame after the background and border, because
// they shouldn't scroll with the rest of the box content.
- if let Some((spatial_id, clip_id)) = self.build_scroll_frame_if_necessary(
+ if let Some((scroll_node_id, clip_id)) = self.build_scroll_frame_if_necessary(
display_list,
- &new_spatial_id,
+ &new_scroll_node_id,
&new_clip_id,
&containing_block.rect,
) {
- new_spatial_id = spatial_id;
+ new_scroll_node_id = scroll_node_id;
new_clip_id = clip_id;
}
@@ -799,9 +820,9 @@ impl BoxFragment {
.translate(containing_block.rect.origin.to_vector());
let for_absolute_descendants =
- ContainingBlock::new(&padding_rect, new_spatial_id, new_clip_id);
+ ContainingBlock::new(&padding_rect, new_scroll_node_id, new_clip_id);
let for_non_absolute_descendants =
- ContainingBlock::new(&content_rect, new_spatial_id, new_clip_id);
+ ContainingBlock::new(&content_rect, new_scroll_node_id, new_clip_id);
// Create a new `ContainingBlockInfo` for descendants depending on
// whether or not this fragment establishes a containing block for
@@ -840,7 +861,7 @@ impl BoxFragment {
fn build_clip_frame_if_necessary(
&self,
display_list: &mut DisplayList,
- parent_spatial_id: &wr::SpatialId,
+ parent_scroll_node_id: &ScrollTreeNodeId,
parent_clip_id: &wr::ClipId,
containing_block_rect: &PhysicalRect<Length>,
) -> Option<wr::ClipId> {
@@ -867,7 +888,7 @@ impl BoxFragment {
Some(display_list.wr.define_clip_rect(
&wr::SpaceAndClipInfo {
- spatial_id: *parent_spatial_id,
+ spatial_id: parent_scroll_node_id.spatial_id,
clip_id: *parent_clip_id,
},
clip_rect,
@@ -877,10 +898,10 @@ impl BoxFragment {
fn build_scroll_frame_if_necessary(
&self,
display_list: &mut DisplayList,
- parent_spatial_id: &wr::SpatialId,
+ parent_scroll_node_id: &ScrollTreeNodeId,
parent_clip_id: &wr::ClipId,
containing_block_rect: &PhysicalRect<Length>,
- ) -> Option<(wr::SpatialId, wr::ClipId)> {
+ ) -> Option<(ScrollTreeNodeId, wr::ClipId)> {
let overflow_x = self.style.get_box().overflow_x;
let overflow_y = self.style.get_box().overflow_y;
if overflow_x == ComputedOverflow::Visible && overflow_y == ComputedOverflow::Visible {
@@ -908,9 +929,9 @@ impl BoxFragment {
Some(
display_list.define_scroll_frame(
- parent_spatial_id,
+ parent_scroll_node_id,
parent_clip_id,
- Some(external_id),
+ external_id,
self.scrollable_overflow(&containing_block_rect)
.to_webrender(),
padding_rect,
diff --git a/components/layout_thread/lib.rs b/components/layout_thread/lib.rs
index 1e8bd2848be..a6b57778185 100644
--- a/components/layout_thread/lib.rs
+++ b/components/layout_thread/lib.rs
@@ -1064,21 +1064,19 @@ impl LayoutThread {
debug!("Layout done!");
- // TODO: Avoid the temporary conversion and build webrender sc/dl directly!
- let (builder, compositor_info, is_contentful) =
- display_list.convert_to_webrender(self.id);
-
- let viewport_size = Size2D::new(
+ let viewport_size = webrender_api::units::LayoutSize::new(
self.viewport_size.width.to_f32_px(),
self.viewport_size.height.to_f32_px(),
);
+ // TODO: Avoid the temporary conversion and build webrender sc/dl directly!
+ let (builder, compositor_info, is_contentful) =
+ display_list.convert_to_webrender(self.id, viewport_size);
+
let mut epoch = self.epoch.get();
epoch.next();
self.epoch.set(epoch);
- let viewport_size = webrender_api::units::LayoutSize::from_untyped(viewport_size);
-
// Observe notifications about rendered frames if needed right before
// sending the display list to WebRender in order to set time related
// Progressive Web Metrics.
diff --git a/components/script_traits/Cargo.toml b/components/script_traits/Cargo.toml
index 1ce7cd160e3..2c79116a48c 100644
--- a/components/script_traits/Cargo.toml
+++ b/components/script_traits/Cargo.toml
@@ -45,3 +45,6 @@ webdriver = { workspace = true }
webgpu = { path = "../webgpu" }
webrender_api = { git = "https://github.com/servo/webrender" }
webxr-api = { git = "https://github.com/servo/webxr", features = ["ipc"] }
+
+[dev-dependencies]
+std_test_override = { path = "../std_test_override" }
diff --git a/components/script_traits/compositor.rs b/components/script_traits/compositor.rs
index 00f56cbcfa5..00255ee2efb 100644
--- a/components/script_traits/compositor.rs
+++ b/components/script_traits/compositor.rs
@@ -5,6 +5,10 @@
//! Defines data structures which are consumed by the Compositor.
use embedder_traits::Cursor;
+use webrender_api::{
+ units::{LayoutSize, LayoutVector2D},
+ ExternalScrollId, ScrollLocation, ScrollSensitivity, SpatialId,
+};
/// Information that Servo keeps alongside WebRender display items
/// in order to add more context to hit test results.
@@ -15,30 +19,270 @@ pub struct HitTestInfo {
/// 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,
+}
+
+/// An id for a ScrollTreeNode in the ScrollTree. This contains both the index
+/// to the node in the tree's array of nodes as well as the corresponding SpatialId
+/// for the SpatialNode in the WebRender display list.
+#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
+pub struct ScrollTreeNodeId {
+ /// The index of this scroll tree node in the tree's array of nodes.
+ pub index: usize,
+
+ /// The WebRender spatial id of this scroll tree node.
+ pub spatial_id: SpatialId,
+}
+
+/// 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: ScrollSensitivity,
+
+ /// 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) => {
+ info.offset = new_offset;
+ 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 mut info = match self.scroll_info {
+ Some(ref mut data) => data,
+ None => return None,
+ };
+
+ if info.scroll_sensitivity != 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.clone();
+
+ if scrollable_width > 0. {
+ info.offset.x = (info.offset.x + delta.x).min(0.0).max(-scrollable_width);
+ }
+
+ if scrollable_height > 0. {
+ 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,
+ });
+ return 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 ref mut node = 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))
+ }
}
/// A data structure which stores compositor-side information about
/// display lists sent to the compositor.
-/// by a WebRender display list.
-#[derive(Debug, Default, Deserialize, Serialize)]
+#[derive(Debug, Deserialize, Serialize)]
pub struct CompositorDisplayListInfo {
/// 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,
}
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: webrender_api::PipelineId,
+ ) -> 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: ScrollSensitivity::ScriptAndInputEvents,
+ offset: LayoutVector2D::zero(),
+ }),
+ );
+
+ CompositorDisplayListInfo {
+ hit_test_info: Default::default(),
+ scroll_tree,
+ root_reference_frame_id,
+ root_scroll_node_id,
+ }
+ }
+
/// 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>) -> usize {
+ pub fn add_hit_test_info(
+ &mut self,
+ node: u64,
+ cursor: Option<Cursor>,
+ scroll_tree_node: ScrollTreeNodeId,
+ ) -> usize {
if let Some(last) = self.hit_test_info.last() {
if node == last.node && cursor == last.cursor {
return self.hit_test_info.len() - 1;
}
}
- self.hit_test_info.push(HitTestInfo { node, cursor });
+ self.hit_test_info.push(HitTestInfo {
+ node,
+ cursor,
+ scroll_tree_node,
+ });
self.hit_test_info.len() - 1
}
}
diff --git a/components/script_traits/lib.rs b/components/script_traits/lib.rs
index 4eaf90ea7fc..90be1e44910 100644
--- a/components/script_traits/lib.rs
+++ b/components/script_traits/lib.rs
@@ -30,6 +30,7 @@ use crate::transferable::MessagePortImpl;
use crate::webdriver_msg::{LoadStatus, WebDriverScriptCommand};
use bluetooth_traits::BluetoothRequest;
use canvas_traits::webgl::WebGLPipeline;
+use compositor::ScrollTreeNodeId;
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender};
use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, WorkerId};
use embedder_traits::{Cursor, EventLoopWaker};
@@ -1118,6 +1119,9 @@ pub struct CompositorHitTestResult {
/// The cursor that should be used when hovering the item hit by the hit test.
pub cursor: Option<Cursor>,
+
+ /// The scroll tree node associated with this hit test item.
+ pub scroll_tree_node: ScrollTreeNodeId,
}
/// The set of WebRender operations that can be initiated by the content process.
diff --git a/components/script_traits/tests/compositor.rs b/components/script_traits/tests/compositor.rs
new file mode 100644
index 00000000000..e937d3c2cad
--- /dev/null
+++ b/components/script_traits/tests/compositor.rs
@@ -0,0 +1,181 @@
+/* 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 euclid::Size2D;
+use script_traits::compositor::{ScrollTree, ScrollTreeNodeId, ScrollableNodeInfo};
+use webrender_api::{
+ units::LayoutVector2D, ExternalScrollId, PipelineId, ScrollLocation, ScrollSensitivity,
+ 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: 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(|mut info| {
+ info.scroll_sensitivity = 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))
+ );
+}
diff --git a/python/servo/testing_commands.py b/python/servo/testing_commands.py
index 4b518f48e98..18780b2ac01 100644
--- a/python/servo/testing_commands.py
+++ b/python/servo/testing_commands.py
@@ -225,6 +225,7 @@ class MachCommands(CommandBase):
"net",
"net_traits",
"selectors",
+ "script_traits",
"servo_config",
"servo_remutex",
]