mirror of
https://github.com/servo/servo
synced 2026-04-26 01:25:32 +02:00
This removes some complains that clippy will have in 1.95. As this is mostly just match guards, it doesn't update MSRV. Testing: This is equivalent exchanges, so current WPT would find anything. --------- Signed-off-by: Narfinger <Narfinger@users.noreply.github.com>
1058 lines
46 KiB
Rust
1058 lines
46 KiB
Rust
/* 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::cell::{Cell, RefCell};
|
||
use std::rc::Rc;
|
||
use std::time::Duration;
|
||
|
||
use app_units::Au;
|
||
use cssparser::{Parser, ParserInput};
|
||
use dom_struct::dom_struct;
|
||
use euclid::{Rect, SideOffsets2D, Size2D, Vector2D};
|
||
use js::rust::{HandleObject, MutableHandleValue};
|
||
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;
|
||
use style::values::specified::intersection_observer::IntersectionObserverMargin;
|
||
use style_traits::{CSSPixel, ParsingMode, ToCss};
|
||
use url::Url;
|
||
|
||
use crate::css::parser_context_for_anonymous_content;
|
||
use crate::dom::bindings::callback::ExceptionHandling;
|
||
use crate::dom::bindings::cell::DomRefCell;
|
||
use crate::dom::bindings::codegen::Bindings::IntersectionObserverBinding::{
|
||
IntersectionObserverCallback, IntersectionObserverInit, IntersectionObserverMethods,
|
||
};
|
||
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
|
||
use crate::dom::bindings::codegen::UnionTypes::{DoubleOrDoubleSequence, ElementOrDocument};
|
||
use crate::dom::bindings::error::{Error, Fallible};
|
||
use crate::dom::bindings::inheritance::Castable;
|
||
use crate::dom::bindings::num::Finite;
|
||
use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
|
||
use crate::dom::bindings::root::{Dom, DomRoot};
|
||
use crate::dom::bindings::str::DOMString;
|
||
use crate::dom::bindings::utils::to_frozen_array;
|
||
use crate::dom::document::{Document, RenderingUpdateReason};
|
||
use crate::dom::domrectreadonly::DOMRectReadOnly;
|
||
use crate::dom::element::Element;
|
||
use crate::dom::intersectionobserverentry::IntersectionObserverEntry;
|
||
use crate::dom::node::{Node, NodeTraits};
|
||
use crate::dom::window::Window;
|
||
use crate::script_runtime::{CanGc, JSContext};
|
||
|
||
/// > The intersection root for an IntersectionObserver is the value of its root attribute if the attribute is non-null;
|
||
/// > otherwise, it is the top-level browsing context’s document node, referred to as the implicit root.
|
||
///
|
||
/// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-intersection-root>
|
||
pub type IntersectionRoot = Option<ElementOrDocument>;
|
||
|
||
/// The Intersection Observer interface
|
||
///
|
||
/// > The IntersectionObserver interface can be used to observe changes in the intersection
|
||
/// > of an intersection root and one or more target Elements.
|
||
///
|
||
/// <https://w3c.github.io/IntersectionObserver/#intersection-observer-interface>
|
||
#[dom_struct]
|
||
pub(crate) struct IntersectionObserver {
|
||
reflector_: Reflector,
|
||
|
||
/// [`Document`] that should process this observer's observation steps.
|
||
/// Following Chrome and Firefox, it is the current document on construction.
|
||
/// <https://github.com/w3c/IntersectionObserver/issues/525>
|
||
owner_doc: Dom<Document>,
|
||
|
||
/// > The root provided to the IntersectionObserver constructor, or null if none was provided.
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-root>
|
||
root: IntersectionRoot,
|
||
|
||
/// > This callback will be invoked when there are changes to a target’s intersection
|
||
/// > with the intersection root, as per the processing model.
|
||
///
|
||
/// <https://w3c.github.io/IntersectionObserver/#intersection-observer-callback>
|
||
#[conditional_malloc_size_of]
|
||
callback: Rc<IntersectionObserverCallback>,
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-queuedentries-slot>
|
||
queued_entries: DomRefCell<Vec<Dom<IntersectionObserverEntry>>>,
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observationtargets-slot>
|
||
observation_targets: DomRefCell<Vec<Dom<Element>>>,
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin-slot>
|
||
#[no_trace]
|
||
#[ignore_malloc_size_of = "Defined in style"]
|
||
root_margin: RefCell<IntersectionObserverMargin>,
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-scrollmargin-slot>
|
||
#[no_trace]
|
||
#[ignore_malloc_size_of = "Defined in style"]
|
||
scroll_margin: RefCell<IntersectionObserverMargin>,
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-thresholds-slot>
|
||
thresholds: RefCell<Vec<Finite<f64>>>,
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-delay-slot>
|
||
delay: Cell<i32>,
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-trackvisibility-slot>
|
||
track_visibility: Cell<bool>,
|
||
|
||
/// Whether or not this [`IntersectionObserver`] is connected to its owning [`Document`].
|
||
connected_to_document: Cell<bool>,
|
||
}
|
||
|
||
impl IntersectionObserver {
|
||
fn new_inherited(
|
||
window: &Window,
|
||
callback: Rc<IntersectionObserverCallback>,
|
||
root: IntersectionRoot,
|
||
root_margin: IntersectionObserverMargin,
|
||
scroll_margin: IntersectionObserverMargin,
|
||
) -> Self {
|
||
Self {
|
||
reflector_: Reflector::new(),
|
||
owner_doc: window.Document().as_traced(),
|
||
root,
|
||
callback,
|
||
queued_entries: Default::default(),
|
||
observation_targets: Default::default(),
|
||
root_margin: RefCell::new(root_margin),
|
||
scroll_margin: RefCell::new(scroll_margin),
|
||
thresholds: Default::default(),
|
||
delay: Default::default(),
|
||
track_visibility: Default::default(),
|
||
connected_to_document: Cell::new(false),
|
||
}
|
||
}
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#initialize-new-intersection-observer>
|
||
fn new(
|
||
window: &Window,
|
||
proto: Option<HandleObject>,
|
||
callback: Rc<IntersectionObserverCallback>,
|
||
init: &IntersectionObserverInit,
|
||
can_gc: CanGc,
|
||
) -> Fallible<DomRoot<Self>> {
|
||
// Step 3.
|
||
// > Attempt to parse a margin from options.rootMargin. If a list is returned,
|
||
// > set this’s internal [[rootMargin]] slot to that. Otherwise, throw a SyntaxError exception.
|
||
let root_margin = if let Ok(margin) = parse_a_margin(init.rootMargin.as_ref()) {
|
||
margin
|
||
} else {
|
||
return Err(Error::Syntax(None));
|
||
};
|
||
|
||
// Step 4.
|
||
// > Attempt to parse a margin from options.scrollMargin. If a list is returned,
|
||
// > set this’s internal [[scrollMargin]] slot to that. Otherwise, throw a SyntaxError exception.
|
||
let scroll_margin = if let Ok(margin) = parse_a_margin(init.scrollMargin.as_ref()) {
|
||
margin
|
||
} else {
|
||
return Err(Error::Syntax(None));
|
||
};
|
||
|
||
// Step 1 and step 2, 3, 4 setter
|
||
// > 1. Let this be a new IntersectionObserver object
|
||
// > 2. Set this’s internal [[callback]] slot to callback.
|
||
// > 3. ... set this’s internal [[rootMargin]] slot to that.
|
||
// > 4. ... set this’s internal [[scrollMargin]] slot to that.
|
||
let observer = reflect_dom_object_with_proto(
|
||
Box::new(Self::new_inherited(
|
||
window,
|
||
callback,
|
||
init.root.clone(),
|
||
root_margin,
|
||
scroll_margin,
|
||
)),
|
||
window,
|
||
proto,
|
||
can_gc,
|
||
);
|
||
|
||
// Step 5-13
|
||
observer.init_observer(init)?;
|
||
|
||
Ok(observer)
|
||
}
|
||
|
||
/// Step 5-13 of <https://w3c.github.io/IntersectionObserver/#initialize-new-intersection-observer>
|
||
fn init_observer(&self, init: &IntersectionObserverInit) -> Fallible<()> {
|
||
// Step 5
|
||
// > Let thresholds be a list equal to options.threshold.
|
||
//
|
||
// Non-sequence value should be converted into Vec.
|
||
// Default value of thresholds is [0].
|
||
let mut thresholds = match &init.threshold {
|
||
Some(DoubleOrDoubleSequence::Double(num)) => vec![*num],
|
||
Some(DoubleOrDoubleSequence::DoubleSequence(sequence)) => sequence.clone(),
|
||
None => vec![Finite::wrap(0.)],
|
||
};
|
||
|
||
// Step 6
|
||
// > If any value in thresholds is less than 0.0 or greater than 1.0, throw a RangeError exception.
|
||
for num in &thresholds {
|
||
if **num < 0.0 || **num > 1.0 {
|
||
return Err(Error::Range(
|
||
c"Value in thresholds should not be less than 0.0 or greater than 1.0"
|
||
.to_owned(),
|
||
));
|
||
}
|
||
}
|
||
|
||
// Step 7
|
||
// > Sort thresholds in ascending order.
|
||
thresholds.sort_by(|lhs, rhs| lhs.partial_cmp(&**rhs).unwrap());
|
||
|
||
// Step 8
|
||
// > If thresholds is empty, append 0 to thresholds.
|
||
if thresholds.is_empty() {
|
||
thresholds.push(Finite::wrap(0.));
|
||
}
|
||
|
||
// Step 9
|
||
// > The thresholds attribute getter will return this sorted thresholds list.
|
||
//
|
||
// Set this internal [[thresholds]] slot to the sorted thresholds list
|
||
// and getter will return the internal [[thresholds]] slot.
|
||
self.thresholds.replace(thresholds);
|
||
|
||
// Step 10
|
||
// > Let delay be the value of options.delay.
|
||
//
|
||
// Default value of delay is 0.
|
||
let mut delay = init.delay.unwrap_or(0);
|
||
|
||
// Step 11
|
||
// > If options.trackVisibility is true and delay is less than 100, set delay to 100.
|
||
//
|
||
// In Chromium, the minimum delay required is 100 milliseconds for observation that consider trackVisibilty.
|
||
// Currently, visibility is not implemented.
|
||
if init.trackVisibility {
|
||
delay = delay.max(100);
|
||
}
|
||
|
||
// Step 12
|
||
// > Set this’s internal [[delay]] slot to options.delay to delay.
|
||
self.delay.set(delay);
|
||
|
||
// Step 13
|
||
// > Set this’s internal [[trackVisibility]] slot to options.trackVisibility.
|
||
self.track_visibility.set(init.trackVisibility);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#observe-target-element>
|
||
fn observe_target_element(&self, target: &Element) {
|
||
// Step 1
|
||
// > If target is in observer’s internal [[ObservationTargets]] slot, return.
|
||
let is_present = self
|
||
.observation_targets
|
||
.borrow()
|
||
.iter()
|
||
.any(|element| &**element == target);
|
||
if is_present {
|
||
return;
|
||
}
|
||
|
||
// Step 2
|
||
// > Let intersectionObserverRegistration be an IntersectionObserverRegistration record with
|
||
// > an observer property set to observer, a previousThresholdIndex property set to -1,
|
||
// > a previousIsIntersecting property set to false, and a previousIsVisible property set to false.
|
||
// Step 3
|
||
// > Append intersectionObserverRegistration to target’s internal [[RegisteredIntersectionObservers]] slot.
|
||
target.add_initial_intersection_observer_registration(self);
|
||
|
||
if self.observation_targets.borrow().is_empty() {
|
||
self.connect_to_owner();
|
||
}
|
||
|
||
// Step 4
|
||
// > Add target to observer’s internal [[ObservationTargets]] slot.
|
||
self.observation_targets
|
||
.borrow_mut()
|
||
.push(Dom::from_ref(target));
|
||
|
||
target
|
||
.owner_window()
|
||
.Document()
|
||
.add_rendering_update_reason(
|
||
RenderingUpdateReason::IntersectionObserverStartedObservingTarget,
|
||
);
|
||
}
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#unobserve-target-element>
|
||
fn unobserve_target_element(&self, target: &Element) {
|
||
// Step 1
|
||
// > Remove the IntersectionObserverRegistration record whose observer property is equal to
|
||
// > this from target’s internal [[RegisteredIntersectionObservers]] slot, if present.
|
||
target
|
||
.registered_intersection_observers_mut()
|
||
.retain(|registration| &*registration.observer != self);
|
||
|
||
// Step 2
|
||
// > Remove target from this’s internal [[ObservationTargets]] slot, if present
|
||
self.observation_targets
|
||
.borrow_mut()
|
||
.retain(|element| &**element != target);
|
||
|
||
// Should disconnect from owner if it is not observing anything.
|
||
if self.observation_targets.borrow().is_empty() {
|
||
self.disconnect_from_owner();
|
||
}
|
||
}
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#queue-an-intersectionobserverentry>
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn queue_an_intersectionobserverentry(
|
||
&self,
|
||
document: &Document,
|
||
time: CrossProcessInstant,
|
||
root_bounds: Rect<Au, CSSPixel>,
|
||
bounding_client_rect: Rect<Au, CSSPixel>,
|
||
intersection_rect: Rect<Au, CSSPixel>,
|
||
is_intersecting: bool,
|
||
is_visible: bool,
|
||
intersection_ratio: f64,
|
||
target: &Element,
|
||
can_gc: CanGc,
|
||
) {
|
||
let rect_to_domrectreadonly = |rect: Rect<Au, CSSPixel>| {
|
||
DOMRectReadOnly::new(
|
||
self.owner_doc.window().as_global_scope(),
|
||
None,
|
||
rect.origin.x.to_f64_px(),
|
||
rect.origin.y.to_f64_px(),
|
||
rect.size.width.to_f64_px(),
|
||
rect.size.height.to_f64_px(),
|
||
can_gc,
|
||
)
|
||
};
|
||
|
||
let root_bounds = rect_to_domrectreadonly(root_bounds);
|
||
let bounding_client_rect = rect_to_domrectreadonly(bounding_client_rect);
|
||
let intersection_rect = rect_to_domrectreadonly(intersection_rect);
|
||
|
||
// Step 1-2
|
||
// > 1. Construct an IntersectionObserverEntry, passing in time, rootBounds,
|
||
// > boundingClientRect, intersectionRect, isIntersecting, and target.
|
||
// > 2. Append it to observer’s internal [[QueuedEntries]] slot.
|
||
self.queued_entries.borrow_mut().push(
|
||
IntersectionObserverEntry::new(
|
||
self.owner_doc.window(),
|
||
None,
|
||
document
|
||
.owner_global()
|
||
.performance()
|
||
.to_dom_high_res_time_stamp(time),
|
||
Some(&root_bounds),
|
||
&bounding_client_rect,
|
||
&intersection_rect,
|
||
is_intersecting,
|
||
is_visible,
|
||
Finite::wrap(intersection_ratio),
|
||
target,
|
||
can_gc,
|
||
)
|
||
.as_traced(),
|
||
);
|
||
// > Step 3
|
||
// Queue an intersection observer task for document.
|
||
document.queue_an_intersection_observer_task();
|
||
}
|
||
|
||
/// Step 3.1-3.5 of <https://w3c.github.io/IntersectionObserver/#notify-intersection-observers-algo>
|
||
pub(crate) fn invoke_callback_if_necessary(&self, can_gc: CanGc) {
|
||
// Step 1
|
||
// > If observer’s internal [[QueuedEntries]] slot is empty, continue.
|
||
if self.queued_entries.borrow().is_empty() {
|
||
return;
|
||
}
|
||
|
||
// Step 2-3
|
||
// We trivially moved the entries and root them.
|
||
let queued_entries = self
|
||
.queued_entries
|
||
.take()
|
||
.iter_mut()
|
||
.map(|entry| entry.as_rooted())
|
||
.collect();
|
||
|
||
// Step 4-5
|
||
let _ = self.callback.Call_(
|
||
self,
|
||
queued_entries,
|
||
self,
|
||
ExceptionHandling::Report,
|
||
can_gc,
|
||
);
|
||
}
|
||
|
||
/// Connect the observer itself into owner doc if it is unconnected.
|
||
/// If the [`IntersectionObserver`] is already connected, do nothing.
|
||
fn connect_to_owner(&self) {
|
||
if !self.connected_to_document.get() {
|
||
self.owner_doc.add_intersection_observer(self);
|
||
self.connected_to_document.set(true);
|
||
}
|
||
}
|
||
|
||
/// Disconnect the observer itself from owner doc.
|
||
/// If not connected to a [`Document`], do nothing.
|
||
fn disconnect_from_owner(&self) {
|
||
if self.connected_to_document.get() {
|
||
self.owner_doc.remove_intersection_observer(self);
|
||
}
|
||
}
|
||
|
||
/// <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) -> 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 IntersectionObserver::has_content_clip(&element) {
|
||
// > Otherwise, if the intersection root has a content clip, it’s the element’s padding area.
|
||
element.upcast::<Node>().padding_box_without_reflow()
|
||
} else {
|
||
// > Otherwise, it’s the result of getting the bounding box for the intersection root.
|
||
element.upcast::<Node>().border_box_without_reflow()
|
||
}
|
||
},
|
||
// Handle if root is a Document, which includes implicit root and explicit Document root.
|
||
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.
|
||
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,
|
||
// > the rectangle is then expanded according to the offsets in the IntersectionObserver’s
|
||
// > [[rootMargin]] slot in a manner similar to CSS’s margin property, with the four values
|
||
// > indicating the amount the top, right, bottom, and left edges, respectively, are offset by,
|
||
// > with positive lengths indicating an outward offset. Percentages are resolved relative to
|
||
// > 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(
|
||
&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
|
||
/// return default values conformant to step 2.2.4. See [`IntersectionObservationOutput::default_skipped`].
|
||
///
|
||
/// Note that current draft specs skipped wrong steps, as it should skip computing fields that
|
||
/// would result in different intersection entry other than the default entry per published spec.
|
||
/// <https://www.w3.org/TR/intersection-observer/>
|
||
fn maybe_compute_intersection_output(
|
||
&self,
|
||
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.
|
||
// 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.
|
||
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 = 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), Some(root_intersection)) =
|
||
(maybe_root_bounds, maybe_target_rect, self.concrete_root())
|
||
else {
|
||
return IntersectionObservationOutput::default_skipped();
|
||
};
|
||
|
||
// TODO(stevennovaryo): we should probably also consider adding visibity check, ideally
|
||
// it would require new query from LayoutThread.
|
||
|
||
// Step 8
|
||
// > Let intersectionRect be the result of running the compute the intersection algorithm on
|
||
// > target and observer’s intersection root.
|
||
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.
|
||
// Step 10
|
||
// > Let intersectionArea be intersectionRect’s area.
|
||
// These steps are folded in Step 12, rewriting (w1 * h1) / (w2 * h2) as (w1 / w2) * (h1 / h2)
|
||
// to avoid multiplication overflows.
|
||
|
||
// Step 11
|
||
// > Let isIntersecting be true if targetRect and rootBounds intersect or are edge-adjacent,
|
||
// > even if the intersection has zero area (because rootBounds or targetRect have zero area).
|
||
// Because we are considering edge-adjacent, instead of checking whether the rectangle is empty,
|
||
// 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 = maybe_intersection_rect.is_some();
|
||
|
||
// Step 12
|
||
// > If targetArea is non-zero, let intersectionRatio be intersectionArea divided by targetArea.
|
||
// > Otherwise, let intersectionRatio be 1 if isIntersecting is true, or 0 if isIntersecting is false.
|
||
let intersection_ratio = if target_rect.size.width.0 == 0 || target_rect.size.height.0 == 0
|
||
{
|
||
is_intersecting.into()
|
||
} else {
|
||
(intersection_rect.size.width.0 as f64 / target_rect.size.width.0 as f64) *
|
||
(intersection_rect.size.height.0 as f64 / target_rect.size.height.0 as f64)
|
||
};
|
||
|
||
// Step 13
|
||
// > Set thresholdIndex to the index of the first entry in observer.thresholds whose value is
|
||
// > greater than intersectionRatio, or the length of observer.thresholds if intersectionRatio is
|
||
// > greater than or equal to the last entry in observer.thresholds.
|
||
let threshold_index = self
|
||
.thresholds
|
||
.borrow()
|
||
.iter()
|
||
.position(|threshold| **threshold > intersection_ratio)
|
||
.unwrap_or(self.thresholds.borrow().len()) as i32;
|
||
|
||
// Step 14
|
||
// > Let isVisible be the result of running the visibility algorithm on target.
|
||
// TODO: Implement visibility algorithm
|
||
let is_visible = false;
|
||
|
||
IntersectionObservationOutput::new_computed(
|
||
threshold_index,
|
||
is_intersecting,
|
||
target_rect,
|
||
intersection_rect,
|
||
intersection_ratio,
|
||
is_visible,
|
||
root_bounds,
|
||
)
|
||
}
|
||
|
||
/// Step 2.2.1-2.2.21 of <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>
|
||
pub(crate) fn update_intersection_observations_steps(
|
||
&self,
|
||
document: &Document,
|
||
time: CrossProcessInstant,
|
||
root_bounds: Option<Rect<Au, CSSPixel>>,
|
||
can_gc: CanGc,
|
||
) {
|
||
for target in &*self.observation_targets.borrow() {
|
||
// Step 1
|
||
// > Let registration be the IntersectionObserverRegistration record in target’s internal
|
||
// > [[RegisteredIntersectionObservers]] slot whose observer property is equal to observer.
|
||
let registration = target.get_intersection_observer_registration(self).unwrap();
|
||
|
||
// Step 2
|
||
// > If (time - registration.lastUpdateTime < observer.delay), skip further processing for target.
|
||
if time - registration.last_update_time.get() <
|
||
Duration::from_millis(self.delay.get().max(0) as u64)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Step 3
|
||
// > Set registration.lastUpdateTime to time.
|
||
registration.last_update_time.set(time);
|
||
|
||
// step 4-14
|
||
let intersection_output = self.maybe_compute_intersection_output(target, root_bounds);
|
||
|
||
// Step 15-17
|
||
// > 15. Let previousThresholdIndex be the registration’s previousThresholdIndex property.
|
||
// > 16. Let previousIsIntersecting be the registration’s previousIsIntersecting property.
|
||
// > 17. Let previousIsVisible be the registration’s previousIsVisible property.
|
||
let previous_threshold_index = registration.previous_threshold_index.get();
|
||
let previous_is_intersecting = registration.previous_is_intersecting.get();
|
||
let previous_is_visible = registration.previous_is_visible.get();
|
||
|
||
// Step 18
|
||
// > If thresholdIndex does not equal previousThresholdIndex, or
|
||
// > if isIntersecting does not equal previousIsIntersecting, or
|
||
// > if isVisible does not equal previousIsVisible,
|
||
// > queue an IntersectionObserverEntry, passing in observer, time, rootBounds,
|
||
// > targetRect, intersectionRect, isIntersecting, isVisible, and target.
|
||
if intersection_output.threshold_index != previous_threshold_index ||
|
||
intersection_output.is_intersecting != previous_is_intersecting ||
|
||
intersection_output.is_visible != previous_is_visible
|
||
{
|
||
// TODO(stevennovaryo): Per IntersectionObserverEntry interface, the rootBounds
|
||
// should be null for cross-origin-domain target.
|
||
self.queue_an_intersectionobserverentry(
|
||
document,
|
||
time,
|
||
intersection_output.root_bounds,
|
||
intersection_output.target_rect,
|
||
intersection_output.intersection_rect,
|
||
intersection_output.is_intersecting,
|
||
intersection_output.is_visible,
|
||
intersection_output.intersection_ratio,
|
||
target,
|
||
can_gc,
|
||
);
|
||
}
|
||
|
||
// Step 19-21
|
||
// > 19. Assign thresholdIndex to registration’s previousThresholdIndex property.
|
||
// > 20. Assign isIntersecting to registration’s previousIsIntersecting property.
|
||
// > 21. Assign isVisible to registration’s previousIsVisible property.
|
||
registration
|
||
.previous_threshold_index
|
||
.set(intersection_output.threshold_index);
|
||
registration
|
||
.previous_is_intersecting
|
||
.set(intersection_output.is_intersecting);
|
||
registration
|
||
.previous_is_visible
|
||
.set(intersection_output.is_visible);
|
||
}
|
||
}
|
||
|
||
fn resolve_percentages_with_basis(
|
||
margin: &IntersectionObserverMargin,
|
||
containing_block: Rect<Au, CSSPixel>,
|
||
) -> SideOffsets2D<Au, CSSPixel> {
|
||
let inner = &margin.0;
|
||
SideOffsets2D::new(
|
||
inner.0.to_used_value(containing_block.height()),
|
||
inner.1.to_used_value(containing_block.width()),
|
||
inner.2.to_used_value(containing_block.height()),
|
||
inner.3.to_used_value(containing_block.width()),
|
||
)
|
||
}
|
||
}
|
||
|
||
impl IntersectionObserverMethods<crate::DomTypeHolder> for IntersectionObserver {
|
||
/// > The root provided to the IntersectionObserver constructor, or null if none was provided.
|
||
///
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-root>
|
||
fn GetRoot(&self) -> Option<ElementOrDocument> {
|
||
self.root.clone()
|
||
}
|
||
|
||
/// > Offsets applied to the root intersection rectangle, effectively growing or
|
||
/// > shrinking the box that is used to calculate intersections. These offsets are only
|
||
/// > applied when handling same-origin-domain targets; for cross-origin-domain targets
|
||
/// > they are ignored.
|
||
///
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin>
|
||
fn RootMargin(&self) -> DOMString {
|
||
self.root_margin.borrow().to_css_string().into()
|
||
}
|
||
|
||
/// > Offsets are applied to scrollports on the path from intersection root to target,
|
||
/// > effectively growing or shrinking the clip rects used to calculate intersections.
|
||
///
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-scrollmargin>
|
||
fn ScrollMargin(&self) -> DOMString {
|
||
self.scroll_margin.borrow().to_css_string().into()
|
||
}
|
||
|
||
/// > A list of thresholds, sorted in increasing numeric order, where each threshold
|
||
/// > is a ratio of intersection area to bounding box area of an observed target.
|
||
/// > Notifications for a target are generated when any of the thresholds are crossed
|
||
/// > for that target. If no options.threshold was provided to the IntersectionObserver
|
||
/// > constructor, or the sequence is empty, the value of this attribute will be `[0]`.
|
||
///
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-thresholds>
|
||
fn Thresholds(&self, context: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
|
||
to_frozen_array(&self.thresholds.borrow(), context, retval, can_gc);
|
||
}
|
||
|
||
/// > A number indicating the minimum delay in milliseconds between notifications from
|
||
/// > this observer for a given target.
|
||
///
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-delay>
|
||
fn Delay(&self) -> i32 {
|
||
self.delay.get()
|
||
}
|
||
|
||
/// > A boolean indicating whether this IntersectionObserver will track changes in a target’s visibility.
|
||
///
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-trackvisibility>
|
||
fn TrackVisibility(&self) -> bool {
|
||
self.track_visibility.get()
|
||
}
|
||
|
||
/// > Run the observe a target Element algorithm, providing this and target.
|
||
///
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observe>
|
||
fn Observe(&self, target: &Element) {
|
||
self.observe_target_element(target);
|
||
}
|
||
|
||
/// > Run the unobserve a target Element algorithm, providing this and target.
|
||
///
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-unobserve>
|
||
fn Unobserve(&self, target: &Element) {
|
||
self.unobserve_target_element(target);
|
||
}
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-disconnect>
|
||
fn Disconnect(&self) {
|
||
// > For each target in this’s internal [[ObservationTargets]] slot:
|
||
self.observation_targets.borrow().iter().for_each(|target| {
|
||
// > 1. Remove the IntersectionObserverRegistration record whose observer property is equal to
|
||
// > this from target’s internal [[RegisteredIntersectionObservers]] slot.
|
||
target.remove_intersection_observer(self);
|
||
});
|
||
// > 2. Remove target from this’s internal [[ObservationTargets]] slot.
|
||
self.observation_targets.borrow_mut().clear();
|
||
|
||
// We should remove this observer from the event loop.
|
||
self.disconnect_from_owner();
|
||
}
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-takerecords>
|
||
fn TakeRecords(&self) -> Vec<DomRoot<IntersectionObserverEntry>> {
|
||
// Step 1-3.
|
||
self.queued_entries
|
||
.take()
|
||
.iter()
|
||
.map(|entry| entry.as_rooted())
|
||
.collect()
|
||
}
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-intersectionobserver>
|
||
fn Constructor(
|
||
window: &Window,
|
||
proto: Option<HandleObject>,
|
||
can_gc: CanGc,
|
||
callback: Rc<IntersectionObserverCallback>,
|
||
init: &IntersectionObserverInit,
|
||
) -> Fallible<DomRoot<IntersectionObserver>> {
|
||
Self::new(window, proto, callback, init, can_gc)
|
||
}
|
||
}
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#intersectionobserverregistration>
|
||
#[derive(Clone, JSTraceable, MallocSizeOf)]
|
||
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
||
pub(crate) struct IntersectionObserverRegistration {
|
||
pub(crate) observer: Dom<IntersectionObserver>,
|
||
pub(crate) previous_threshold_index: Cell<i32>,
|
||
pub(crate) previous_is_intersecting: Cell<bool>,
|
||
#[no_trace]
|
||
pub(crate) last_update_time: Cell<CrossProcessInstant>,
|
||
pub(crate) previous_is_visible: Cell<bool>,
|
||
}
|
||
|
||
impl IntersectionObserverRegistration {
|
||
/// Initial value of [`IntersectionObserverRegistration`] according to
|
||
/// step 2 of <https://w3c.github.io/IntersectionObserver/#observe-target-element>.
|
||
/// > Let intersectionObserverRegistration be an IntersectionObserverRegistration record with
|
||
/// > an observer property set to observer, a previousThresholdIndex property set to -1,
|
||
/// > a previousIsIntersecting property set to false, and a previousIsVisible property set to false.
|
||
pub(crate) fn new_initial(observer: &IntersectionObserver) -> Self {
|
||
IntersectionObserverRegistration {
|
||
observer: Dom::from_ref(observer),
|
||
previous_threshold_index: Cell::new(-1),
|
||
previous_is_intersecting: Cell::new(false),
|
||
last_update_time: Cell::new(CrossProcessInstant::epoch()),
|
||
previous_is_visible: Cell::new(false),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <https://w3c.github.io/IntersectionObserver/#parse-a-margin>
|
||
fn parse_a_margin(value: Option<&DOMString>) -> Result<IntersectionObserverMargin, ()> {
|
||
// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserverinit-rootmargin> &&
|
||
// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserverinit-scrollmargin>
|
||
// > ... defaulting to "0px".
|
||
let value = match value {
|
||
Some(str) => &str.str(),
|
||
_ => "0px",
|
||
};
|
||
|
||
// Create necessary style ParserContext and utilize stylo's IntersectionObserverMargin
|
||
let mut input = ParserInput::new(value);
|
||
let mut parser = Parser::new(&mut input);
|
||
|
||
let url = Url::parse("about:blank").unwrap().into();
|
||
let context =
|
||
parser_context_for_anonymous_content(CssRuleType::Style, ParsingMode::DEFAULT, &url);
|
||
|
||
parser
|
||
.parse_entirely(|p| IntersectionObserverMargin::parse(&context, p))
|
||
.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(
|
||
target: &Element,
|
||
root: &ElementOrDocument,
|
||
root_bounds: Rect<Au, CSSPixel>,
|
||
mut intersection_rect: 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:
|
||
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(#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.
|
||
intersection_rect = intersect_rectangle(&intersection_rect, &root_bounds)?;
|
||
|
||
// Step 6
|
||
// > Map intersectionRect to the coordinate space of the viewport of the document containing target.
|
||
// Offset the intersectionRect back to the coordinate space of target's document.
|
||
intersection_rect.origin -= total_inter_document_offset;
|
||
|
||
// Step 7
|
||
// > Return intersectionRect.
|
||
Some(intersection_rect)
|
||
}
|
||
|
||
/// The values from computing step 2.2.4-2.2.14 in
|
||
/// <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>.
|
||
/// See [`IntersectionObserver::maybe_compute_intersection_output`].
|
||
struct IntersectionObservationOutput {
|
||
pub(crate) threshold_index: i32,
|
||
pub(crate) is_intersecting: bool,
|
||
pub(crate) target_rect: Rect<Au, CSSPixel>,
|
||
pub(crate) intersection_rect: Rect<Au, CSSPixel>,
|
||
pub(crate) intersection_ratio: f64,
|
||
pub(crate) is_visible: bool,
|
||
|
||
/// The root intersection rectangle [`IntersectionObserver::root_intersection_rectangle`].
|
||
/// If the processing is skipped, computation should report the default zero value.
|
||
pub(crate) root_bounds: Rect<Au, CSSPixel>,
|
||
}
|
||
|
||
impl IntersectionObservationOutput {
|
||
/// Default values according to
|
||
/// <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>.
|
||
/// Step 4.
|
||
/// > Let:
|
||
/// > - thresholdIndex be 0.
|
||
/// > - isIntersecting be false.
|
||
/// > - targetRect be a DOMRectReadOnly with x, y, width, and height set to 0.
|
||
/// > - intersectionRect be a DOMRectReadOnly with x, y, width, and height set to 0.
|
||
///
|
||
/// For fields that the default values is not directly mentioned, the values conformant
|
||
/// to current browser implementation or WPT test is used instead.
|
||
fn default_skipped() -> Self {
|
||
Self {
|
||
threshold_index: 0,
|
||
is_intersecting: false,
|
||
target_rect: Rect::zero(),
|
||
intersection_rect: Rect::zero(),
|
||
intersection_ratio: 0.,
|
||
is_visible: false,
|
||
root_bounds: Rect::zero(),
|
||
}
|
||
}
|
||
|
||
fn new_computed(
|
||
threshold_index: i32,
|
||
is_intersecting: bool,
|
||
target_rect: Rect<Au, CSSPixel>,
|
||
intersection_rect: Rect<Au, CSSPixel>,
|
||
intersection_ratio: f64,
|
||
is_visible: bool,
|
||
root_bounds: Rect<Au, CSSPixel>,
|
||
) -> Self {
|
||
Self {
|
||
threshold_index,
|
||
is_intersecting,
|
||
target_rect,
|
||
intersection_rect,
|
||
intersection_ratio,
|
||
is_visible,
|
||
root_bounds,
|
||
}
|
||
}
|
||
}
|