layout: Add a layout hit test and use it for document.elementsFromPoint (#38463)

In #18933, hit testing was moved from layout to WebRender. This presents
some issues. For instance, the DOM can change at the same time that hit
test is happening. This can mean that hit test returns references to
defunct DOM nodes, introducing memory safety issues. Currently, Servo
will try to ensure that the epochs used for testing and those recorded
in the DOM match, but this is not very reliable and has led to code that
retries failed hit tests.

This change reintroduces (8 years later) a layout hit tester and turns
it on for `document.elementFromPoint` and `document.elementsFromPoint`.
The idea is that this hit tester will gradually replace the majority of
the WebRender hit testing happening in the renderer.

Testing: This shouldn't really change the behavior hit testing, but it
seems to improve one WPT test.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
Co-authored-by: kongbai1996 <1782765876@qq.com>
This commit is contained in:
Martin Robinson 2025-08-05 11:48:21 +02:00 committed by GitHub
parent 3e856cbf11
commit 11844ca5af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 539 additions and 113 deletions

View file

@ -296,6 +296,7 @@ impl ScrollableNodeInfo {
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
pub struct ScrollTreeNodeTransformationCache {
node_to_root_transform: LayoutTransform,
root_to_node_transform: Option<LayoutTransform>,
}
#[derive(Default)]
@ -599,7 +600,8 @@ impl ScrollTree {
})
}
/// Traverse a scroll node to its root to calculate the transform.
/// Find a transformation that can convert a point in the node coordinate system to a
/// point in the root coordinate system.
pub fn cumulative_node_to_root_transform(&self, node_id: &ScrollTreeNodeId) -> LayoutTransform {
let node = self.get_node(node_id);
if let Some(cached_transforms) = node.transformation_cache.get() {
@ -611,6 +613,23 @@ impl ScrollTree {
transforms.node_to_root_transform
}
/// Find a transformation that can convert a point in the root coordinate system to a
/// point in the coordinate system of the given node. This may be `None` if the cumulative
/// transform is uninvertible.
pub fn cumulative_root_to_node_transform(
&self,
node_id: &ScrollTreeNodeId,
) -> Option<LayoutTransform> {
let node = self.get_node(node_id);
if let Some(cached_transforms) = node.transformation_cache.get() {
return cached_transforms.root_to_node_transform;
}
let (transforms, _) = self.cumulative_node_transform_inner(node);
node.transformation_cache.set(Some(transforms));
transforms.root_to_node_transform
}
/// Traverse a scroll node to its root to calculate the transform.
fn cumulative_node_transform_inner(
&self,
@ -628,7 +647,7 @@ impl ScrollTree {
post_translation.then(transform).then(&pre_translation)
};
let node_to_parent_transform = match &node.info {
let (node_to_parent_transform, parent_to_node_transform) = match &node.info {
SpatialTreeNodeInfo::ReferenceFrame(info) => {
// To apply a transformation we need to make sure the rectangle's
// coordinate space is the same as reference frame's coordinate space.
@ -638,30 +657,52 @@ impl ScrollTree {
info.frame_origin_for_query.y,
);
let parent_to_node_transform =
Transform3D::translation(-info.origin.x, -info.origin.y, 0.0);
let parent_to_node_transform = info
.transform
.inverse()
.map(|inverse_transform| parent_to_node_transform.then(&inverse_transform));
sticky_info.nearest_scrolling_ancestor_viewport = sticky_info
.nearest_scrolling_ancestor_viewport
.translate(-info.origin.to_vector());
node_to_parent_transform
(node_to_parent_transform, parent_to_node_transform)
},
SpatialTreeNodeInfo::Scroll(info) => {
sticky_info.nearest_scrolling_ancestor_viewport = info.clip_rect;
sticky_info.nearest_scrolling_ancestor_offset = -info.offset;
Transform3D::translation(-info.offset.x, -info.offset.y, 0.0)
(
Transform3D::translation(-info.offset.x, -info.offset.y, 0.0),
Some(Transform3D::translation(info.offset.x, info.offset.y, 0.0)),
)
},
SpatialTreeNodeInfo::Sticky(info) => {
let offset = info.calculate_sticky_offset(&sticky_info);
sticky_info.nearest_scrolling_ancestor_offset += offset;
Transform3D::translation(offset.x, offset.y, 0.0)
(
Transform3D::translation(offset.x, offset.y, 0.0),
Some(Transform3D::translation(-offset.x, -offset.y, 0.0)),
)
},
};
let node_to_root_transform =
node_to_parent_transform.then(&parent_transforms.node_to_root_transform);
let root_to_node_transform = parent_to_node_transform.map(|parent_to_node_transform| {
parent_transforms
.root_to_node_transform
.map_or(parent_to_node_transform, |parent_transform| {
parent_transform.then(&parent_to_node_transform)
})
});
let transforms = ScrollTreeNodeTransformationCache {
node_to_root_transform,
root_to_node_transform,
};
(transforms, sticky_info)
}

View file

@ -53,7 +53,7 @@ use style::properties::PropertyId;
use style::properties::style_structs::Font;
use style::selector_parser::{PseudoElement, RestyleDamage, Snapshot};
use style::stylesheets::Stylesheet;
use webrender_api::units::{DeviceIntSize, LayoutVector2D};
use webrender_api::units::{DeviceIntSize, LayoutPoint, LayoutVector2D};
use webrender_api::{ExternalScrollId, ImageKey};
pub trait GenericLayoutDataTrait: Any + MallocSizeOfTrait {
@ -266,11 +266,6 @@ pub trait Layout {
fn query_content_boxes(&self, node: TrustedNodeAddress) -> Vec<Rect<Au>>;
fn query_client_rect(&self, node: TrustedNodeAddress) -> Rect<i32>;
fn query_element_inner_outer_text(&self, node: TrustedNodeAddress) -> String;
fn query_nodes_from_point(
&self,
point: Point2D<f32>,
query_type: NodesFromPointQueryType,
) -> Vec<UntrustedNodeAddress>;
fn query_offset_parent(&self, node: TrustedNodeAddress) -> OffsetParentResponse;
fn query_resolved_style(
&self,
@ -289,6 +284,11 @@ pub trait Layout {
) -> Option<ServoArc<Font>>;
fn query_scrolling_area(&self, node: Option<TrustedNodeAddress>) -> Rect<i32>;
fn query_text_indext(&self, node: OpaqueNode, point: Point2D<f32>) -> Option<usize>;
fn query_elements_from_point(
&self,
point: LayoutPoint,
flags: ElementsFromPointFlags,
) -> Vec<ElementsFromPointResult>;
}
/// This trait is part of `layout_api` because it depends on both `script_traits`
@ -309,26 +309,21 @@ pub struct OffsetParentResponse {
pub rect: Rect<Au>,
}
#[derive(Debug, PartialEq)]
pub enum NodesFromPointQueryType {
All,
Topmost,
}
#[derive(Debug, PartialEq)]
pub enum QueryMsg {
ClientRectQuery,
ContentBox,
ContentBoxes,
ClientRectQuery,
ScrollingAreaOrOffsetQuery,
OffsetParentQuery,
TextIndexQuery,
NodesFromPointQuery,
ResolvedStyleQuery,
StyleQuery,
ElementInnerOuterTextQuery,
ResolvedFontStyleQuery,
ElementsFromPoint,
InnerWindowDimensionsQuery,
NodesFromPointQuery,
OffsetParentQuery,
ResolvedFontStyleQuery,
ResolvedStyleQuery,
ScrollingAreaOrOffsetQuery,
StyleQuery,
TextIndexQuery,
}
/// The goal of a reflow request.
@ -602,6 +597,25 @@ impl ImageAnimationState {
}
}
/// Describe an item that matched a hit-test query.
#[derive(Debug)]
pub struct ElementsFromPointResult {
/// An [`OpaqueNode`] that contains a pointer to the node hit by
/// this hit test result.
pub node: OpaqueNode,
/// The [`LayoutPoint`] of the original query point relative to the
/// node fragment rectangle.
pub point_in_target: LayoutPoint,
}
bitflags! {
pub struct ElementsFromPointFlags: u8 {
/// Whether or not to find all of the items for a hit test or stop at the
/// first hit.
const FindAll = 0b00000001;
}
}
#[cfg(test)]
mod test {
use std::sync::Arc;