layout: Add a hit test item that covers all scroll frame contents (#34347)

When building scroll frames, add a special
`StackingContextContent::Fragment` type for a hit test that covers all
scroll frame contents. This makes it so that you don't have to be
hovering over actual content to scroll the scroll frame.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Mukilan Thiyagarajan <mukilan@igalia.com>
This commit is contained in:
Martin Robinson 2024-11-24 20:04:57 +01:00 committed by GitHub
parent c60e4afbee
commit c11e0e8e70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 104 additions and 55 deletions

View file

@ -33,7 +33,7 @@ use style::values::specified::text::TextDecorationLine;
use style::values::specified::ui::CursorKind;
use style::Zero;
use style_traits::{CSSPixel, DevicePixel};
use webrender_api::units::{LayoutPixel, LayoutSize};
use webrender_api::units::{LayoutPixel, LayoutRect, LayoutSize};
use webrender_api::{
self as wr, units, BorderDetails, BoxShadowClipMode, ClipChainId, CommonItemProperties,
ImageRendering, NinePatchBorder, NinePatchBorderSource,
@ -250,14 +250,20 @@ impl Fragment {
builder: &mut DisplayListBuilder,
containing_block: &PhysicalRect<Au>,
section: StackingContextSection,
is_hit_test_for_scrollable_overflow: bool,
) {
match self {
Fragment::Box(b) | Fragment::Float(b) => match b.style.get_inherited_box().visibility {
Visibility::Visible => {
BuilderForBoxFragment::new(b, containing_block).build(builder, section)
},
Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => {
match box_fragment.style.get_inherited_box().visibility {
Visibility::Visible => BuilderForBoxFragment::new(
box_fragment,
containing_block,
is_hit_test_for_scrollable_overflow,
)
.build(builder, section),
Visibility::Hidden => (),
Visibility::Collapse => (),
}
},
Fragment::AbsoluteOrFixedPositioned(_) => {},
Fragment::Positioning(positioning_fragment) => {
@ -508,10 +514,15 @@ 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,
}
impl<'a> BuilderForBoxFragment<'a> {
fn new(fragment: &'a BoxFragment, containing_block: &'a PhysicalRect<Au>) -> Self {
fn new(
fragment: &'a BoxFragment,
containing_block: &'a PhysicalRect<Au>,
is_hit_test_for_scrollable_overflow: bool,
) -> Self {
let border_rect = fragment
.border_rect()
.translate(containing_block.origin.to_vector());
@ -550,6 +561,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,
}
}
@ -635,10 +647,17 @@ impl<'a> BuilderForBoxFragment<'a> {
}
fn build(&mut self, builder: &mut DisplayListBuilder, section: StackingContextSection) {
if self.is_hit_test_for_scrollable_overflow {
self.build_hit_test(builder, self.fragment.scrollable_overflow().to_webrender());
return;
}
if section == StackingContextSection::Outline {
self.build_outline(builder);
} else {
self.build_hit_test(builder);
return;
}
self.build_hit_test(builder, self.border_rect);
if self
.fragment
.base
@ -651,9 +670,8 @@ impl<'a> BuilderForBoxFragment<'a> {
self.build_box_shadow(builder);
self.build_border(builder);
}
}
fn build_hit_test(&self, builder: &mut DisplayListBuilder) {
fn build_hit_test(&self, builder: &mut DisplayListBuilder, rect: LayoutRect) {
let hit_info = builder.hit_info(
&self.fragment.style,
self.fragment.base.tag,
@ -664,7 +682,7 @@ impl<'a> BuilderForBoxFragment<'a> {
None => return,
};
let mut common = builder.common_properties(self.border_rect, &self.fragment.style);
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;
}

View file

@ -261,6 +261,7 @@ pub(crate) enum StackingContextContent {
section: StackingContextSection,
containing_block: PhysicalRect<Au>,
fragment: ArcRefCell<Fragment>,
is_hit_test_for_scrollable_overflow: bool,
},
/// An index into [StackingContext::atomic_inline_stacking_containers].
@ -290,13 +291,17 @@ impl StackingContextContent {
section,
containing_block,
fragment,
is_hit_test_for_scrollable_overflow,
} => {
builder.current_scroll_node_id = *scroll_node_id;
builder.current_reference_frame_scroll_node_id = *reference_frame_scroll_node_id;
builder.current_clip_chain_id = *clip_chain_id;
fragment
.borrow()
.build_display_list(builder, containing_block, *section);
fragment.borrow().build_display_list(
builder,
containing_block,
*section,
*is_hit_test_for_scrollable_overflow,
);
},
Self::AtomicInlineStackingContainer { index } => {
inline_stacking_containers[*index].build_display_list(builder);
@ -666,7 +671,8 @@ impl StackingContext {
}
}
let mut fragment_builder = BuilderForBoxFragment::new(box_fragment, containing_block);
let mut fragment_builder =
BuilderForBoxFragment::new(box_fragment, containing_block, false);
let painter = super::background::BackgroundPainter {
style,
painting_area_override: Some(painting_area),
@ -804,10 +810,10 @@ impl StackingContext {
match field {
DebugPrintField::Contents => match self.contents[*index] {
StackingContextContent::Fragment { section, .. } => {
tree.add_item(format!("{:?}", section));
tree.add_item(format!("{section:?}"));
},
StackingContextContent::AtomicInlineStackingContainer { index } => {
tree.new_level(format!("AtomicInlineStackingContainer #{}", index));
tree.new_level(format!("AtomicInlineStackingContainer #{index}"));
self.atomic_inline_stacking_containers[index].debug_print_with_tree(tree);
tree.end_level();
},
@ -912,6 +918,7 @@ impl Fragment {
clip_chain_id: containing_block.clip_chain_id,
containing_block: containing_block.rect,
fragment: fragment_ref.clone(),
is_hit_test_for_scrollable_overflow: false,
});
},
}
@ -923,6 +930,11 @@ struct ReferenceFrameData {
transform: LayoutTransform,
kind: wr::ReferenceFrameKind,
}
struct ScrollFrameData {
scroll_tree_node_id: ScrollTreeNodeId,
clip_chain_id: wr::ClipChainId,
scroll_frame_rect: LayoutRect,
}
impl BoxFragment {
fn get_stacking_context_type(&self) -> Option<StackingContextType> {
@ -1080,7 +1092,7 @@ impl BoxFragment {
display_list,
&containing_block.scroll_node_id,
&containing_block.clip_chain_id,
BuilderForBoxFragment::new(self, &containing_block.rect),
BuilderForBoxFragment::new(self, &containing_block.rect, false),
)
.unwrap_or(containing_block.clip_chain_id);
@ -1152,7 +1164,7 @@ impl BoxFragment {
display_list,
&new_scroll_node_id,
&new_clip_chain_id,
BuilderForBoxFragment::new(self, &containing_block.rect),
BuilderForBoxFragment::new(self, &containing_block.rect, false),
) {
new_clip_chain_id = clip_chain_id;
}
@ -1181,6 +1193,7 @@ impl BoxFragment {
section: self.get_stacking_context_section(),
containing_block: containing_block.rect,
fragment: fragment.clone(),
is_hit_test_for_scrollable_overflow: false,
});
if !self.style.get_outline().outline_width.is_zero() {
@ -1193,22 +1206,33 @@ impl BoxFragment {
section: StackingContextSection::Outline,
containing_block: containing_block.rect,
fragment: fragment.clone(),
is_hit_test_for_scrollable_overflow: false,
});
}
// 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((scroll_node_id, clip_chain_id, scroll_frame_size)) = self
.build_scroll_frame_if_necessary(
if let Some(scroll_frame_data) = self.build_scroll_frame_if_necessary(
display_list,
&new_scroll_node_id,
&new_clip_chain_id,
&containing_block.rect,
)
{
new_scroll_node_id = scroll_node_id;
new_clip_chain_id = clip_chain_id;
new_scroll_frame_size = Some(scroll_frame_size);
) {
new_scroll_node_id = scroll_frame_data.scroll_tree_node_id;
new_clip_chain_id = scroll_frame_data.clip_chain_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_chain_id: new_clip_chain_id,
section: self.get_stacking_context_section(),
containing_block: containing_block.rect,
fragment: fragment.clone(),
is_hit_test_for_scrollable_overflow: true,
});
}
let padding_rect = self
@ -1297,8 +1321,7 @@ impl BoxFragment {
parent_scroll_node_id: &ScrollTreeNodeId,
parent_clip_id: &wr::ClipChainId,
containing_block_rect: &PhysicalRect<Au>,
) -> Option<(ScrollTreeNodeId, wr::ClipChainId, LayoutSize)> {
let overflow = self.style.effective_overflow();
) -> Option<ScrollFrameData> {
if !self.style.establishes_scroll_container() {
return None;
}
@ -1327,6 +1350,7 @@ impl BoxFragment {
display_list.wr.pipeline_id,
);
let overflow = self.style.effective_overflow();
let sensitivity =
if ComputedOverflow::Hidden == overflow.x && ComputedOverflow::Hidden == overflow.y {
ScrollSensitivity::Script
@ -1334,21 +1358,26 @@ impl BoxFragment {
ScrollSensitivity::ScriptAndInputEvents
};
let padding_rect = self
let scroll_frame_rect = self
.padding_rect()
.translate(containing_block_rect.origin.to_vector())
.to_webrender();
let content_rect = self.scrollable_overflow().to_webrender();
let (scroll_tree_node_id, clip_chain_id) = display_list.define_scroll_frame(
parent_scroll_node_id,
parent_clip_id,
external_id,
self.scrollable_overflow().to_webrender(),
padding_rect,
content_rect,
scroll_frame_rect,
sensitivity,
);
Some((scroll_tree_node_id, clip_chain_id, padding_rect.size()))
Some(ScrollFrameData {
scroll_tree_node_id,
clip_chain_id,
scroll_frame_rect,
})
}
fn build_sticky_frame_if_necessary(

View file

@ -21,7 +21,7 @@ pub enum ScrollSensitivity {
/// Information that Servo keeps alongside WebRender display items
/// in order to add more context to hit test results.
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub struct HitTestInfo {
/// The id of the node of this hit test item.
pub node: u64,
@ -327,17 +327,19 @@ impl CompositorDisplayListInfo {
cursor: Option<Cursor>,
scroll_tree_node: ScrollTreeNodeId,
) -> usize {
let hit_test_info = HitTestInfo {
node,
cursor,
scroll_tree_node,
};
if let Some(last) = self.hit_test_info.last() {
if node == last.node && cursor == last.cursor {
if hit_test_info == *last {
return self.hit_test_info.len() - 1;
}
}
self.hit_test_info.push(HitTestInfo {
node,
cursor,
scroll_tree_node,
});
self.hit_test_info.push(hit_test_info);
self.hit_test_info.len() - 1
}
}