diff options
-rw-r--r-- | Cargo.lock | 2 | ||||
-rw-r--r-- | components/compositing/Cargo.toml | 1 | ||||
-rw-r--r-- | components/compositing/compositor.rs | 89 | ||||
-rw-r--r-- | components/layout/display_list/webrender_helpers.rs | 115 | ||||
-rw-r--r-- | components/layout_2020/display_list/mod.rs | 26 | ||||
-rw-r--r-- | components/layout_2020/display_list/stacking_context.rs | 93 | ||||
-rw-r--r-- | components/layout_thread/lib.rs | 12 | ||||
-rw-r--r-- | components/script_traits/Cargo.toml | 3 | ||||
-rw-r--r-- | components/script_traits/compositor.rs | 252 | ||||
-rw-r--r-- | components/script_traits/lib.rs | 4 | ||||
-rw-r--r-- | components/script_traits/tests/compositor.rs | 181 | ||||
-rw-r--r-- | python/servo/testing_commands.py | 1 |
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", ] |