mirror of
https://github.com/servo/servo.git
synced 2025-08-06 22:15:33 +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
|
@ -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);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue