First hit testing stuff

This commit is contained in:
Martin Robinson 2025-07-31 15:06:48 +02:00
parent b18f5a1b89
commit 3537ddd95c
8 changed files with 497 additions and 82 deletions

View file

@ -16,7 +16,7 @@ use super::{BuilderForBoxFragment, compute_margin_box_radius, normalize_radii};
/// An identifier for a clip used during StackingContextTree construction. This is a simple index in
/// a [`ClipStore`]s vector of clips.
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) struct ClipId(pub usize);
impl ClipId {
@ -43,6 +43,10 @@ pub(crate) struct Clip {
pub(crate) struct StackingContextTreeClipStore(pub Vec<Clip>);
impl StackingContextTreeClipStore {
pub(super) fn get(&self, clip_id: ClipId) -> &Clip {
&self.0[clip_id.0]
}
pub(crate) fn add(
&mut self,
radii: webrender_api::BorderRadius,

View file

@ -0,0 +1,354 @@
/* 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 std::collections::HashMap;
use app_units::Au;
use base::id::ScrollTreeNodeId;
use layout_api::{ElementsFromPointFlags, ElementsFromPointResult};
use style::computed_values::pointer_events::T as PointerEvents;
use style::computed_values::visibility::T as Visibility;
use webrender_api::units::LayoutPoint;
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::{BoxFragment, Fragment};
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,
/// 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: HashMap<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,
stacking_context_tree,
results: Vec::new(),
clip_hit_test_results: HashMap::new(),
};
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(
&self,
scroll_tree_node_id: ScrollTreeNodeId,
) -> Option<LayoutPoint> {
self.stacking_context_tree
.compositor_info
.scroll_tree
.cumulative_root_to_node_transform(&scroll_tree_node_id)
.and_then(|transform| transform.transform_point2d(self.point_to_test))
}
}
impl Clip {
fn contains(&self, point: LayoutPoint) -> bool {
// TODO: Handle rounded rectangles.
self.rect.contains(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() {
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 point_in_target;
let hit = match self {
Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => {
point_in_target = match hit_test.location_in_spatial_node(spatial_node_id) {
Some(point) => point,
None => return false,
};
box_fragment
.borrow()
.hit_test(point_in_target, containing_block)
},
Fragment::Text(text) => {
let text = &*text.borrow();
let visibilty = text
.inline_styles
.style
.borrow()
.get_inherited_box()
.visibility;
if visibilty != Visibility::Visible {
return false;
}
point_in_target = match hit_test.location_in_spatial_node(spatial_node_id) {
Some(point) => point,
None => return false,
};
text.rect
.translate(containing_block.origin.to_vector())
.to_webrender()
.contains(point_in_target)
},
Fragment::AbsoluteOrFixedPositioned(_) |
Fragment::IFrame(_) |
Fragment::Image(_) |
Fragment::Positioning(_) => return false,
};
if !hit {
return false;
}
hit_test.results.push(ElementsFromPointResult {
node: tag.node,
point_in_target,
});
!hit_test.flags.contains(ElementsFromPointFlags::FindAll)
}
}
impl BoxFragment {
fn hit_test(
&self,
point_in_fragment: LayoutPoint,
containing_block: &PhysicalRect<Au>,
) -> bool {
if self.style.get_inherited_ui().pointer_events == PointerEvents::None {
return false;
}
let border_rect = self
.border_rect()
.translate(containing_block.origin.to_vector())
.to_webrender();
border_rect.contains(point_in_fragment)
// TODO: Rounded rectangles.
}
}
//pub(crate) fn hit_test_fragment_in_rect(
// &self,
// hit_test_location: LayoutPoint,
// hit_test_flags: &HitTestFlags,
// hit_test_result: &mut HitTestResult,
// border_rect: PhysicalRect<Au>,
// style: &ComputedValues,
//) -> bool {
// let inherited_ui = style.get_inherited_ui();
// if inherited_ui.pointer_events == PointerEvents::None {
// return false;
// }
// if let Some(tag) = self.tag() {
// // obtain border radius
// let webrender_border_rect = border_rect.to_webrender();
// let border_radius = {
// let resolve = |radius: &LengthPercentage, box_size: Au| {
// radius.to_used_value(box_size).to_f32_px()
// };
// let corner = |corner: &style::values::computed::BorderCornerRadius| {
// Size2D::new(
// resolve(&corner.0.width.0, border_rect.size.width),
// resolve(&corner.0.height.0, border_rect.size.height),
// )
// };
// let b = style.get_border();
// let mut radius = wr::BorderRadius {
// top_left: corner(&b.border_top_left_radius),
// top_right: corner(&b.border_top_right_radius),
// bottom_right: corner(&b.border_bottom_right_radius),
// bottom_left: corner(&b.border_bottom_left_radius),
// };
//
// normalize_radii(&webrender_border_rect, &mut radius);
// radius
// };
// if border_radius.is_zero() {
// // if not has border radius hit test in rect.
// if webrender_border_rect.contains(hit_test_location) {
// let point_in_target = hit_test_location - webrender_border_rect.min.to_vector();
// debug!(
// "Fragment::hit_test_fragment_in_rect true point_in_target:{point_in_target:?}"
// );
// hit_test_result.items.push(HitTestResultItem {
// opaque_node: tag.node,
// point_in_target,
// });
// return !hit_test_flags.contains(HitTestFlags::FIND_ALL);
// }
// } else {
// // if has border radius, handle clip region.
// if rounded_rectangle_contains_point(
// &hit_test_location,
// &webrender_border_rect,
// &border_radius,
// ) {
// let point_in_target = hit_test_location - webrender_border_rect.min.to_vector();
// debug!(
// "Fragment::hit_test_fragment_in_rect true point_in_target:{point_in_target:?}"
// );
// hit_test_result.items.push(HitTestResultItem {
// opaque_node: tag.node,
// point_in_target,
// });
// return !hit_test_flags.contains(HitTestFlags::FIND_ALL);
// }
// }
// }
// false
//}

View file

@ -65,9 +65,11 @@ mod background;
mod clip;
mod conversions;
mod gradient;
mod hit_test;
mod stacking_context;
use background::BackgroundPainter;
pub(crate) use hit_test::HitTest;
pub use stacking_context::*;
// webrender's `ItemTag` is private.

View file

@ -293,7 +293,7 @@ pub(crate) enum StackingContextContent {
}
impl StackingContextContent {
fn section(&self) -> StackingContextSection {
pub(crate) fn section(&self) -> StackingContextSection {
match self {
Self::Fragment { section, .. } => *section,
Self::AtomicInlineStackingContainer { .. } => StackingContextSection::Foreground,
@ -365,7 +365,7 @@ pub struct StackingContext {
context_type: StackingContextType,
/// The contents that need to be painted in fragment order.
contents: Vec<StackingContextContent>,
pub(super) contents: Vec<StackingContextContent>,
/// Stacking contexts that need to be stolen by the parent stacking context
/// if this is a stacking container, that is, real stacking contexts and
@ -376,13 +376,13 @@ pub struct StackingContext {
/// > if it created a new stacking context, but omitting any positioned
/// > descendants or descendants that actually create a stacking context
/// > (letting the parent stacking context paint them, instead).
real_stacking_contexts_and_positioned_stacking_containers: Vec<StackingContext>,
pub(super) real_stacking_contexts_and_positioned_stacking_containers: Vec<StackingContext>,
/// Float stacking containers.
/// Separate from real_stacking_contexts_or_positioned_stacking_containers
/// because they should never be stolen by the parent stacking context.
/// <https://drafts.csswg.org/css-position-4/#paint-a-stacking-container>
float_stacking_containers: Vec<StackingContext>,
pub(super) float_stacking_containers: Vec<StackingContext>,
/// Atomic inline stacking containers.
/// Separate from real_stacking_contexts_or_positioned_stacking_containers
@ -391,7 +391,7 @@ pub struct StackingContext {
/// can index into this vec to paint them in fragment order.
/// <https://drafts.csswg.org/css-position-4/#paint-a-stacking-container>
/// <https://drafts.csswg.org/css-position-4/#paint-a-box-in-a-line-box>
atomic_inline_stacking_containers: Vec<StackingContext>,
pub(super) atomic_inline_stacking_containers: Vec<StackingContext>,
/// Information gathered about the painting order, for [Self::debug_print].
debug_print_items: Option<RefCell<Vec<DebugPrintItem>>>,
@ -473,7 +473,7 @@ impl StackingContext {
.push(stacking_context)
}
fn z_index(&self) -> i32 {
pub(crate) fn z_index(&self) -> i32 {
self.initializing_fragment.as_ref().map_or(0, |fragment| {
let fragment = fragment.borrow();
fragment.style.effective_z_index(fragment.base.flags)
@ -652,6 +652,9 @@ impl StackingContext {
fragment_builder.build_background_image(builder, &painter);
}
/// Build a display list from a a [`StackingContext`]. Note that this is the forward
/// version of the reversed stacking context walk algorithm in `hit_test.rs`. Any
/// changes made here should be reflected in the forward version in that file.
pub(crate) fn build_display_list(&self, builder: &mut DisplayListBuilder) {
let pushed_context = self.push_webrender_stacking_context_if_necessary(builder);

View file

@ -17,7 +17,7 @@ use base::id::{PipelineId, WebViewId};
use bitflags::bitflags;
use compositing_traits::CrossProcessCompositorApi;
use compositing_traits::display_list::ScrollType;
use embedder_traits::{Theme, UntrustedNodeAddress, ViewportDetails};
use embedder_traits::{Theme, ViewportDetails};
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect};
use euclid::{Point2D, Scale, Size2D};
use fnv::FnvHashMap;
@ -26,9 +26,8 @@ use fonts_traits::StylesheetWebFontLoadFinishedCallback;
use fxhash::FxHashMap;
use ipc_channel::ipc::IpcSender;
use layout_api::{
IFrameSizes, Layout, LayoutConfig, LayoutDamage, LayoutFactory, NodesFromPointQueryType,
OffsetParentResponse, QueryMsg, ReflowGoal, ReflowRequest, ReflowRequestRestyle, ReflowResult,
TrustedNodeAddress,
IFrameSizes, Layout, LayoutConfig, LayoutDamage, LayoutFactory, OffsetParentResponse, QueryMsg,
ReflowGoal, ReflowRequest, ReflowRequestRestyle, ReflowResult, TrustedNodeAddress,
};
use log::{debug, error, warn};
use malloc_size_of::{MallocConditionalSizeOf, MallocSizeOf, MallocSizeOfOps};
@ -75,11 +74,11 @@ use style::{Zero, driver};
use style_traits::{CSSPixel, SpeculativePainter};
use stylo_atoms::Atom;
use url::Url;
use webrender_api::units::{DevicePixel, DevicePoint, LayoutVector2D};
use webrender_api::{ExternalScrollId, HitTestFlags};
use webrender_api::ExternalScrollId;
use webrender_api::units::{DevicePixel, LayoutVector2D};
use crate::context::{CachedImageOrError, ImageResolver, LayoutContext};
use crate::display_list::{DisplayListBuilder, StackingContextTree};
use crate::display_list::{DisplayListBuilder, HitTest, StackingContextTree};
use crate::query::{
get_the_text_steps, process_client_rect_request, process_content_box_request,
process_content_boxes_request, process_node_scroll_area_request, process_offset_parent_query,
@ -294,30 +293,6 @@ impl Layout for LayoutThread {
let node = unsafe { ServoLayoutNode::new(&node) };
get_the_text_steps(node)
}
#[servo_tracing::instrument(skip_all)]
fn query_nodes_from_point(
&self,
point: UntypedPoint2D<f32>,
query_type: NodesFromPointQueryType,
) -> Vec<UntrustedNodeAddress> {
let mut flags = match query_type {
NodesFromPointQueryType::Topmost => HitTestFlags::empty(),
NodesFromPointQueryType::All => HitTestFlags::FIND_ALL,
};
// The point we get is not relative to the entire WebRender scene, but to this
// particular pipeline, so we need to tell WebRender about that.
flags.insert(HitTestFlags::POINT_RELATIVE_TO_PIPELINE_VIEWPORT);
let client_point = DevicePoint::from_untyped(point);
let results = self
.compositor_api
.hit_test(Some(self.id.into()), client_point, flags);
results.iter().map(|result| result.node).collect()
}
#[servo_tracing::instrument(skip_all)]
fn query_offset_parent(&self, node: TrustedNodeAddress) -> OffsetParentResponse {
let node = unsafe { ServoLayoutNode::new(&node) };
@ -495,6 +470,19 @@ impl Layout for LayoutThread {
.as_mut()
.and_then(|tree| tree.compositor_info.scroll_tree.scroll_offset(id))
}
#[servo_tracing::instrument(skip_all)]
fn query_elements_from_point(
&self,
point: webrender_api::units::LayoutPoint,
flags: layout_api::ElementsFromPointFlags,
) -> Vec<layout_api::ElementsFromPointResult> {
self.stacking_context_tree
.borrow_mut()
.as_mut()
.map(|tree| HitTest::run(tree, point, flags))
.unwrap_or_default()
}
}
impl LayoutThread {
@ -1452,7 +1440,8 @@ impl ReflowPhases {
QueryMsg::ContentBox |
QueryMsg::ContentBoxes |
QueryMsg::ResolvedStyleQuery |
QueryMsg::ScrollingAreaOrOffsetQuery => Self::StackingContextTreeConstruction,
QueryMsg::ScrollingAreaOrOffsetQuery |
QueryMsg::ElementsFromPoint => Self::StackingContextTreeConstruction,
QueryMsg::ClientRectQuery |
QueryMsg::ElementInnerOuterTextQuery |
QueryMsg::InnerWindowDimensionsQuery |

View file

@ -3,12 +3,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::collections::HashSet;
use std::ffi::c_void;
use std::fmt;
use embedder_traits::UntrustedNodeAddress;
use euclid::default::Point2D;
use js::rust::HandleValue;
use layout_api::{NodesFromPointQueryType, QueryMsg};
use layout_api::{ElementsFromPointFlags, ElementsFromPointResult, QueryMsg};
use script_bindings::error::{Error, ErrorResult};
use script_bindings::script_runtime::JSContext;
use servo_arc::Arc;
@ -19,6 +19,7 @@ use style::shared_lock::{SharedRwLock as StyleSharedRwLock, SharedRwLockReadGuar
use style::stylesheets::scope_rule::ImplicitScopeRoot;
use style::stylesheets::{Stylesheet, StylesheetContents};
use stylo_atoms::Atom;
use webrender_api::units::LayoutPoint;
use super::bindings::trace::HashMapTracedValues;
use crate::dom::bindings::cell::DomRefCell;
@ -137,17 +138,15 @@ impl DocumentOrShadowRoot {
}
}
pub(crate) fn nodes_from_point(
pub(crate) fn query_elements_from_point(
&self,
client_point: &Point2D<f32>,
query_type: NodesFromPointQueryType,
point: LayoutPoint,
flags: ElementsFromPointFlags,
can_gc: CanGc,
) -> Vec<UntrustedNodeAddress> {
) -> Vec<ElementsFromPointResult> {
self.window
.layout_reflow(QueryMsg::NodesFromPointQuery, can_gc);
self.window
.layout()
.query_nodes_from_point(*client_point, query_type)
.layout_reflow(QueryMsg::ElementsFromPoint, can_gc);
self.window.layout().query_elements_from_point(point, flags)
}
#[allow(unsafe_code)]
@ -162,7 +161,6 @@ impl DocumentOrShadowRoot {
) -> Option<DomRoot<Element>> {
let x = *x as f32;
let y = *y as f32;
let point = &Point2D::new(x, y);
let viewport = self.window.viewport_details().size;
if !has_browsing_context {
@ -174,11 +172,18 @@ impl DocumentOrShadowRoot {
}
match self
.nodes_from_point(point, NodesFromPointQueryType::Topmost, can_gc)
.query_elements_from_point(
LayoutPoint::new(x, y),
ElementsFromPointFlags::empty(),
can_gc,
)
.first()
{
Some(address) => {
let node = unsafe { node::from_untrusted_node_address(*address) };
Some(result) => {
// SAFETY: This is safe because `Self::query_elements_from_point` has ensured that
// layout has run and any OpaqueNodes that no longer refer to real nodes are gone.
let address = UntrustedNodeAddress(result.node.0 as *const c_void);
let node = unsafe { node::from_untrusted_node_address(address) };
let parent_node = node.GetParentNode().unwrap();
let shadow_host = parent_node
.downcast::<ShadowRoot>()
@ -210,7 +215,6 @@ impl DocumentOrShadowRoot {
) -> Vec<DomRoot<Element>> {
let x = *x as f32;
let y = *y as f32;
let point = &Point2D::new(x, y);
let viewport = self.window.viewport_details().size;
if !has_browsing_context {
@ -223,11 +227,18 @@ impl DocumentOrShadowRoot {
}
// Step 1 and Step 3
let nodes = self.nodes_from_point(point, NodesFromPointQueryType::All, can_gc);
let nodes = self.query_elements_from_point(
LayoutPoint::new(x, y),
ElementsFromPointFlags::FindAll,
can_gc,
);
let mut elements: Vec<DomRoot<Element>> = nodes
.iter()
.flat_map(|&untrusted_node_address| {
let node = unsafe { node::from_untrusted_node_address(untrusted_node_address) };
.flat_map(|result| {
// SAFETY: This is safe because `Self::query_elements_from_point` has ensured that
// layout has run and any OpaqueNodes that no longer refer to real nodes are gone.
let address = UntrustedNodeAddress(result.node.0 as *const c_void);
let node = unsafe { node::from_untrusted_node_address(address) };
DomRoot::downcast::<Element>(node)
})
.collect();

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,49 @@ impl ScrollTree {
info.frame_origin_for_query.y,
);
let parent_to_node_transform = info.transform.inverse().map(|inverse_transform| {
change_basis(&inverse_transform, -info.origin.x, info.origin.y)
});
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 {
@ -263,11 +263,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,
@ -286,6 +281,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`
@ -306,26 +306,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.
@ -584,6 +579,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;