script: Partially implement IntersectionObserver compute the intersection algo (#42204)

Depends on #42251 for the overflow query.

Use containing block queries to implement the iteration of step [3.2.7.
Compute the Intersection of a Target Element and the
Root](https://w3c.github.io/IntersectionObserver/#compute-the-intersection)
within `IntersectionObserver` algorithm.

Contrary to the algorithm in the spec that maps the coordinate space of
the element for each iteration, we uses bounding client rect queries to
get the appropriate clip rect. And, to handle the mapping between
different coordinate spaces of iframes, we use a simple translation.

Due to the platform limitation, currently it would only handle the same
`ScriptThread` browsing context. Therefore innaccuracy any usage with
cross origin iframes is expected.

Testing: Existing and new WPTs.
Part of: https://github.com/servo/servo/issues/35767

Co-authored-by: Josh Matthews
[josh@joshmatthews.net](josh@joshmatthews.net)

---------

Signed-off-by: Josh Matthews <josh@joshmatthews.net>
Signed-off-by: Jo Steven Novaryo <steven.novaryo@gmail.com>
Co-authored-by: Josh Matthews <josh@joshmatthews.net>
This commit is contained in:
Steven Novaryo
2026-04-16 17:04:53 +08:00
committed by GitHub
parent 6c14aed993
commit 02ba2eaa62
30 changed files with 412 additions and 217 deletions

View File

@@ -13,7 +13,9 @@ use std::sync::{Arc, LazyLock};
use app_units::Au;
use bitflags::bitflags;
use embedder_traits::{EmbedderMsg, ScriptToEmbedderChan, Theme, ViewportDetails};
use embedder_traits::{
EmbedderMsg, ScriptToEmbedderChan, Theme, UntrustedNodeAddress, ViewportDetails,
};
use euclid::{Point2D, Rect, Scale, Size2D};
use fonts::{FontContext, FontContextWebFontMethods, WebFontDocumentContext};
use fonts_traits::StylesheetWebFontLoadFinishedCallback;
@@ -86,10 +88,11 @@ use crate::context::{CachedImageOrError, ImageResolver, LayoutContext};
use crate::display_list::{DisplayListBuilder, HitTest, PaintTimingHandler, StackingContextTree};
use crate::query::{
find_character_offset_in_fragment_descendants, get_the_text_steps, process_box_area_request,
process_box_areas_request, process_client_rect_request, process_current_css_zoom_query,
process_effective_overflow_query, process_node_scroll_area_request,
process_offset_parent_query, process_padding_request, process_resolved_font_style_query,
process_resolved_style_request, process_scroll_container_query,
process_box_areas_request, process_client_rect_request, process_containing_block_query,
process_current_css_zoom_query, process_effective_overflow_query,
process_node_scroll_area_request, process_offset_parent_query, process_padding_request,
process_resolved_font_style_query, process_resolved_style_request,
process_scroll_container_query,
};
use crate::traversal::{RecalcStyle, compute_damage_and_rebuild_box_tree};
use crate::{BoxTree, FragmentTree};
@@ -317,6 +320,15 @@ impl Layout for LayoutThread {
resolved_images_cache.remove(url);
}
/// Return the node corresponding to the containing block of the provided node.
#[servo_tracing::instrument(skip_all)]
fn query_containing_block(&self, node: TrustedNodeAddress) -> Option<UntrustedNodeAddress> {
with_layout_state(|| {
let node = unsafe { ServoLayoutNode::new(&node) };
process_containing_block_query(node)
})
}
/// Return the resolved values of this node's padding rect.
#[servo_tracing::instrument(skip_all)]
fn query_padding(&self, node: TrustedNodeAddress) -> Option<PhysicalSides> {

View File

@@ -6,6 +6,7 @@
use std::rc::Rc;
use app_units::Au;
use embedder_traits::UntrustedNodeAddress;
use euclid::{Point2D, Rect, SideOffsets2D, Size2D};
use itertools::Itertools;
use layout_api::{
@@ -737,6 +738,57 @@ pub fn process_offset_parent_query(
})
}
fn style_and_flags_for_node(
node: &ServoLayoutNode,
) -> Option<(ServoArc<ComputedValues>, FragmentFlags)> {
let layout_data = node.inner_layout_data()?;
let layout_box = layout_data.self_box.borrow();
let layout_box = layout_box.as_ref()?;
layout_box.with_base(|base| (base.style.clone(), base.base_fragment_info.flags))
}
fn is_containing_block_for_position(
position: Position,
ancestor_style: &ServoArc<ComputedValues>,
ancestor_flags: FragmentFlags,
) -> bool {
match position {
Position::Static | Position::Relative | Position::Sticky => {
!ancestor_style.is_inline_box(ancestor_flags)
},
Position::Absolute => {
ancestor_style.establishes_containing_block_for_absolute_descendants(ancestor_flags)
},
Position::Fixed => {
ancestor_style.establishes_containing_block_for_all_descendants(ancestor_flags)
},
}
}
fn containing_block_for_node<'a>(node: ServoLayoutNode<'a>) -> Option<ServoLayoutNode<'a>> {
let (style, _flags) = style_and_flags_for_node(&node)?;
let mut current_position_value = style.clone_position();
let mut current_ancestor = node;
#[expect(unsafe_code)]
while let Some(ancestor) = unsafe { current_ancestor.dangerous_flat_tree_parent() } {
let Some((ancestor_style, ancestor_flags)) = style_and_flags_for_node(&ancestor) else {
continue;
};
if is_containing_block_for_position(current_position_value, &ancestor_style, ancestor_flags)
{
return Some(ancestor);
}
current_position_value = ancestor_style.clone_position();
current_ancestor = ancestor;
}
None
}
/// An implementation of `scrollParent` that can also be used to for `scrollIntoView`:
/// <https://drafts.csswg.org/cssom-view/#dom-htmlelement-scrollparent>.
///
@@ -750,15 +802,9 @@ pub(crate) fn process_scroll_container_query(
return Some(ScrollContainerResponse::Viewport(viewport_overflow));
};
let layout_data = node.inner_layout_data()?;
// 1. If any of the following holds true, return null and terminate this algorithm:
// - The element does not have an associated box.
let layout_box = layout_data.self_box.borrow();
let layout_box = layout_box.as_ref()?;
let (style, flags) =
layout_box.with_base(|base| (base.style.clone(), base.base_fragment_info.flags))?;
let (style, flags) = style_and_flags_for_node(&node)?;
// - The element is the root element.
// - The element is the body element.
@@ -808,32 +854,15 @@ pub(crate) fn process_scroll_container_query(
while let Some(ancestor) = unsafe { current_ancestor.dangerous_flat_tree_parent() } {
current_ancestor = ancestor;
let Some(layout_data) = ancestor.inner_layout_data() else {
continue;
};
let ancestor_layout_box = layout_data.self_box.borrow();
let Some(ancestor_layout_box) = ancestor_layout_box.as_ref() else {
let Some((ancestor_style, ancestor_flags)) = style_and_flags_for_node(&ancestor) else {
continue;
};
let Some((ancestor_style, ancestor_flags)) = ancestor_layout_box
.with_base(|base| (base.style.clone(), base.base_fragment_info.flags))
else {
continue;
};
let is_containing_block = match current_position_value {
Position::Static | Position::Relative | Position::Sticky => {
!ancestor_style.is_inline_box(ancestor_flags)
},
Position::Absolute => {
ancestor_style.establishes_containing_block_for_absolute_descendants(ancestor_flags)
},
Position::Fixed => {
ancestor_style.establishes_containing_block_for_all_descendants(ancestor_flags)
},
};
if !is_containing_block {
if !is_containing_block_for_position(
current_position_value,
&ancestor_style,
ancestor_flags,
) {
continue;
}
@@ -1396,6 +1425,11 @@ pub fn find_character_offset_in_fragment_descendants(
})
}
pub fn process_containing_block_query(node: ServoLayoutNode) -> Option<UntrustedNodeAddress> {
let containing_block = containing_block_for_node(node);
containing_block.map(|node| node.opaque().into())
}
pub fn process_resolved_font_style_query<'dom, E>(
context: &SharedStyleContext,
node: E,

View File

@@ -3146,7 +3146,7 @@ impl Document {
) {
// Step 1
// > Let rootBounds be observers root intersection rectangle.
let root_bounds = intersection_observer.root_intersection_rectangle(self);
let root_bounds = intersection_observer.root_intersection_rectangle();
// Step 2
// > For each target in observers internal [[ObservationTargets]] slot,

View File

@@ -9,10 +9,10 @@ use std::time::Duration;
use app_units::Au;
use cssparser::{Parser, ParserInput};
use dom_struct::dom_struct;
use euclid::{Rect, SideOffsets2D, Size2D};
use euclid::{Rect, SideOffsets2D, Size2D, Vector2D};
use js::rust::{HandleObject, MutableHandleValue};
use layout_api::BoxAreaType;
use servo_base::cross_process_instant::CrossProcessInstant;
use servo_geometry::f32_rect_to_au_rect;
use style::parser::Parse;
use style::stylesheets::CssRuleType;
use style::values::computed::Overflow;
@@ -245,19 +245,6 @@ impl IntersectionObserver {
Ok(())
}
/// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-implicit-root>
fn root_is_implicit_root(&self) -> bool {
self.root.is_none()
}
/// Return unwrapped root if it was an element, None if otherwise.
fn maybe_element_root(&self) -> Option<&Element> {
match &self.root {
Some(ElementOrDocument::Element(element)) => Some(element),
_ => None,
}
}
/// <https://w3c.github.io/IntersectionObserver/#observe-target-element>
fn observe_target_element(&self, target: &Element) {
// Step 1
@@ -421,69 +408,48 @@ impl IntersectionObserver {
}
}
/// <https://w3c.github.io/IntersectionObserver/#ref-for-intersectionobserver-content-clip>
/// An Element is defined as having a content clip if its computed style has overflow properties
/// that cause its content to be clipped to the elements padding edge.
// TODO: this is not clear for `overflow: clip` since it is clipped based on overflow clip rect.
fn has_content_clip(element: &Element) -> bool {
element
.upcast::<Node>()
.effective_overflow_without_reflow()
.is_some_and(|overflow_axes| {
overflow_axes.x != Overflow::Visible || overflow_axes.y != Overflow::Visible
})
}
/// > The root intersection rectangle for an IntersectionObserver is
/// > the rectangle well use to check against the targets.
///
/// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle>
pub(crate) fn root_intersection_rectangle(
&self,
document: &Document,
) -> Option<Rect<Au, CSSPixel>> {
let window = document.window();
let intersection_rectangle = match &self.root {
pub(crate) fn root_intersection_rectangle(&self) -> Option<Rect<Au, CSSPixel>> {
let intersection_rectangle = match self.concrete_root() {
// Handle if root is an element.
Some(ElementOrDocument::Element(element)) => {
// TODO: recheck scrollbar approach and clip-path clipping from Chromium implementation.
if element
.upcast::<Node>()
.effective_overflow_without_reflow()
.is_some_and(|overflow_axes| {
overflow_axes.x != Overflow::Visible || overflow_axes.y != Overflow::Visible
})
{
if IntersectionObserver::has_content_clip(&element) {
// > Otherwise, if the intersection root has a content clip, its the elements padding area.
window.box_area_query_without_reflow(
&DomRoot::upcast::<Node>(element.clone()),
BoxAreaType::Padding,
false,
)
element.upcast::<Node>().padding_box_without_reflow()
} else {
// > Otherwise, its the result of getting the bounding box for the intersection root.
window.box_area_query_without_reflow(
&DomRoot::upcast::<Node>(element.clone()),
BoxAreaType::Border,
false,
)
element.upcast::<Node>().border_box_without_reflow()
}
},
// Handle if root is a Document, which includes implicit root and explicit Document root.
_ => {
let document = if self.root.is_none() {
// > If the IntersectionObserver is an implicit root observer,
// > its treated as if the root were the top-level browsing contexts document,
// > according to the following rule for document.
//
// There are uncertainties whether the browsing context we should consider is the browsing
// context of the target or observer. <https://github.com/w3c/IntersectionObserver/issues/456>
// TODO: This wouldn't work if top level document is in another ScriptThread.
document.window().top_level_document_if_local()
} else if let Some(ElementOrDocument::Document(document)) = &self.root {
Some(document.clone())
} else {
None
};
Some(ElementOrDocument::Document(document)) => {
// > If the intersection root is a document, its the size of the document's viewport
// > (note that this processing step can only be reached if the document is fully active).
// TODO: viewport should consider native scrollbar if exist. Recheck Servo's scrollbar approach.
document.map(|document| {
let viewport = document.window().viewport_details().size;
Rect::from_size(Size2D::new(
Au::from_f32_px(viewport.width),
Au::from_f32_px(viewport.height),
))
})
let viewport = document.window().viewport_details().size;
Some(Rect::from_size(Size2D::new(
Au::from_f32_px(viewport.width),
Au::from_f32_px(viewport.height),
)))
},
None => None,
};
// > When calculating the root intersection rectangle for a same-origin-domain target,
@@ -494,11 +460,28 @@ impl IntersectionObserver {
// > the width of the undilated rectangle.
// TODO(stevennovaryo): add check for same-origin-domain
intersection_rectangle.map(|intersection_rectangle| {
let margin = self.resolve_percentages_with_basis(intersection_rectangle);
let margin = Self::resolve_percentages_with_basis(
&self.root_margin.borrow(),
intersection_rectangle,
);
intersection_rectangle.outer_rect(margin)
})
}
/// Return root or try to get the top-level browsing context document in case if this is a implicit root.
/// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-intersection-root>
// TODO: Currently we are unable to get the cross `ScriptThread` document.
fn concrete_root(&self) -> Option<ElementOrDocument> {
match &self.root {
Some(root) => Some(root.clone()),
None => self
.owner_doc
.window()
.top_level_document_if_local()
.map(ElementOrDocument::Document),
}
}
/// Step 2.2.4-2.2.21 of <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>
///
/// If some conditions require to skips "processing further", we will skips those steps and
@@ -509,36 +492,42 @@ impl IntersectionObserver {
/// <https://www.w3.org/TR/intersection-observer/>
fn maybe_compute_intersection_output(
&self,
document: &Document,
target: &Element,
maybe_root_bounds: Option<Rect<Au, CSSPixel>>,
) -> IntersectionObservationOutput {
// Step 5
// > If the intersection root is not the implicit root, and target is not in
// > the same document as the intersection root, skip to step 11.
if !self.root_is_implicit_root() && *target.owner_document() != *document {
return IntersectionObservationOutput::default_skipped();
}
// Step 6
// > If the intersection root is an Element, and target is not a descendant of
// > the intersection root in the containing block chain, skip to step 11.
// TODO(stevennovaryo): implement LayoutThread query that support this.
if let Some(_element) = self.maybe_element_root() {
debug!("descendant of containing block chain is not implemented");
match &self.root {
Some(ElementOrDocument::Document(document)) => {
if document != &target.owner_document() {
return IntersectionObservationOutput::default_skipped();
}
},
Some(ElementOrDocument::Element(element)) => {
// To ensure consistency, we also check for elements right now, but we can depend on the
// layout query later.
if element.owner_document() != target.owner_document() {
return IntersectionObservationOutput::default_skipped();
}
// TODO(stevennovaryo): implement LayoutThread query for descendant of containing block chain.
debug!("descendant of containing block chain is not implemented");
},
_ => {},
}
// Step 7
// > Set targetRect to the DOMRectReadOnly obtained by getting the bounding box for target.
let maybe_target_rect = document.window().box_area_query_without_reflow(
target.upcast::<Node>(),
BoxAreaType::Border,
false,
);
let maybe_target_rect = target.upcast::<Node>().border_box_without_reflow();
// Following the implementation of Gecko, we will skip further processing if these
// information not available. This would also handle display none element.
let (Some(root_bounds), Some(target_rect)) = (maybe_root_bounds, maybe_target_rect) else {
let (Some(root_bounds), Some(target_rect), Some(root_intersection)) =
(maybe_root_bounds, maybe_target_rect, self.concrete_root())
else {
return IntersectionObservationOutput::default_skipped();
};
@@ -548,8 +537,14 @@ impl IntersectionObserver {
// Step 8
// > Let intersectionRect be the result of running the compute the intersection algorithm on
// > target and observers intersection root.
let intersection_rect =
compute_the_intersection(document, target, &self.root, root_bounds, target_rect);
let maybe_intersection_rect = compute_the_intersection(
target,
&root_intersection,
root_bounds,
target_rect,
&self.scroll_margin.borrow(),
);
let intersection_rect = maybe_intersection_rect.unwrap_or_default();
// Step 9
// > Let targetArea be targetRects area.
@@ -565,10 +560,7 @@ impl IntersectionObserver {
// we are checking whether the rectangle is negative or not.
// TODO(stevennovaryo): there is a dicussion regarding isIntersecting definition, we should update
// it accordingly. https://github.com/w3c/IntersectionObserver/issues/432
let is_intersecting = !target_rect
.to_box2d()
.intersection_unchecked(&root_bounds.to_box2d())
.is_negative();
let is_intersecting = maybe_intersection_rect.is_some();
// Step 12
// > If targetArea is non-zero, let intersectionRatio be intersectionArea divided by targetArea.
@@ -635,8 +627,7 @@ impl IntersectionObserver {
registration.last_update_time.set(time);
// step 4-14
let intersection_output =
self.maybe_compute_intersection_output(document, target, root_bounds);
let intersection_output = self.maybe_compute_intersection_output(target, root_bounds);
// Step 15-17
// > 15. Let previousThresholdIndex be the registrations previousThresholdIndex property.
@@ -689,10 +680,10 @@ impl IntersectionObserver {
}
fn resolve_percentages_with_basis(
&self,
margin: &IntersectionObserverMargin,
containing_block: Rect<Au, CSSPixel>,
) -> SideOffsets2D<Au, CSSPixel> {
let inner = &self.root_margin.borrow().0;
let inner = &margin.0;
SideOffsets2D::new(
inner.0.to_used_value(containing_block.height()),
inner.1.to_used_value(containing_block.width()),
@@ -857,56 +848,153 @@ fn parse_a_margin(value: Option<&DOMString>) -> Result<IntersectionObserverMargi
.map_err(|_| ())
}
/// In terms of intersection observer, we consider zero-area rectangles as long as the area is not negative.
fn intersect_rectangle(
lhs: &Rect<Au, CSSPixel>,
rhs: &Rect<Au, CSSPixel>,
) -> Option<Rect<Au, CSSPixel>> {
let box_result = lhs.to_box2d().intersection_unchecked(&rhs.to_box2d());
if box_result.is_negative() {
None
} else {
Some(box_result.to_rect())
}
}
/// Compute the intersection rectangle of the target [`Element`] returning the results of intersection in the coordinate
/// space of the target's owning [`Document`]. Additionally, we assume that both the target and the root is connected.
/// <https://w3c.github.io/IntersectionObserver/#compute-the-intersection>
fn compute_the_intersection(
_document: &Document,
_target: &Element,
_root: &IntersectionRoot,
target: &Element,
root: &ElementOrDocument,
root_bounds: Rect<Au, CSSPixel>,
mut intersection_rect: Rect<Au, CSSPixel>,
) -> Rect<Au, CSSPixel> {
scroll_margin: &IntersectionObserverMargin,
) -> Option<Rect<Au, CSSPixel>> {
// > 1. Let intersectionRect be the result of getting the bounding box for target.
// We had delegated the computation of this to the caller of the function.
// > 2. Let container be the containing block of target.
let mut container = match target
.upcast::<Node>()
.containing_block_node_without_reflow()
{
Some(node) => ElementOrDocument::Element(DomRoot::downcast(node).unwrap()),
None => ElementOrDocument::Document(target.owner_document()),
};
// Total offsets gained from traversing through multiple navigables. We use this to map the coordinate space.
// TODO: We should store the product sum of transformation matrices instead. But this should be enough to handle
// scrolling, simple translation, and offset from containing block.
let mut total_inter_document_offset = Vector2D::zero();
// > 3. While container is not root:
// > 1. If container is the document of a nested browsing context, update intersectionRect
// > by clipping to the viewport of the document,
// > and update container to be the browsing context container of container.
// > 2. Map intersectionRect to the coordinate space of container.
// > 3. If container is a scroll container, apply the IntersectionObservers [[scrollMargin]]
// > to the containers clip rect as described in apply scroll margin to a scrollport.
// > 4. If container has a content clip or a css clip-path property, update intersectionRect
// > by applying containers clip.
// > 5. If container is the root element of a browsing context, update container to be the
// > browsing contexts document; otherwise, update container to be the containing block
// > of container.
// TODO: Implement rest of step 2 and 3, which will consider transform matrix, window scroll, etc.
while container != *root {
let containing_element = match container {
ElementOrDocument::Document(ref containing_document) => {
// > 3.1. If container is the document of a nested browsing context, update intersectionRect by clipping
// > to the viewport of the document, and update container to be the browsing context container of container.
if let Some(frame_container) = containing_document
.browsing_context()
.and_then(|window| window.frame_element().map(DomRoot::from_ref))
{
let viewport_rect = f32_rect_to_au_rect(Rect::from_size(
containing_document.window().viewport_details().size,
));
if let Some(rect) = intersect_rectangle(&intersection_rect, &viewport_rect) {
intersection_rect = rect;
} else {
return None;
}
let current_offset = frame_container
.upcast::<Node>()
.padding_box()
.unwrap()
.origin
.to_vector();
intersection_rect.origin += current_offset;
total_inter_document_offset += current_offset;
frame_container
} else {
// TODO: Theoritically, this shouldn't be reachable as we have ensured that the root is reachable
// in the previous steps. But we are still unable to iterate through cross-origin ancestor iframes,
// and we will need to stop the iteration for that case.
break;
}
},
ElementOrDocument::Element(ref root) => root.clone(),
};
// > 3.2. Map intersectionRect to the coordinate space of container.
// TODO(#35767): We don't map the coordinate space per each iteration yet, instead all the rectangles are
// in the viewport coordinate space. But this would cause the scroll margin calculation to be inaccurate
// with respect to transforms.
// > 3.3. If container is a scroll container, apply the IntersectionObservers [[scrollMargin]]
// > to the containers clip rect as described in apply scroll margin to a scrollport.
// > 3.4. If container has a content clip or a css clip-path property, update intersectionRect
// > by applying containers clip.
// TODO(#35767): handle `overflow: clip` and resolve clipping for x-axis and y-axis independently.
// Additionally, handle css `clip-path` as well.
if IntersectionObserver::has_content_clip(&containing_element) {
if let Some(container_padding_box) = containing_element
.upcast::<Node>()
.padding_box_without_reflow()
{
let container_padding_box = if containing_element.establishes_scroll_container() {
let margin = IntersectionObserver::resolve_percentages_with_basis(
scroll_margin,
container_padding_box,
);
container_padding_box.outer_rect(margin)
} else {
container_padding_box
};
if let Some(rect) = intersect_rectangle(&intersection_rect, &container_padding_box)
{
intersection_rect = rect;
} else {
return None;
}
}
}
// > 3.5. If container is the root element of a browsing context, update container to be the
// > browsing contexts document; otherwise, update container to be the containing block
// > of container.
// Additionally, for a node that doesn't have an element that establishes its containing block, we should
// refer to the browsing context's document.
container = match containing_element
.upcast::<Node>()
.containing_block_node_without_reflow()
.and_then(DomRoot::downcast::<Element>)
{
Some(element) => ElementOrDocument::Element(element),
None => ElementOrDocument::Document(containing_element.owner_document()),
};
}
// Step 4
// > Map intersectionRect to the coordinate space of root.
// TODO: implement this by considering the transform matrix, window scroll, etc.
// TODO(#35767): we don't map the coordinate space per each iteration yet, instead all the rectangles are
// in the viewport coordinate space.
// Step 5
// > Update intersectionRect by intersecting it with the root intersection rectangle.
// Note that we also consider the edge-adjacent intersection.
let intersection_box = intersection_rect
.to_box2d()
.intersection_unchecked(&root_bounds.to_box2d());
// Although not specified, the result for non-intersecting rectangle should be zero rectangle.
// So we should give zero rectangle immediately without modifying it.
if intersection_box.is_negative() {
return Rect::zero();
}
intersection_rect = intersection_box.to_rect();
intersection_rect = intersect_rectangle(&intersection_rect, &root_bounds)?;
// Step 6
// > Map intersectionRect to the coordinate space of the viewport of the document containing target.
// TODO: implement this by considering the transform matrix, window scroll, etc.
// Offset the intersectionRect back to the coordinate space of target's document.
intersection_rect.origin -= total_inter_document_offset;
// Step 7
// > Return intersectionRect.
intersection_rect
Some(intersection_rect)
}
/// The values from computing step 2.2.4-2.2.14 in

View File

@@ -1052,6 +1052,12 @@ impl Node {
TrustedNodeAddress(self as *const Node as *const libc::c_void)
}
/// Return the node that establishes a containing block for this node.
pub(crate) fn containing_block_node_without_reflow(&self) -> Option<DomRoot<Node>> {
self.owner_window()
.containing_block_node_query_without_reflow(self)
}
pub(crate) fn padding(&self) -> Option<PhysicalSides> {
self.owner_window().padding_query_without_reflow(self)
}
@@ -1066,11 +1072,21 @@ impl Node {
.box_area_query(self, BoxAreaType::Border, false)
}
pub(crate) fn border_box_without_reflow(&self) -> Option<Rect<Au, CSSPixel>> {
self.owner_window()
.box_area_query_without_reflow(self, BoxAreaType::Border, false)
}
pub(crate) fn padding_box(&self) -> Option<Rect<Au, CSSPixel>> {
self.owner_window()
.box_area_query(self, BoxAreaType::Padding, false)
}
pub(crate) fn padding_box_without_reflow(&self) -> Option<Rect<Au, CSSPixel>> {
self.owner_window()
.box_area_query_without_reflow(self, BoxAreaType::Padding, false)
}
pub(crate) fn border_boxes(&self) -> CSSPixelRectIterator {
self.owner_window()
.box_areas_query(self, BoxAreaType::Border)

View File

@@ -2854,6 +2854,19 @@ impl Window {
)
}
/// Query the ancestor node that establishes the containing block for the given node.
/// <https://drafts.csswg.org/css-position-3/#def-cb>
#[expect(unsafe_code)]
pub(crate) fn containing_block_node_query_without_reflow(
&self,
node: &Node,
) -> Option<DomRoot<Node>> {
self.layout
.borrow()
.query_containing_block(node.to_trusted_node_address())
.map(|address| unsafe { from_untrusted_node_address(address) })
}
/// Query the used padding values for the given node, but do not force a reflow.
/// This is used for things like `ResizeObserver` which should observe the value
/// from the most recent reflow, but do not need it to reflect the current state of

View File

@@ -1176,7 +1176,7 @@ Unions = {
},
'ElementOrDocument': {
'derives': ['Clone', 'MallocSizeOf']
'derives': ['Clone', 'MallocSizeOf', 'PartialEq']
},
'HTMLCanvasElementOrOffscreenCanvas': {

View File

@@ -339,6 +339,7 @@ pub trait Layout {
/// Marks that this layout needs to produce a new display list for rendering updates.
fn set_needs_new_display_list(&self);
fn query_containing_block(&self, node: TrustedNodeAddress) -> Option<UntrustedNodeAddress>;
fn query_padding(&self, node: TrustedNodeAddress) -> Option<PhysicalSides>;
fn query_box_area(
&self,

View File

@@ -532065,6 +532065,10 @@
"522dfc9413063b55f915ebb14498adb66ef3164c",
[]
],
"iframe-body-with-overflow-propagation.html": [
"5e7fab6d4ce25cd7793397ceb2006f3b8d315482",
[]
],
"iframe-no-root-subframe.html": [
"ee63a06ca0ff30eb6bc82d28350cf8d85313251b",
[]
@@ -811063,6 +811067,13 @@
{}
]
],
"iframe-root-with-overflow-propagation.html": [
"6667196910ee852f91ef222a3cc09aebb796f2d2",
[
null,
{}
]
],
"initial-observation-with-threshold.html": [
"b9218b09ea6c8c3a1b12dca8cfdb13531449920b",
[

View File

@@ -1,4 +1,3 @@
[overflow-clip-margin-intersection-observer.html]
expected: TIMEOUT
[ParentWithOverflowClipMargin]
expected: TIMEOUT
expected: FAIL

View File

@@ -1,3 +0,0 @@
[document-scrolling-element-root.html]
[First rAF.]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[fixed-position-iframe-scroll.html]
[scrollTo(0, 1000)]
expected: FAIL

View File

@@ -1,9 +0,0 @@
[iframe-no-root-with-wrapping-scroller.html]
[First rAF.]
expected: FAIL
[iframe.contentDocument.scrollingElement.scrollTop = 250]
expected: FAIL
[document.scrollingElement.scrollTop = 100]
expected: FAIL

View File

@@ -1,9 +0,0 @@
[iframe-no-root.html]
[First rAF.]
expected: FAIL
[iframe.contentDocument.scrollingElement.scrollTop = 250]
expected: FAIL
[document.scrollingElement.scrollTop = 100]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[scroll-margin-4-val.html]
[Test scroll margin intersection]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[scroll-margin-dynamic.html]
[Test no initial scroll margin intersection]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[scroll-margin-horizontal.html]
[Test scroll margin intersection]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[scroll-margin-nested-2.html]
[Test scroll margin intersection]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[scroll-margin-nested-3.html]
[Test scroll margin intersection]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[scroll-margin-nested.html]
[Test scroll margin intersection]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[scroll-margin-no-intersect.html]
[Test scroll margin intersection]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[scroll-margin-percent.html]
[Test scroll margin intersection]
expected: FAIL

View File

@@ -1,7 +1,4 @@
[scroll-margin-propagation.html]
[Scroll margin is applied to iframe 2, because it's same-origin-domain with iframe 3]
expected: FAIL
[Scroll margin is not applied to iframe 1, because it's cross-origin-domain with iframe 3]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[scroll-margin-with-border-outline.html]
[Test scroll margin intersection]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[scroll-margin-zero.html]
[Test scroll margin intersection]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[scroll-margin.html]
[Test scroll margin intersection]
expected: FAIL

View File

@@ -1,3 +0,0 @@
[timestamp.html]
[Generate notifications.]
expected: FAIL

View File

@@ -0,0 +1,3 @@
[position-absolute-overflow-visible-and-not-visible.html]
[ParentWithOverflowVisibleAndNotVisible]
expected: FAIL

View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<title>IntersectionObserver observing table with border and overflow scroll</title>
<link rel="author" href="mailto:steven.novaryo@gmail.com" title="Steven Novaryo">
<link rel="help" href="https://w3c.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle">
<link rel="help" href="https://drafts.csswg.org/css-overflow-3/#overflow-propagation">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="./resources/intersection-observer-test-utils.js"></script>
<iframe id="target-iframe" width="200" height="200" src="resources/iframe-body-with-overflow-propagation.html">
</iframe>
<script>
var iframe = document.getElementById("target-iframe");
var target;
var entries = [];
var rootRect;
iframe.onload = function() {
runTestCycle(function() {
target = iframe.contentDocument.getElementById("target");
assert_true(!!target, "target exists");
rootRect = iframe.contentDocument.documentElement.getBoundingClientRect();
var observer = new IntersectionObserver(function(changes) {
entries = entries.concat(changes)
}, { root: iframe.contentDocument });
observer.observe(target);
entries = entries.concat(observer.takeRecords());
assert_equals(entries.length, 0, "No initial notifications.");
runTestCycle(step0, "First rAF.");
}, "IntersectionObserver observing an element inside a root table.");
}
function step0() {
var targetRect = target.getBoundingClientRect();
iframe.contentDocument.scrollingElement.scrollTop = 1000;
runTestCycle(step1, "Scroll the iframe container to show the target element.");
checkLastEntry(entries, 0, [
targetRect.left, targetRect.right, targetRect.top, targetRect.bottom,
0, 0, 0, 0,
rootRect.left, rootRect.right, rootRect.top, rootRect.bottom,
false
]);
}
function step1() {
var targetRect = target.getBoundingClientRect();
checkLastEntry(entries, 1, [
targetRect.left, targetRect.right, targetRect.top, targetRect.bottom,
targetRect.left, targetRect.right, targetRect.top, targetRect.bottom,
rootRect.left, rootRect.right, rootRect.top, rootRect.bottom,
true
]);
}
</script>

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<style>
body {
overflow-y: auto;
width: 100%;
height: 200px;
margin: 0;
}
#overflowing-child {
background-color: red;
height: 200%;
width: 100%;
}
#target {
background-color: green;
height: 100%;
width: 100%;
}
</style>
<body>
<div id="overflowing-child"></div>
<div id="target"></div>
</body>