mirror of
https://github.com/servo/servo.git
synced 2025-08-06 06:00:15 +01:00
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:
parent
3e856cbf11
commit
11844ca5af
11 changed files with 539 additions and 113 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4723,6 +4723,7 @@ dependencies = [
|
|||
"icu_segmenter",
|
||||
"ipc-channel",
|
||||
"itertools 0.14.0",
|
||||
"kurbo",
|
||||
"layout_api",
|
||||
"log",
|
||||
"malloc_size_of_derive",
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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,
|
||||
|
|
368
components/layout/display_list/hit_test.rs
Normal file
368
components/layout/display_list/hit_test.rs
Normal file
|
@ -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<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, 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<Au>,
|
||||
) -> 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<Au>,
|
||||
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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>>>,
|
||||
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<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) };
|
||||
|
@ -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<layout_api::ElementsFromPointResult> {
|
||||
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<Report>, 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 |
|
||||
|
|
|
@ -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<f32>,
|
||||
query_type: NodesFromPointQueryType,
|
||||
) -> Vec<UntrustedNodeAddress> {
|
||||
self.window.layout_reflow(QueryMsg::NodesFromPointQuery);
|
||||
self.window
|
||||
.layout()
|
||||
.query_nodes_from_point(*client_point, query_type)
|
||||
point: LayoutPoint,
|
||||
flags: ElementsFromPointFlags,
|
||||
) -> Vec<ElementsFromPointResult> {
|
||||
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<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 {
|
||||
|
@ -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::<ShadowRoot>()
|
||||
|
@ -205,7 +206,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 {
|
||||
|
@ -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<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();
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
[transform-scale-hittest.html]
|
||||
[Hit test intersecting scaled box]
|
||||
expected: FAIL
|
Loading…
Add table
Add a link
Reference in a new issue