layout: Move hit testing for scrollable areas to display list construction (#39066)

Move the construction of hit test items for scroll nodes to the display
list construction stage. This way they respect the `z-index` of their
originating fragments and stacking context ordering in general.

Testing: We currently do not have great testing for this as this tests
the combination of hit testing of input events and scrolling at that
point. The completion of WebDriver support should make this easier to
test.
Fixes: #38967

Signed-off-by: coding-joedow <ibluegalaxy_taoj@163.com>
Co-authored-by: kongbai1996 <1782765876@qq.com>
This commit is contained in:
JoeDow 2025-09-02 19:57:23 +08:00 committed by GitHub
parent 802fdd9068
commit f8c0746c44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 68 additions and 72 deletions

View file

@ -200,7 +200,18 @@ impl DisplayListBuilder<'_> {
builder.add_clip_to_display_list(clip);
}
builder.push_hit_tests_for_scrollable_areas(&stacking_context_tree.hit_test_items);
// Add a single hit test that covers the entire viewport, so that WebRender knows
// which pipeline it hits when doing hit testing.
let pipeline_id = builder.compositor_info.pipeline_id;
let viewport_size = builder.compositor_info.viewport_details.size;
let viewport_rect = LayoutRect::from_size(viewport_size.cast_unit());
builder.wr().push_hit_test(
viewport_rect,
ClipChainId::INVALID,
SpatialId::root_reference_frame(pipeline_id),
PrimitiveFlags::default(),
(0, 0), /* tag */
);
// Paint the canvas background (if any) before/under everything else
stacking_context_tree
@ -311,39 +322,6 @@ impl DisplayListBuilder<'_> {
self.compositor_info.scroll_tree = scroll_tree;
}
fn push_hit_tests_for_scrollable_areas(
&mut self,
scroll_frame_hit_test_items: &[ScrollFrameHitTestItem],
) {
// Add a single hit test that covers the entire viewport, so that WebRender knows
// which pipeline it hits when doing hit testing.
let pipeline_id = self.compositor_info.pipeline_id;
let viewport_size = self.compositor_info.viewport_details.size;
let viewport_rect = LayoutRect::from_size(viewport_size.cast_unit());
self.wr().push_hit_test(
viewport_rect,
ClipChainId::INVALID,
SpatialId::root_reference_frame(pipeline_id),
PrimitiveFlags::default(),
(0, 0), /* tag */
);
for item in scroll_frame_hit_test_items {
let spatial_id = self
.compositor_info
.scroll_tree
.webrender_id(&item.scroll_node_id);
let clip_chain_id = self.clip_chain_id(item.clip_id);
self.wr().push_hit_test(
item.rect,
clip_chain_id,
spatial_id,
PrimitiveFlags::default(),
(item.external_scroll_id.0, 0), /* tag */
);
}
}
/// Add the given [`Clip`] to the WebRender display list and create a mapping from
/// its [`ClipId`] to a WebRender [`ClipChainId`]. This happens:
/// - When WebRender display list construction starts: All clips created during the
@ -583,6 +561,7 @@ impl Fragment {
builder: &mut DisplayListBuilder,
containing_block: &PhysicalRect<Au>,
section: StackingContextSection,
is_hit_test_for_scrollable_overflow: bool,
is_collapsed_table_borders: bool,
text_decorations: &Arc<Vec<FragmentTextDecoration>>,
) {
@ -606,6 +585,7 @@ impl Fragment {
Visibility::Visible => BuilderForBoxFragment::new(
box_fragment,
containing_block,
is_hit_test_for_scrollable_overflow,
is_collapsed_table_borders,
)
.build(builder, section),
@ -937,6 +917,7 @@ struct BuilderForBoxFragment<'a> {
border_edge_clip_chain_id: RefCell<Option<ClipChainId>>,
padding_edge_clip_chain_id: RefCell<Option<ClipChainId>>,
content_edge_clip_chain_id: RefCell<Option<ClipChainId>>,
is_hit_test_for_scrollable_overflow: bool,
is_collapsed_table_borders: bool,
}
@ -944,6 +925,7 @@ impl<'a> BuilderForBoxFragment<'a> {
fn new(
fragment: &'a BoxFragment,
containing_block: &'a PhysicalRect<Au>,
is_hit_test_for_scrollable_overflow: bool,
is_collapsed_table_borders: bool,
) -> Self {
let border_rect = fragment
@ -960,6 +942,7 @@ impl<'a> BuilderForBoxFragment<'a> {
border_edge_clip_chain_id: RefCell::new(None),
padding_edge_clip_chain_id: RefCell::new(None),
content_edge_clip_chain_id: RefCell::new(None),
is_hit_test_for_scrollable_overflow,
is_collapsed_table_borders,
}
}
@ -1042,6 +1025,13 @@ impl<'a> BuilderForBoxFragment<'a> {
}
fn build(&mut self, builder: &mut DisplayListBuilder, section: StackingContextSection) {
if self.is_hit_test_for_scrollable_overflow &&
self.fragment.style.get_inherited_ui().pointer_events !=
style::computed_values::pointer_events::T::None
{
self.build_hit_test(builder, self.fragment.scrollable_overflow().to_webrender());
return;
}
if self.is_collapsed_table_borders {
self.build_collapsed_table_borders(builder);
return;
@ -1066,6 +1056,24 @@ impl<'a> BuilderForBoxFragment<'a> {
self.build_border(builder);
}
fn build_hit_test(&self, builder: &mut DisplayListBuilder, rect: LayoutRect) {
let external_scroll_node_id = builder
.compositor_info
.external_scroll_id_for_scroll_tree_node(builder.current_scroll_node_id);
let mut common = builder.common_properties(rect, &self.fragment.style);
if let Some(clip_chain_id) = self.border_edge_clip(builder, false) {
common.clip_chain_id = clip_chain_id;
}
builder.wr().push_hit_test(
common.clip_rect,
common.clip_chain_id,
common.spatial_id,
common.flags,
(external_scroll_node_id.0, 0), /* tag */
);
}
fn build_background_for_painter(
&mut self,
builder: &mut DisplayListBuilder,

View file

@ -34,7 +34,7 @@ use style::values::generics::box_::Perspective;
use style::values::generics::transform::{self, GenericRotate, GenericScale, GenericTranslate};
use style::values::specified::box_::DisplayOutside;
use webrender_api::units::{LayoutPoint, LayoutRect, LayoutTransform, LayoutVector2D};
use webrender_api::{self as wr, BorderRadius, ExternalScrollId};
use webrender_api::{self as wr, BorderRadius};
use wr::StickyOffsetBounds;
use wr::units::{LayoutPixel, LayoutSize};
@ -101,23 +101,6 @@ pub(crate) enum StackingContextSection {
Outline,
}
#[derive(MallocSizeOf)]
pub(crate) struct ScrollFrameHitTestItem {
/// The [`ScrollTreeNodeId`] of the spatial node that contains this hit test item.
pub scroll_node_id: ScrollTreeNodeId,
/// The [`ClipId`] of the clip that clips this [`ScrollFrameHitTestItems`].
pub clip_id: ClipId,
/// The rectangle of the scroll frame in the coordinate space of [`Self::scroll_node_id`].
pub rect: LayoutRect,
/// The WebRender [`ExternalScrollId`] of the scrolling spatial node that
/// this [`ScrollFrameHitTestItem`] identifies. Note that this is a *different*
/// spatial node than the one identified by [`Self::scroll_node_id`] (the parent).
pub external_scroll_id: ExternalScrollId,
}
#[derive(MallocSizeOf)]
pub(crate) struct StackingContextTree {
/// The root stacking context of this [`StackingContextTree`].
@ -133,10 +116,6 @@ pub(crate) struct StackingContextTree {
/// for things like `overflow`. More clips may be created later during WebRender
/// display list construction, but they are never added here.
pub clip_store: StackingContextTreeClipStore,
/// A vector of hit test items, one per scroll frame. These are used for allowing
/// renderer-side scrolling in the Servo renderer.
pub hit_test_items: Vec<ScrollFrameHitTestItem>,
}
impl StackingContextTree {
@ -197,7 +176,6 @@ impl StackingContextTree {
root_stacking_context: StackingContext::create_root(root_scroll_node_id, debug),
compositor_info,
clip_store: Default::default(),
hit_test_items: Vec::new(),
};
let mut root_stacking_context = StackingContext::create_root(root_scroll_node_id, debug);
@ -306,6 +284,7 @@ pub(crate) enum StackingContextContent {
section: StackingContextSection,
containing_block: PhysicalRect<Au>,
fragment: Fragment,
is_hit_test_for_scrollable_overflow: bool,
is_collapsed_table_borders: bool,
#[conditional_malloc_size_of]
text_decorations: Arc<Vec<FragmentTextDecoration>>,
@ -338,6 +317,7 @@ impl StackingContextContent {
section,
containing_block,
fragment,
is_hit_test_for_scrollable_overflow,
is_collapsed_table_borders,
text_decorations,
} => {
@ -348,6 +328,7 @@ impl StackingContextContent {
builder,
containing_block,
*section,
*is_hit_test_for_scrollable_overflow,
*is_collapsed_table_borders,
text_decorations,
);
@ -664,6 +645,7 @@ impl StackingContext {
let mut fragment_builder = BuilderForBoxFragment::new(
&root_fragment,
&fragment_tree.initial_containing_block,
false, /* is_hit_test_for_scrollable_overflow */
false, /* is_collapsed_table_borders */
);
let painter = super::background::BackgroundPainter {
@ -918,6 +900,7 @@ impl Fragment {
clip_id: containing_block.clip_id,
containing_block: containing_block.rect,
fragment: fragment_clone,
is_hit_test_for_scrollable_overflow: false,
is_collapsed_table_borders: false,
text_decorations: text_decorations.clone(),
});
@ -1121,6 +1104,7 @@ impl BoxFragment {
BuilderForBoxFragment::new(
self,
&containing_block.rect,
false, /* is_hit_test_for_scrollable_overflow */
false, /* is_collapsed_table_borders */
),
)
@ -1204,6 +1188,7 @@ impl BoxFragment {
BuilderForBoxFragment::new(
self,
&containing_block.rect,
false, /* is_hit_test_for_scrollable_overflow */
false, /* is_collapsed_table_borders */
),
) {
@ -1236,6 +1221,7 @@ impl BoxFragment {
section,
containing_block: containing_block.rect,
fragment: fragment.clone(),
is_hit_test_for_scrollable_overflow: false,
is_collapsed_table_borders: false,
text_decorations: text_decorations.clone(),
});
@ -1264,6 +1250,20 @@ impl BoxFragment {
if let Some(scroll_frame_data) = overflow_frame_data.scroll_frame_data {
new_scroll_node_id = scroll_frame_data.scroll_tree_node_id;
new_scroll_frame_size = Some(scroll_frame_data.scroll_frame_rect.size());
stacking_context
.contents
.push(StackingContextContent::Fragment {
scroll_node_id: new_scroll_node_id,
reference_frame_scroll_node_id:
reference_frame_scroll_node_id_for_fragments,
clip_id: new_clip_id,
section,
containing_block: containing_block.rect,
fragment: fragment.clone(),
is_hit_test_for_scrollable_overflow: true,
is_collapsed_table_borders: false,
text_decorations: text_decorations.clone(),
});
}
}
@ -1360,6 +1360,7 @@ impl BoxFragment {
section,
containing_block: containing_block.rect,
fragment: fragment.clone(),
is_hit_test_for_scrollable_overflow: false,
is_collapsed_table_borders: true,
text_decorations: text_decorations.clone(),
});
@ -1431,7 +1432,7 @@ impl BoxFragment {
// https://drafts.csswg.org/css-overflow-3/#corner-clipping
let radii;
if overflow.x == ComputedOverflow::Clip && overflow.y == ComputedOverflow::Clip {
let builder = BuilderForBoxFragment::new(self, containing_block_rect, false);
let builder = BuilderForBoxFragment::new(self, containing_block_rect, false, false);
radii = offset_radii(builder.border_radius, clip_margin);
} else if overflow.x != ComputedOverflow::Clip {
overflow_clip_rect.min.x = f32::MIN;
@ -1462,7 +1463,7 @@ impl BoxFragment {
.to_webrender();
let clip_id = stacking_context_tree.clip_store.add(
BuilderForBoxFragment::new(self, containing_block_rect, false).border_radius,
BuilderForBoxFragment::new(self, containing_block_rect, false, false).border_radius,
scroll_frame_rect,
*parent_scroll_node_id,
parent_clip_id,
@ -1487,19 +1488,6 @@ impl BoxFragment {
sensitivity,
);
use style::computed_values::pointer_events::T as PointerEvents;
if self.style.get_inherited_ui().pointer_events != PointerEvents::None {
stacking_context_tree
.hit_test_items
.push(ScrollFrameHitTestItem {
scroll_node_id: *parent_scroll_node_id,
clip_id,
rect: scroll_frame_rect,
external_scroll_id,
});
}
Some(OverflowFrameData {
clip_id,
scroll_frame_data: Some(ScrollFrameData {