Auto merge of #29745 - mrobinson:compositor-side-scroll-tree, r=mukilan

Add a compositor-side scroll tree

This will allow the compositor to properly chain scrolling requests up
when a node has reached the extent of the scroll area. In addition, it
removes the use of the deprecated WebRender `scroll()` API. This fixes
scrolling on servo.org.

---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: -->
- [x] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [x] These changes:
  - Fix #29402.
  - Fix #27996.
  - Fix #27624.
  - Fix #24028.
  - Fix #23918.
  - Fix #21165.
- [x] There are tests for these changes
This commit is contained in:
bors-servo 2023-05-23 10:51:11 +02:00 committed by GitHub
commit ec4d90d572
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 677 additions and 102 deletions

2
Cargo.lock generated
View file

@ -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",

View file

@ -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 }

View file

@ -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);

View file

@ -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.");

View file

@ -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(

View file

@ -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,

View file

@ -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.

View file

@ -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" }

View file

@ -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
}
}

View file

@ -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.

View file

@ -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))
);
}

View file

@ -225,6 +225,7 @@ class MachCommands(CommandBase):
"net",
"net_traits",
"selectors",
"script_traits",
"servo_config",
"servo_remutex",
]