mirror of
https://github.com/servo/servo
synced 2026-04-25 17:15:48 +02:00
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:
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3146,7 +3146,7 @@ impl Document {
|
||||
) {
|
||||
// Step 1
|
||||
// > Let rootBounds be observer’s 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 observer’s internal [[ObservationTargets]] slot,
|
||||
|
||||
@@ -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 element’s 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 we’ll 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, it’s the element’s 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, it’s 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,
|
||||
// > it’s treated as if the root were the top-level browsing context’s 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, it’s 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 observer’s 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 targetRect’s 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 registration’s 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 IntersectionObserver’s [[scrollMargin]]
|
||||
// > to the container’s 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 container’s clip.
|
||||
// > 5. If container is the root element of a browsing context, update container to be the
|
||||
// > browsing context’s 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 IntersectionObserver’s [[scrollMargin]]
|
||||
// > to the container’s 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 container’s 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 context’s 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1176,7 +1176,7 @@ Unions = {
|
||||
},
|
||||
|
||||
'ElementOrDocument': {
|
||||
'derives': ['Clone', 'MallocSizeOf']
|
||||
'derives': ['Clone', 'MallocSizeOf', 'PartialEq']
|
||||
},
|
||||
|
||||
'HTMLCanvasElementOrOffscreenCanvas': {
|
||||
|
||||
@@ -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,
|
||||
|
||||
11
tests/wpt/meta/MANIFEST.json
vendored
11
tests/wpt/meta/MANIFEST.json
vendored
@@ -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",
|
||||
[
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
[overflow-clip-margin-intersection-observer.html]
|
||||
expected: TIMEOUT
|
||||
[ParentWithOverflowClipMargin]
|
||||
expected: TIMEOUT
|
||||
expected: FAIL
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[document-scrolling-element-root.html]
|
||||
[First rAF.]
|
||||
expected: FAIL
|
||||
@@ -1,3 +0,0 @@
|
||||
[fixed-position-iframe-scroll.html]
|
||||
[scrollTo(0, 1000)]
|
||||
expected: FAIL
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
[scroll-margin-4-val.html]
|
||||
[Test scroll margin intersection]
|
||||
expected: FAIL
|
||||
@@ -1,3 +0,0 @@
|
||||
[scroll-margin-dynamic.html]
|
||||
[Test no initial scroll margin intersection]
|
||||
expected: FAIL
|
||||
@@ -1,3 +0,0 @@
|
||||
[scroll-margin-horizontal.html]
|
||||
[Test scroll margin intersection]
|
||||
expected: FAIL
|
||||
@@ -1,3 +0,0 @@
|
||||
[scroll-margin-nested-2.html]
|
||||
[Test scroll margin intersection]
|
||||
expected: FAIL
|
||||
@@ -1,3 +0,0 @@
|
||||
[scroll-margin-nested-3.html]
|
||||
[Test scroll margin intersection]
|
||||
expected: FAIL
|
||||
@@ -1,3 +0,0 @@
|
||||
[scroll-margin-nested.html]
|
||||
[Test scroll margin intersection]
|
||||
expected: FAIL
|
||||
@@ -1,3 +0,0 @@
|
||||
[scroll-margin-no-intersect.html]
|
||||
[Test scroll margin intersection]
|
||||
expected: FAIL
|
||||
@@ -1,3 +0,0 @@
|
||||
[scroll-margin-percent.html]
|
||||
[Test scroll margin intersection]
|
||||
expected: FAIL
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[scroll-margin-with-border-outline.html]
|
||||
[Test scroll margin intersection]
|
||||
expected: FAIL
|
||||
@@ -1,3 +0,0 @@
|
||||
[scroll-margin-zero.html]
|
||||
[Test scroll margin intersection]
|
||||
expected: FAIL
|
||||
@@ -1,3 +0,0 @@
|
||||
[scroll-margin.html]
|
||||
[Test scroll margin intersection]
|
||||
expected: FAIL
|
||||
@@ -1,3 +0,0 @@
|
||||
[timestamp.html]
|
||||
[Generate notifications.]
|
||||
expected: FAIL
|
||||
@@ -0,0 +1,3 @@
|
||||
[position-absolute-overflow-visible-and-not-visible.html]
|
||||
[ParentWithOverflowVisibleAndNotVisible]
|
||||
expected: FAIL
|
||||
58
tests/wpt/tests/intersection-observer/iframe-root-with-overflow-propagation.html
vendored
Normal file
58
tests/wpt/tests/intersection-observer/iframe-root-with-overflow-propagation.html
vendored
Normal 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>
|
||||
23
tests/wpt/tests/intersection-observer/resources/iframe-body-with-overflow-propagation.html
vendored
Normal file
23
tests/wpt/tests/intersection-observer/resources/iframe-body-with-overflow-propagation.html
vendored
Normal 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>
|
||||
Reference in New Issue
Block a user