diff --git a/Cargo.lock b/Cargo.lock index 1aed6e8e0db..fc4d12e6704 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4723,6 +4723,7 @@ dependencies = [ "icu_segmenter", "ipc-channel", "itertools 0.14.0", + "kurbo", "layout_api", "log", "malloc_size_of_derive", diff --git a/components/layout/Cargo.toml b/components/layout/Cargo.toml index cd0b5c55477..3270d5ffb26 100644 --- a/components/layout/Cargo.toml +++ b/components/layout/Cargo.toml @@ -34,6 +34,7 @@ icu_locid = { workspace = true } icu_segmenter = { workspace = true } ipc-channel = { workspace = true } itertools = { workspace = true } +kurbo = { workspace = true } layout_api = { workspace = true } log = { workspace = true } malloc_size_of = { workspace = true } diff --git a/components/layout/display_list/clip.rs b/components/layout/display_list/clip.rs index 11c0a2ce5fe..606ed6204da 100644 --- a/components/layout/display_list/clip.rs +++ b/components/layout/display_list/clip.rs @@ -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); 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, diff --git a/components/layout/display_list/hit_test.rs b/components/layout/display_list/hit_test.rs new file mode 100644 index 00000000000..ffdc31e069b --- /dev/null +++ b/components/layout/display_list/hit_test.rs @@ -0,0 +1,368 @@ +/* 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 euclid::{Box2D, Point2D, Point3D}; +use kurbo::{Ellipse, Shape}; +use layout_api::{ElementsFromPointFlags, ElementsFromPointResult}; +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 webrender_api::BorderRadius; +use webrender_api::units::{LayoutPoint, LayoutRect, LayoutSize, LayoutTransform, 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::{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, + /// A cache of hit test results for shared clip nodes. + clip_hit_test_results: HashMap, +} + +impl<'a> HitTest<'a> { + pub(crate) fn run( + stacking_context_tree: &'a StackingContextTree, + point_to_test: LayoutPoint, + flags: ElementsFromPointFlags, + ) -> Vec { + 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, LayoutTransform)> { + let transform = self + .stacking_context_tree + .compositor_info + .scroll_tree + .cumulative_root_to_node_transform(&scroll_tree_node_id)?; + + // This comes from WebRender at `webrender/utils.rs`. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1765204#c3. + // + // We are going from world coordinate space to spatial node coordinate space. + // Normally, that transformation happens in the opposite direction, but for hit + // testing everything is reversed. The result of the display transformation comes + // with a z-coordinate that we do not have access to here. + // + // We must solve for a value of z here that transforms to 0 (the value of z for our + // point). + let point = self.point_to_test; + let z = + -(point.x * transform.m13 + point.y * transform.m23 + transform.m43) / transform.m33; + let projected_point = transform.transform_point3d(Point3D::new(point.x, point.y, z))?; + + Some(( + Point2D::new(projected_point.x, projected_point.y), + 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, + ) -> bool { + let Some(tag) = self.tag() else { + return false; + }; + + let point_in_target; + let transform; + let hit = match self { + Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => { + (point_in_target, transform) = + 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, &transform) + }, + Fragment::Text(text) => { + let text = &*text.borrow(); + let style = text.inline_styles.style.borrow(); + if style.get_inherited_ui().pointer_events == PointerEvents::None { + return false; + } + if style.get_inherited_box().visibility != Visibility::Visible { + return false; + } + + (point_in_target, transform) = + match hit_test.location_in_spatial_node(spatial_node_id) { + Some(point) => point, + None => return false, + }; + + if style.get_box().backface_visibility == BackfaceVisibility::Hidden && + transform.is_backface_visible() + { + 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, + transform: &LayoutTransform, + ) -> bool { + if self.style.get_inherited_ui().pointer_events == PointerEvents::None { + return false; + } + if self.style.get_inherited_box().visibility != Visibility::Visible { + return false; + } + + if self.style.get_box().backface_visibility == BackfaceVisibility::Hidden && + transform.is_backface_visible() + { + return false; + } + + let border_rect = self + .border_rect() + .translate(containing_block.origin.to_vector()) + .to_webrender(); + rounded_rect_contains_point(border_rect, || self.border_radius(), point_in_fragment) + } +} + +fn rounded_rect_contains_point( + rect: LayoutRect, + border_radius: impl FnOnce() -> BorderRadius, + point: LayoutPoint, +) -> bool { + if !rect.contains(point) { + return false; + } + + let border_radius = border_radius(); + 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) +} diff --git a/components/layout/display_list/mod.rs b/components/layout/display_list/mod.rs index af58454317b..0d3fac1453c 100644 --- a/components/layout/display_list/mod.rs +++ b/components/layout/display_list/mod.rs @@ -41,8 +41,8 @@ use style::values::specified::ui::CursorKind; use style_traits::{CSSPixel as StyloCSSPixel, DevicePixel as StyloDevicePixel}; use webrender_api::units::{DeviceIntSize, DevicePixel, LayoutPixel, LayoutRect, LayoutSize}; use webrender_api::{ - self as wr, BorderDetails, BoxShadowClipMode, BuiltDisplayList, ClipChainId, ClipMode, - CommonItemProperties, ComplexClipRegion, NinePatchBorder, NinePatchBorderSource, + self as wr, BorderDetails, BorderRadius, BoxShadowClipMode, BuiltDisplayList, ClipChainId, + ClipMode, CommonItemProperties, ComplexClipRegion, NinePatchBorder, NinePatchBorderSource, PropertyBinding, SpatialId, SpatialTreeItemKey, units, }; use wr::units::LayoutVector2D; @@ -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(crate) use stacking_context::*; // webrender's `ItemTag` is private. @@ -989,35 +991,11 @@ impl<'a> BuilderForBoxFragment<'a> { let border_rect = fragment .border_rect() .translate(containing_block.origin.to_vector()); - - 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 = fragment.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 - }; - Self { fragment, containing_block, - border_rect: webrender_border_rect, - border_radius, + border_rect: border_rect.to_webrender(), + border_radius: fragment.border_radius(), margin_rect: OnceCell::new(), padding_rect: OnceCell::new(), content_rect: OnceCell::new(), @@ -1921,3 +1899,28 @@ pub(super) fn compute_margin_box_radius( ), } } + +impl BoxFragment { + fn border_radius(&self) -> BorderRadius { + let resolve = + |radius: &LengthPercentage, box_size: Au| radius.to_used_value(box_size).to_f32_px(); + + let border_rect = self.border_rect(); + 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 border = self.style.get_border(); + let mut radius = wr::BorderRadius { + top_left: corner(&border.border_top_left_radius), + top_right: corner(&border.border_top_right_radius), + bottom_right: corner(&border.border_bottom_right_radius), + bottom_left: corner(&border.border_bottom_left_radius), + }; + + normalize_radii(&border_rect.to_webrender(), &mut radius); + radius + } +} diff --git a/components/layout/display_list/stacking_context.rs b/components/layout/display_list/stacking_context.rs index 317dd7fa107..a2e6f762c7a 100644 --- a/components/layout/display_list/stacking_context.rs +++ b/components/layout/display_list/stacking_context.rs @@ -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, + pub(super) contents: Vec, /// 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, + pub(super) real_stacking_contexts_and_positioned_stacking_containers: Vec, /// Float stacking containers. /// Separate from real_stacking_contexts_or_positioned_stacking_containers /// because they should never be stolen by the parent stacking context. /// - float_stacking_containers: Vec, + pub(super) float_stacking_containers: Vec, /// 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. /// /// - atomic_inline_stacking_containers: Vec, + pub(super) atomic_inline_stacking_containers: Vec, /// Information gathered about the painting order, for [Self::debug_print]. debug_print_items: Option>>, @@ -472,7 +472,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) @@ -651,6 +651,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 reverse 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); diff --git a/components/layout/layout_impl.rs b/components/layout/layout_impl.rs index 7340127765c..bca7f7bee59 100644 --- a/components/layout/layout_impl.rs +++ b/components/layout/layout_impl.rs @@ -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,9 @@ use fonts_traits::StylesheetWebFontLoadFinishedCallback; use fxhash::FxHashMap; use ipc_channel::ipc::IpcSender; use layout_api::{ - IFrameSizes, Layout, LayoutConfig, LayoutDamage, LayoutFactory, NodesFromPointQueryType, - OffsetParentResponse, QueryMsg, ReflowGoal, ReflowPhasesRun, ReflowRequest, - ReflowRequestRestyle, ReflowResult, TrustedNodeAddress, + IFrameSizes, Layout, LayoutConfig, LayoutDamage, LayoutFactory, OffsetParentResponse, QueryMsg, + ReflowGoal, ReflowPhasesRun, ReflowRequest, ReflowRequestRestyle, ReflowResult, + TrustedNodeAddress, }; use log::{debug, error, warn}; use malloc_size_of::{MallocConditionalSizeOf, MallocSizeOf, MallocSizeOfOps}; @@ -75,11 +75,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 +294,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, - query_type: NodesFromPointQueryType, - ) -> Vec { - 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) }; @@ -405,6 +381,19 @@ impl Layout for LayoutThread { process_text_index_request(node, point_in_node) } + #[servo_tracing::instrument(skip_all)] + fn query_elements_from_point( + &self, + point: webrender_api::units::LayoutPoint, + flags: layout_api::ElementsFromPointFlags, + ) -> Vec { + self.stacking_context_tree + .borrow_mut() + .as_mut() + .map(|tree| HitTest::run(tree, point, flags)) + .unwrap_or_default() + } + fn exit_now(&mut self) {} fn collect_reports(&self, reports: &mut Vec, ops: &mut MallocSizeOfOps) { @@ -1486,7 +1475,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 | diff --git a/components/script/dom/documentorshadowroot.rs b/components/script/dom/documentorshadowroot.rs index 0e617712d34..18e4f93c2f4 100644 --- a/components/script/dom/documentorshadowroot.rs +++ b/components/script/dom/documentorshadowroot.rs @@ -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; @@ -136,15 +137,13 @@ impl DocumentOrShadowRoot { } } - pub(crate) fn nodes_from_point( + pub(crate) fn query_elements_from_point( &self, - client_point: &Point2D, - query_type: NodesFromPointQueryType, - ) -> Vec { - self.window.layout_reflow(QueryMsg::NodesFromPointQuery); - self.window - .layout() - .query_nodes_from_point(*client_point, query_type) + point: LayoutPoint, + flags: ElementsFromPointFlags, + ) -> Vec { + self.window.layout_reflow(QueryMsg::ElementsFromPoint); + self.window.layout().query_elements_from_point(point, flags) } #[allow(unsafe_code)] @@ -158,7 +157,6 @@ impl DocumentOrShadowRoot { ) -> Option> { 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 { @@ -170,11 +168,14 @@ impl DocumentOrShadowRoot { } match self - .nodes_from_point(point, NodesFromPointQueryType::Topmost) + .query_elements_from_point(LayoutPoint::new(x, y), ElementsFromPointFlags::empty()) .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::() @@ -205,7 +206,6 @@ impl DocumentOrShadowRoot { ) -> Vec> { 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 { @@ -218,11 +218,15 @@ impl DocumentOrShadowRoot { } // Step 1 and Step 3 - let nodes = self.nodes_from_point(point, NodesFromPointQueryType::All); + let nodes = + self.query_elements_from_point(LayoutPoint::new(x, y), ElementsFromPointFlags::FindAll); let mut elements: Vec> = 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::(node) }) .collect(); diff --git a/components/shared/compositing/display_list.rs b/components/shared/compositing/display_list.rs index 476f5c9aee0..57c3944165d 100644 --- a/components/shared/compositing/display_list.rs +++ b/components/shared/compositing/display_list.rs @@ -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, } #[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 { + 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) } diff --git a/components/shared/layout/lib.rs b/components/shared/layout/lib.rs index 478a2f15838..53ce56c6ca8 100644 --- a/components/shared/layout/lib.rs +++ b/components/shared/layout/lib.rs @@ -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>; fn query_client_rect(&self, node: TrustedNodeAddress) -> Rect; fn query_element_inner_outer_text(&self, node: TrustedNodeAddress) -> String; - fn query_nodes_from_point( - &self, - point: Point2D, - query_type: NodesFromPointQueryType, - ) -> Vec; fn query_offset_parent(&self, node: TrustedNodeAddress) -> OffsetParentResponse; fn query_resolved_style( &self, @@ -289,6 +284,11 @@ pub trait Layout { ) -> Option>; fn query_scrolling_area(&self, node: Option) -> Rect; fn query_text_indext(&self, node: OpaqueNode, point: Point2D) -> Option; + fn query_elements_from_point( + &self, + point: LayoutPoint, + flags: ElementsFromPointFlags, + ) -> Vec; } /// This trait is part of `layout_api` because it depends on both `script_traits` @@ -309,26 +309,21 @@ pub struct OffsetParentResponse { pub rect: Rect, } -#[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; diff --git a/tests/wpt/meta/css/css-transforms/transform-scale-hittest.html.ini b/tests/wpt/meta/css/css-transforms/transform-scale-hittest.html.ini deleted file mode 100644 index 3f9c5003739..00000000000 --- a/tests/wpt/meta/css/css-transforms/transform-scale-hittest.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[transform-scale-hittest.html] - [Hit test intersecting scaled box] - expected: FAIL