servo/components/layout/display_list/hit_test.rs
Oriol Brufau 1473121fe5
layout: Avoid ClipId, ExternalScrollId and ScrollTreeNodeId references (#39372)
Changes function signatures to accept `ClipId`, `ExternalScrollId` and
`ScrollTreeNodeId` instead of `&ClipId`, `&ExternalScrollId` and
`&ScrollTreeNodeId`. This avoids several `&` and `*`.

Testing: not needed, no behavior change.

Signed-off-by: Oriol Brufau <obrufau@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
2025-09-18 23:34:49 +00:00

414 lines
15 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 app_units::Au;
use base::id::ScrollTreeNodeId;
use embedder_traits::Cursor;
use euclid::{Box2D, Vector2D};
use kurbo::{Ellipse, Shape};
use layout_api::{ElementsFromPointFlags, ElementsFromPointResult};
use rustc_hash::FxHashMap;
use servo_geometry::FastLayoutTransform;
use style::computed_values::backface_visibility::T as BackfaceVisibility;
use style::computed_values::pointer_events::T as PointerEvents;
use style::computed_values::visibility::T as Visibility;
use style::properties::ComputedValues;
use style::values::computed::ui::CursorKind;
use webrender_api::BorderRadius;
use webrender_api::units::{LayoutPoint, LayoutRect, LayoutSize, RectExt};
use crate::display_list::clip::{Clip, ClipId};
use crate::display_list::stacking_context::StackingContextSection;
use crate::display_list::{
StackingContext, StackingContextContent, StackingContextTree, ToWebRender,
};
use crate::fragment_tree::{Fragment, FragmentFlags};
use crate::geom::PhysicalRect;
pub(crate) struct HitTest<'a> {
/// The flags which describe how to perform this [`HitTest`].
flags: ElementsFromPointFlags,
/// The point to test for this hit test, relative to the page.
point_to_test: LayoutPoint,
/// A cached version of [`Self::point_to_test`] projected to a spatial node, to avoid
/// doing a lot of matrix math over and over.
projected_point_to_test: Option<(ScrollTreeNodeId, LayoutPoint, FastLayoutTransform)>,
/// The stacking context tree against which to perform the hit test.
stacking_context_tree: &'a StackingContextTree,
/// The resulting [`HitTestResultItems`] for this hit test.
results: Vec<ElementsFromPointResult>,
/// A cache of hit test results for shared clip nodes.
clip_hit_test_results: FxHashMap<ClipId, bool>,
}
impl<'a> HitTest<'a> {
pub(crate) fn run(
stacking_context_tree: &'a StackingContextTree,
point_to_test: LayoutPoint,
flags: ElementsFromPointFlags,
) -> Vec<ElementsFromPointResult> {
let mut hit_test = Self {
flags,
point_to_test,
projected_point_to_test: None,
stacking_context_tree,
results: Vec::new(),
clip_hit_test_results: FxHashMap::default(),
};
stacking_context_tree
.root_stacking_context
.hit_test(&mut hit_test);
hit_test.results
}
/// Perform a hit test against a the clip node for the given [`ClipId`], returning
/// true if it is not clipped out or false if is clipped out.
fn hit_test_clip_id(&mut self, clip_id: ClipId) -> bool {
if clip_id == ClipId::INVALID {
return true;
}
if let Some(result) = self.clip_hit_test_results.get(&clip_id) {
return *result;
}
let clip = self.stacking_context_tree.clip_store.get(clip_id);
let result = self
.location_in_spatial_node(clip.parent_scroll_node_id)
.is_some_and(|(point, _)| {
clip.contains(point) && self.hit_test_clip_id(clip.parent_clip_id)
});
self.clip_hit_test_results.insert(clip_id, result);
result
}
/// Get the hit test location in the coordinate system of the given spatial node,
/// returning `None` if the transformation is uninvertible or the point cannot be
/// projected into the spatial node.
fn location_in_spatial_node(
&mut self,
scroll_tree_node_id: ScrollTreeNodeId,
) -> Option<(LayoutPoint, FastLayoutTransform)> {
match self.projected_point_to_test {
Some((cached_scroll_tree_node_id, projected_point, transform))
if cached_scroll_tree_node_id == scroll_tree_node_id =>
{
return Some((projected_point, transform));
},
_ => {},
}
let transform = self
.stacking_context_tree
.compositor_info
.scroll_tree
.cumulative_root_to_node_transform(scroll_tree_node_id)?;
let projected_point = transform.project_point2d(self.point_to_test)?;
self.projected_point_to_test = Some((scroll_tree_node_id, projected_point, transform));
Some((projected_point, transform))
}
}
impl Clip {
fn contains(&self, point: LayoutPoint) -> bool {
rounded_rect_contains_point(self.rect, &self.radii, point)
}
}
impl StackingContext {
/// Perform a hit test against a [`StackingContext`]. Note that this is the reverse
/// of the stacking context walk algorithm in `stacking_context.rs`. Any changes made
/// here should be reflected in the forward version in that file.
fn hit_test(&self, hit_test: &mut HitTest) -> bool {
let mut contents = self.contents.iter().rev().peekable();
// Step 10: Outlines
while contents
.peek()
.is_some_and(|child| child.section() == StackingContextSection::Outline)
{
// The hit test will not hit the outline.
let _ = contents.next().unwrap();
}
// Steps 8 and 9: Stacking contexts with non-negative z-index, and
// positioned stacking containers (where z-index is auto)
let mut real_stacking_contexts_and_positioned_stacking_containers = self
.real_stacking_contexts_and_positioned_stacking_containers
.iter()
.rev()
.peekable();
while real_stacking_contexts_and_positioned_stacking_containers
.peek()
.is_some_and(|child| child.z_index() >= 0)
{
let child = real_stacking_contexts_and_positioned_stacking_containers
.next()
.unwrap();
if child.hit_test(hit_test) {
return true;
}
}
// Steps 7 and 8: Fragments and inline stacking containers
while contents
.peek()
.is_some_and(|child| child.section() == StackingContextSection::Foreground)
{
let child = contents.next().unwrap();
if self.hit_test_content(child, hit_test) {
return true;
}
}
// Step 6: Float stacking containers
for child in self.float_stacking_containers.iter().rev() {
if child.hit_test(hit_test) {
return true;
}
}
// Step 5: Block backgrounds and borders
while contents.peek().is_some_and(|child| {
child.section() == StackingContextSection::DescendantBackgroundsAndBorders
}) {
let child = contents.next().unwrap();
if self.hit_test_content(child, hit_test) {
return true;
}
}
// Step 4: Stacking contexts with negative z-index
for child in real_stacking_contexts_and_positioned_stacking_containers {
if child.hit_test(hit_test) {
return true;
}
}
// Steps 2 and 3: Borders and background for the root
while contents.peek().is_some_and(|child| {
child.section() == StackingContextSection::OwnBackgroundsAndBorders
}) {
let child = contents.next().unwrap();
if self.hit_test_content(child, hit_test) {
return true;
}
}
false
}
pub(crate) fn hit_test_content(
&self,
content: &StackingContextContent,
hit_test: &mut HitTest<'_>,
) -> bool {
match content {
StackingContextContent::Fragment {
scroll_node_id,
clip_id,
containing_block,
fragment,
..
} => {
hit_test.hit_test_clip_id(*clip_id) &&
fragment.hit_test(hit_test, *scroll_node_id, containing_block)
},
StackingContextContent::AtomicInlineStackingContainer { index } => {
self.atomic_inline_stacking_containers[*index].hit_test(hit_test)
},
}
}
}
impl Fragment {
pub(crate) fn hit_test(
&self,
hit_test: &mut HitTest,
spatial_node_id: ScrollTreeNodeId,
containing_block: &PhysicalRect<Au>,
) -> bool {
let Some(tag) = self.tag() else {
return false;
};
let mut hit_test_fragment_inner =
|style: &ComputedValues,
fragment_rect: PhysicalRect<Au>,
border_radius: BorderRadius,
fragment_flags: FragmentFlags,
auto_cursor: Cursor| {
let is_root_element = fragment_flags.contains(FragmentFlags::IS_ROOT_ELEMENT);
if !is_root_element {
if style.get_inherited_ui().pointer_events == PointerEvents::None {
return false;
}
if style.get_inherited_box().visibility != Visibility::Visible {
return false;
}
}
let (point_in_spatial_node, transform) =
match hit_test.location_in_spatial_node(spatial_node_id) {
Some(point) => point,
None => return false,
};
if !is_root_element &&
style.get_box().backface_visibility == BackfaceVisibility::Hidden &&
transform.is_backface_visible()
{
return false;
}
let fragment_rect = fragment_rect.translate(containing_block.origin.to_vector());
if is_root_element {
let viewport_size = hit_test
.stacking_context_tree
.compositor_info
.viewport_details
.size;
let viewport_rect = LayoutRect::from_origin_and_size(
Default::default(),
viewport_size.cast_unit(),
);
if !viewport_rect.contains(hit_test.point_to_test) {
return false;
}
} else if !rounded_rect_contains_point(
fragment_rect.to_webrender(),
&border_radius,
point_in_spatial_node,
) {
return false;
}
let point_in_target = point_in_spatial_node.cast_unit() -
Vector2D::new(
fragment_rect.origin.x.to_f32_px(),
fragment_rect.origin.y.to_f32_px(),
);
hit_test.results.push(ElementsFromPointResult {
node: tag.node,
point_in_target,
cursor: cursor(style.get_inherited_ui().cursor.keyword, auto_cursor),
});
!hit_test.flags.contains(ElementsFromPointFlags::FindAll)
};
match self {
Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => {
let box_fragment = box_fragment.borrow();
hit_test_fragment_inner(
&box_fragment.style,
box_fragment.border_rect(),
box_fragment.border_radius(),
box_fragment.base.flags,
Cursor::Default,
)
},
Fragment::Text(text) => {
let text = &*text.borrow();
hit_test_fragment_inner(
&text.inline_styles.style.borrow(),
text.rect,
BorderRadius::zero(),
FragmentFlags::empty(),
Cursor::Text,
)
},
_ => false,
}
}
}
fn rounded_rect_contains_point(
rect: LayoutRect,
border_radius: &BorderRadius,
point: LayoutPoint,
) -> bool {
if !rect.contains(point) {
return false;
}
if border_radius.is_zero() {
return true;
}
let check_corner = |corner: LayoutPoint, radius: &LayoutSize, is_right, is_bottom| {
let mut origin = corner;
if is_right {
origin.x -= radius.width;
}
if is_bottom {
origin.y -= radius.height;
}
if !Box2D::from_origin_and_size(origin, *radius).contains(point) {
return true;
}
let center = (
if is_right {
corner.x - radius.width
} else {
corner.x + radius.width
},
if is_bottom {
corner.y - radius.height
} else {
corner.y + radius.height
},
);
let radius = (radius.width as f64, radius.height as f64);
Ellipse::new(center, radius, 0.0).contains((point.x, point.y).into())
};
check_corner(rect.top_left(), &border_radius.top_left, false, false) &&
check_corner(rect.top_right(), &border_radius.top_right, true, false) &&
check_corner(rect.bottom_right(), &border_radius.bottom_right, true, true) &&
check_corner(rect.bottom_left(), &border_radius.bottom_left, false, true)
}
fn cursor(kind: CursorKind, auto_cursor: Cursor) -> Cursor {
match kind {
CursorKind::Auto => auto_cursor,
CursorKind::None => Cursor::None,
CursorKind::Default => Cursor::Default,
CursorKind::Pointer => Cursor::Pointer,
CursorKind::ContextMenu => Cursor::ContextMenu,
CursorKind::Help => Cursor::Help,
CursorKind::Progress => Cursor::Progress,
CursorKind::Wait => Cursor::Wait,
CursorKind::Cell => Cursor::Cell,
CursorKind::Crosshair => Cursor::Crosshair,
CursorKind::Text => Cursor::Text,
CursorKind::VerticalText => Cursor::VerticalText,
CursorKind::Alias => Cursor::Alias,
CursorKind::Copy => Cursor::Copy,
CursorKind::Move => Cursor::Move,
CursorKind::NoDrop => Cursor::NoDrop,
CursorKind::NotAllowed => Cursor::NotAllowed,
CursorKind::Grab => Cursor::Grab,
CursorKind::Grabbing => Cursor::Grabbing,
CursorKind::EResize => Cursor::EResize,
CursorKind::NResize => Cursor::NResize,
CursorKind::NeResize => Cursor::NeResize,
CursorKind::NwResize => Cursor::NwResize,
CursorKind::SResize => Cursor::SResize,
CursorKind::SeResize => Cursor::SeResize,
CursorKind::SwResize => Cursor::SwResize,
CursorKind::WResize => Cursor::WResize,
CursorKind::EwResize => Cursor::EwResize,
CursorKind::NsResize => Cursor::NsResize,
CursorKind::NeswResize => Cursor::NeswResize,
CursorKind::NwseResize => Cursor::NwseResize,
CursorKind::ColResize => Cursor::ColResize,
CursorKind::RowResize => Cursor::RowResize,
CursorKind::AllScroll => Cursor::AllScroll,
CursorKind::ZoomIn => Cursor::ZoomIn,
CursorKind::ZoomOut => Cursor::ZoomOut,
}
}