script/dom/intersectionobserver/intersectionobserver.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{Cell, RefCell};
6use std::time::Duration;
7
8use app_units::Au;
9use cssparser::Parser;
10use dom_struct::dom_struct;
11use euclid::{Rect, SideOffsets2D, Size2D, Vector2D};
12use js::context::{JSContext, NoGC};
13use js::rust::{HandleObject, MutableHandleValue};
14use script_bindings::callback::{RootedCallback, TracedCallback};
15use script_bindings::cell::DomRefCell;
16use script_bindings::reflector::{Reflector, reflect_dom_object_with_proto};
17use servo_base::cross_process_instant::CrossProcessInstant;
18use servo_geometry::f32_rect_to_au_rect;
19use style::parser::Parse;
20use style::stylesheets::CssRuleType;
21use style::values::computed::Overflow;
22use style::values::specified::intersection_observer::IntersectionObserverMargin;
23use style_traits::{CSSPixel, ParsingMode, ToCss};
24
25use crate::css::css::{ANONYMOUS_CONTENT_URL_DATA, parser_context_for_anonymous_content};
26use crate::dom::bindings::callback::ExceptionHandling;
27use crate::dom::bindings::codegen::Bindings::IntersectionObserverBinding::{
28 IntersectionObserverCallback, IntersectionObserverInit, IntersectionObserverMethods,
29};
30use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
31use crate::dom::bindings::codegen::UnionTypes::{DoubleOrDoubleSequence, ElementOrDocument};
32use crate::dom::bindings::error::{Error, Fallible};
33use crate::dom::bindings::inheritance::Castable;
34use crate::dom::bindings::num::Finite;
35use crate::dom::bindings::root::{Dom, DomRoot};
36use crate::dom::bindings::str::DOMString;
37use crate::dom::bindings::utils::to_frozen_array;
38use crate::dom::document::{Document, RenderingUpdateReason};
39use crate::dom::domrectreadonly::DOMRectReadOnly;
40use crate::dom::element::Element;
41use crate::dom::intersectionobserverentry::IntersectionObserverEntry;
42use crate::dom::node::{Node, NodeTraits};
43use crate::dom::window::Window;
44
45#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
46#[derive(JSTraceable, MallocSizeOf)]
47pub(crate) enum UnrootedElementOrDocument {
48 Element(Dom<Element>),
49 Document(Dom<Document>),
50}
51
52impl From<&ElementOrDocument> for UnrootedElementOrDocument {
53 fn from(value: &ElementOrDocument) -> Self {
54 match value {
55 ElementOrDocument::Document(document) => {
56 UnrootedElementOrDocument::Document(document.as_traced())
57 },
58 ElementOrDocument::Element(element) => {
59 UnrootedElementOrDocument::Element(element.as_traced())
60 },
61 }
62 }
63}
64
65impl From<&UnrootedElementOrDocument> for ElementOrDocument {
66 fn from(value: &UnrootedElementOrDocument) -> Self {
67 match value {
68 UnrootedElementOrDocument::Document(document) => {
69 ElementOrDocument::Document(document.as_rooted())
70 },
71 UnrootedElementOrDocument::Element(element) => {
72 ElementOrDocument::Element(element.as_rooted())
73 },
74 }
75 }
76}
77
78/// > The intersection root for an IntersectionObserver is the value of its root attribute if the attribute is non-null;
79/// > otherwise, it is the top-level browsing context’s document node, referred to as the implicit root.
80///
81/// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-intersection-root>
82pub type IntersectionRoot = Option<UnrootedElementOrDocument>;
83
84/// The Intersection Observer interface
85///
86/// > The IntersectionObserver interface can be used to observe changes in the intersection
87/// > of an intersection root and one or more target Elements.
88///
89/// <https://w3c.github.io/IntersectionObserver/#intersection-observer-interface>
90#[dom_struct]
91pub(crate) struct IntersectionObserver {
92 reflector_: Reflector,
93
94 /// [`Document`] that should process this observer's observation steps.
95 /// Following Chrome and Firefox, it is the current document on construction.
96 /// <https://github.com/w3c/IntersectionObserver/issues/525>
97 owner_doc: Dom<Document>,
98
99 /// > The root provided to the IntersectionObserver constructor, or null if none was provided.
100 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-root>
101 root: IntersectionRoot,
102
103 /// > This callback will be invoked when there are changes to a target’s intersection
104 /// > with the intersection root, as per the processing model.
105 ///
106 /// <https://w3c.github.io/IntersectionObserver/#intersection-observer-callback>
107 callback: TracedCallback<IntersectionObserverCallback>,
108
109 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-queuedentries-slot>
110 queued_entries: DomRefCell<Vec<Dom<IntersectionObserverEntry>>>,
111
112 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observationtargets-slot>
113 observation_targets: DomRefCell<Vec<Dom<Element>>>,
114
115 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin-slot>
116 #[no_trace]
117 #[ignore_malloc_size_of = "Defined in style"]
118 root_margin: RefCell<IntersectionObserverMargin>,
119
120 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-scrollmargin-slot>
121 #[no_trace]
122 #[ignore_malloc_size_of = "Defined in style"]
123 scroll_margin: RefCell<IntersectionObserverMargin>,
124
125 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-thresholds-slot>
126 thresholds: RefCell<Vec<Finite<f64>>>,
127
128 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-delay-slot>
129 delay: Cell<i32>,
130
131 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-trackvisibility-slot>
132 track_visibility: Cell<bool>,
133
134 /// Whether or not this [`IntersectionObserver`] is connected to its owning [`Document`].
135 connected_to_document: Cell<bool>,
136}
137
138impl IntersectionObserver {
139 fn new_inherited(
140 window: &Window,
141 callback: RootedCallback<IntersectionObserverCallback>,
142 root: &Option<ElementOrDocument>,
143 root_margin: IntersectionObserverMargin,
144 scroll_margin: IntersectionObserverMargin,
145 ) -> Self {
146 Self {
147 reflector_: Reflector::new(),
148 owner_doc: window.Document().as_traced(),
149 root: root.as_ref().map(|root| root.into()),
150 callback: callback.to_traced(),
151 queued_entries: Default::default(),
152 observation_targets: Default::default(),
153 root_margin: RefCell::new(root_margin),
154 scroll_margin: RefCell::new(scroll_margin),
155 thresholds: Default::default(),
156 delay: Default::default(),
157 track_visibility: Default::default(),
158 connected_to_document: Cell::new(false),
159 }
160 }
161
162 /// <https://w3c.github.io/IntersectionObserver/#initialize-new-intersection-observer>
163 fn new(
164 cx: &mut JSContext,
165 window: &Window,
166 proto: Option<HandleObject>,
167 callback: RootedCallback<IntersectionObserverCallback>,
168 init: &IntersectionObserverInit,
169 ) -> Fallible<DomRoot<Self>> {
170 // Step 3.
171 // > Attempt to parse a margin from options.rootMargin. If a list is returned,
172 // > set this’s internal [[rootMargin]] slot to that. Otherwise, throw a SyntaxError exception.
173 let root_margin = if let Ok(margin) = parse_a_margin(init.rootMargin.as_ref()) {
174 margin
175 } else {
176 return Err(Error::Syntax(None));
177 };
178
179 // Step 4.
180 // > Attempt to parse a margin from options.scrollMargin. If a list is returned,
181 // > set this’s internal [[scrollMargin]] slot to that. Otherwise, throw a SyntaxError exception.
182 let scroll_margin = if let Ok(margin) = parse_a_margin(init.scrollMargin.as_ref()) {
183 margin
184 } else {
185 return Err(Error::Syntax(None));
186 };
187
188 // Step 1 and step 2, 3, 4 setter
189 // > 1. Let this be a new IntersectionObserver object
190 // > 2. Set this’s internal [[callback]] slot to callback.
191 // > 3. ... set this’s internal [[rootMargin]] slot to that.
192 // > 4. ... set this’s internal [[scrollMargin]] slot to that.
193 let observer = reflect_dom_object_with_proto(
194 cx,
195 Box::new(Self::new_inherited(
196 window,
197 callback,
198 &init.root,
199 root_margin,
200 scroll_margin,
201 )),
202 window,
203 proto,
204 );
205
206 // Step 5-13
207 observer.init_observer(init)?;
208
209 Ok(observer)
210 }
211
212 /// Step 5-13 of <https://w3c.github.io/IntersectionObserver/#initialize-new-intersection-observer>
213 fn init_observer(&self, init: &IntersectionObserverInit) -> Fallible<()> {
214 // Step 5
215 // > Let thresholds be a list equal to options.threshold.
216 //
217 // Non-sequence value should be converted into Vec.
218 // Default value of thresholds is [0].
219 let mut thresholds = match &init.threshold {
220 Some(DoubleOrDoubleSequence::Double(num)) => vec![*num],
221 Some(DoubleOrDoubleSequence::DoubleSequence(sequence)) => sequence.clone(),
222 None => vec![Finite::wrap(0.)],
223 };
224
225 // Step 6
226 // > If any value in thresholds is less than 0.0 or greater than 1.0, throw a RangeError exception.
227 for num in &thresholds {
228 if **num < 0.0 || **num > 1.0 {
229 return Err(Error::Range(
230 c"Value in thresholds should not be less than 0.0 or greater than 1.0"
231 .to_owned(),
232 ));
233 }
234 }
235
236 // Step 7
237 // > Sort thresholds in ascending order.
238 thresholds.sort_by(|lhs, rhs| lhs.partial_cmp(&**rhs).unwrap());
239
240 // Step 8
241 // > If thresholds is empty, append 0 to thresholds.
242 if thresholds.is_empty() {
243 thresholds.push(Finite::wrap(0.));
244 }
245
246 // Step 9
247 // > The thresholds attribute getter will return this sorted thresholds list.
248 //
249 // Set this internal [[thresholds]] slot to the sorted thresholds list
250 // and getter will return the internal [[thresholds]] slot.
251 self.thresholds.replace(thresholds);
252
253 // Step 10
254 // > Let delay be the value of options.delay.
255 //
256 // Default value of delay is 0.
257 let mut delay = init.delay.unwrap_or(0);
258
259 // Step 11
260 // > If options.trackVisibility is true and delay is less than 100, set delay to 100.
261 //
262 // In Chromium, the minimum delay required is 100 milliseconds for observation that consider trackVisibilty.
263 // Currently, visibility is not implemented.
264 if init.trackVisibility {
265 delay = delay.max(100);
266 }
267
268 // Step 12
269 // > Set this’s internal [[delay]] slot to options.delay to delay.
270 self.delay.set(delay);
271
272 // Step 13
273 // > Set this’s internal [[trackVisibility]] slot to options.trackVisibility.
274 self.track_visibility.set(init.trackVisibility);
275
276 Ok(())
277 }
278
279 /// <https://w3c.github.io/IntersectionObserver/#observe-target-element>
280 fn observe_target_element(&self, target: &Element, no_gc: &NoGC) {
281 // Step 1
282 // > If target is in observer’s internal [[ObservationTargets]] slot, return.
283 let is_present = self
284 .observation_targets
285 .borrow()
286 .iter()
287 .any(|element| &**element == target);
288 if is_present {
289 return;
290 }
291
292 // Step 2
293 // > Let intersectionObserverRegistration be an IntersectionObserverRegistration record with
294 // > an observer property set to observer, a previousThresholdIndex property set to -1,
295 // > a previousIsIntersecting property set to false, and a previousIsVisible property set to false.
296 // Step 3
297 // > Append intersectionObserverRegistration to target’s internal [[RegisteredIntersectionObservers]] slot.
298 target.add_initial_intersection_observer_registration(self, no_gc);
299
300 if self.observation_targets.borrow().is_empty() {
301 self.connect_to_owner();
302 }
303
304 // Step 4
305 // > Add target to observer’s internal [[ObservationTargets]] slot.
306 self.observation_targets
307 .safe_borrow_mut(no_gc)
308 .push(Dom::from_ref(target));
309
310 target
311 .owner_window()
312 .Document()
313 .add_rendering_update_reason(
314 RenderingUpdateReason::IntersectionObserverStartedObservingTarget,
315 );
316 }
317
318 /// <https://w3c.github.io/IntersectionObserver/#unobserve-target-element>
319 fn unobserve_target_element(&self, target: &Element, no_gc: &NoGC) {
320 // Step 1
321 // > Remove the IntersectionObserverRegistration record whose observer property is equal to
322 // > this from target’s internal [[RegisteredIntersectionObservers]] slot, if present.
323 target
324 .registered_intersection_observers_mut(no_gc)
325 .retain(|registration| &*registration.observer != self);
326
327 // Step 2
328 // > Remove target from this’s internal [[ObservationTargets]] slot, if present
329 self.observation_targets
330 .safe_borrow_mut(no_gc)
331 .retain(|element| &**element != target);
332
333 // Should disconnect from owner if it is not observing anything.
334 if self.observation_targets.borrow().is_empty() {
335 self.disconnect_from_owner();
336 }
337 }
338
339 /// <https://w3c.github.io/IntersectionObserver/#queue-an-intersectionobserverentry>
340 #[allow(clippy::too_many_arguments)]
341 fn queue_an_intersectionobserverentry(
342 &self,
343 cx: &mut JSContext,
344 document: &Document,
345 time: CrossProcessInstant,
346 root_bounds: Rect<Au, CSSPixel>,
347 bounding_client_rect: Rect<Au, CSSPixel>,
348 intersection_rect: Rect<Au, CSSPixel>,
349 is_intersecting: bool,
350 is_visible: bool,
351 intersection_ratio: f64,
352 target: &Element,
353 ) {
354 let mut rect_to_domrectreadonly = |rect: Rect<Au, CSSPixel>| {
355 DOMRectReadOnly::new(
356 cx,
357 self.owner_doc.window().as_global_scope(),
358 None,
359 rect.origin.x.to_f64_px(),
360 rect.origin.y.to_f64_px(),
361 rect.size.width.to_f64_px(),
362 rect.size.height.to_f64_px(),
363 )
364 };
365
366 let root_bounds = rect_to_domrectreadonly(root_bounds);
367 let bounding_client_rect = rect_to_domrectreadonly(bounding_client_rect);
368 let intersection_rect = rect_to_domrectreadonly(intersection_rect);
369
370 // Step 1. Construct an IntersectionObserverEntry, passing in time, rootBounds,
371 // > boundingClientRect, intersectionRect, isIntersecting, and target.
372 let time = document
373 .owner_global()
374 .performance(cx)
375 .to_dom_high_res_time_stamp(time);
376 let entry = IntersectionObserverEntry::new(
377 cx,
378 self.owner_doc.window(),
379 None,
380 time,
381 Some(&root_bounds),
382 &bounding_client_rect,
383 &intersection_rect,
384 is_intersecting,
385 is_visible,
386 Finite::wrap(intersection_ratio),
387 target,
388 );
389
390 // Step 2. Append it to observer's internal [[QueuedEntries]] slot.
391 self.queued_entries
392 .safe_borrow_mut(cx.no_gc())
393 .push(entry.as_traced());
394
395 // Step 3. Queue an intersection observer task for document.
396 document.queue_an_intersection_observer_task();
397 }
398
399 /// Step 3.1-3.5 of <https://w3c.github.io/IntersectionObserver/#notify-intersection-observers-algo>
400 pub(crate) fn invoke_callback_if_necessary(&self, cx: &mut js::context::JSContext) {
401 // Step 1
402 // > If observer’s internal [[QueuedEntries]] slot is empty, continue.
403 if self.queued_entries.borrow().is_empty() {
404 return;
405 }
406
407 // Step 2-3
408 // We trivially moved the entries and root them.
409 let queued_entries = self
410 .queued_entries
411 .take()
412 .iter_mut()
413 .map(|entry| entry.as_rooted())
414 .collect();
415
416 // Step 4-5
417 let _ = self
418 .callback
419 .Call_(cx, self, queued_entries, self, ExceptionHandling::Report);
420 }
421
422 /// Connect the observer itself into owner doc if it is unconnected.
423 /// If the [`IntersectionObserver`] is already connected, do nothing.
424 fn connect_to_owner(&self) {
425 if !self.connected_to_document.get() {
426 self.owner_doc.add_intersection_observer(self);
427 self.connected_to_document.set(true);
428 }
429 }
430
431 /// Disconnect the observer itself from owner doc.
432 /// If not connected to a [`Document`], do nothing.
433 fn disconnect_from_owner(&self) {
434 if self.connected_to_document.get() {
435 self.owner_doc.remove_intersection_observer(self);
436 self.connected_to_document.set(false);
437 }
438 }
439
440 /// <https://w3c.github.io/IntersectionObserver/#ref-for-intersectionobserver-content-clip>
441 /// An Element is defined as having a content clip if its computed style has overflow properties
442 /// that cause its content to be clipped to the element’s padding edge.
443 // TODO: this is not clear for `overflow: clip` since it is clipped based on overflow clip rect.
444 fn has_content_clip(element: &Element) -> bool {
445 element
446 .upcast::<Node>()
447 .effective_overflow_without_reflow()
448 .is_some_and(|overflow_axes| {
449 overflow_axes.x != Overflow::Visible || overflow_axes.y != Overflow::Visible
450 })
451 }
452
453 /// > The root intersection rectangle for an IntersectionObserver is
454 /// > the rectangle we’ll use to check against the targets.
455 ///
456 /// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle>
457 pub(crate) fn root_intersection_rectangle(&self) -> Option<Rect<Au, CSSPixel>> {
458 let intersection_rectangle = match self.concrete_root() {
459 // Handle if root is an element.
460 Some(ElementOrDocument::Element(element)) => {
461 // TODO: recheck scrollbar approach and clip-path clipping from Chromium implementation.
462 if IntersectionObserver::has_content_clip(&element) {
463 // > Otherwise, if the intersection root has a content clip, it’s the element’s padding area.
464 element.upcast::<Node>().padding_box_without_reflow()
465 } else {
466 // > Otherwise, it’s the result of getting the bounding box for the intersection root.
467 element.upcast::<Node>().border_box_without_reflow()
468 }
469 },
470 // Handle if root is a Document, which includes implicit root and explicit Document root.
471 Some(ElementOrDocument::Document(document)) => {
472 // > If the intersection root is a document, it’s the size of the document's viewport
473 // > (note that this processing step can only be reached if the document is fully active).
474 // TODO: viewport should consider native scrollbar if exist. Recheck Servo's scrollbar approach.
475 let viewport = document.window().viewport_details().size;
476 Some(Rect::from_size(Size2D::new(
477 Au::from_f32_px(viewport.width),
478 Au::from_f32_px(viewport.height),
479 )))
480 },
481 None => None,
482 };
483
484 // > When calculating the root intersection rectangle for a same-origin-domain target,
485 // > the rectangle is then expanded according to the offsets in the IntersectionObserver’s
486 // > [[rootMargin]] slot in a manner similar to CSS’s margin property, with the four values
487 // > indicating the amount the top, right, bottom, and left edges, respectively, are offset by,
488 // > with positive lengths indicating an outward offset. Percentages are resolved relative to
489 // > the width of the undilated rectangle.
490 // TODO(stevennovaryo): add check for same-origin-domain
491 intersection_rectangle.map(|intersection_rectangle| {
492 let margin = Self::resolve_percentages_with_basis(
493 &self.root_margin.borrow(),
494 intersection_rectangle,
495 );
496 intersection_rectangle.outer_rect(margin)
497 })
498 }
499
500 /// Return root or try to get the top-level browsing context document in case if this is a implicit root.
501 /// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-intersection-root>
502 // TODO: Currently we are unable to get the cross `ScriptThread` document.
503 fn concrete_root(&self) -> Option<ElementOrDocument> {
504 match &self.root {
505 Some(root) => Some(root.into()),
506 None => self
507 .owner_doc
508 .window()
509 .top_level_document_if_local()
510 .map(ElementOrDocument::Document),
511 }
512 }
513
514 /// Step 2.2.4-2.2.21 of <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>
515 ///
516 /// If some conditions require to skips "processing further", we will skips those steps and
517 /// return default values conformant to step 2.2.4. See [`IntersectionObservationOutput::default_skipped`].
518 ///
519 /// Note that current draft specs skipped wrong steps, as it should skip computing fields that
520 /// would result in different intersection entry other than the default entry per published spec.
521 /// <https://www.w3.org/TR/intersection-observer/>
522 fn maybe_compute_intersection_output(
523 &self,
524 cx: &mut JSContext,
525 target: &Element,
526 maybe_root_bounds: Option<Rect<Au, CSSPixel>>,
527 ) -> IntersectionObservationOutput {
528 // Step 5
529 // > If the intersection root is not the implicit root, and target is not in
530 // > the same document as the intersection root, skip to step 11.
531 // Step 6
532 // > If the intersection root is an Element, and target is not a descendant of
533 // > the intersection root in the containing block chain, skip to step 11.
534 match &self.root {
535 Some(UnrootedElementOrDocument::Document(document))
536 if document.as_rooted() != target.owner_document() =>
537 {
538 return IntersectionObservationOutput::default_skipped();
539 },
540 Some(UnrootedElementOrDocument::Element(element)) => {
541 // To ensure consistency, we also check for elements right now, but we can depend on the
542 // layout query later.
543 if element.owner_document() != target.owner_document() {
544 return IntersectionObservationOutput::default_skipped();
545 }
546 if !element
547 .owner_window()
548 .is_containing_block_descendant_query_without_reflow(
549 element.upcast(),
550 target.upcast(),
551 )
552 {
553 return IntersectionObservationOutput::default_skipped();
554 }
555 },
556 _ => {},
557 }
558
559 // Step 7
560 // > Set targetRect to the DOMRectReadOnly obtained by getting the bounding box for target.
561 let maybe_target_rect = target.upcast::<Node>().border_box_without_reflow();
562
563 // Following the implementation of Gecko, we will skip further processing if these
564 // information not available. This would also handle display none element.
565 let (Some(root_bounds), Some(target_rect), Some(root_intersection)) =
566 (maybe_root_bounds, maybe_target_rect, self.concrete_root())
567 else {
568 return IntersectionObservationOutput::default_skipped();
569 };
570
571 // TODO(stevennovaryo): we should probably also consider adding visibity check, ideally
572 // it would require new query from LayoutThread.
573
574 // Step 8
575 // > Let intersectionRect be the result of running the compute the intersection algorithm on
576 // > target and observer’s intersection root.
577 let maybe_intersection_rect = compute_the_intersection(
578 cx,
579 target,
580 &root_intersection,
581 root_bounds,
582 target_rect,
583 &self.scroll_margin.borrow(),
584 );
585 let intersection_rect = maybe_intersection_rect.unwrap_or_default();
586
587 // Step 9
588 // > Let targetArea be targetRect’s area.
589 // Step 10
590 // > Let intersectionArea be intersectionRect’s area.
591 // These steps are folded in Step 12, rewriting (w1 * h1) / (w2 * h2) as (w1 / w2) * (h1 / h2)
592 // to avoid multiplication overflows.
593
594 // Step 11
595 // > Let isIntersecting be true if targetRect and rootBounds intersect or are edge-adjacent,
596 // > even if the intersection has zero area (because rootBounds or targetRect have zero area).
597 // Because we are considering edge-adjacent, instead of checking whether the rectangle is empty,
598 // we are checking whether the rectangle is negative or not.
599 let is_intersecting = maybe_intersection_rect.is_some();
600
601 // Step 12
602 // > If targetArea is non-zero, let intersectionRatio be intersectionArea divided by targetArea.
603 // > Otherwise, let intersectionRatio be 1 if isIntersecting is true, or 0 if isIntersecting is false.
604 let intersection_ratio = if target_rect.size.width.0 == 0 || target_rect.size.height.0 == 0
605 {
606 is_intersecting.into()
607 } else {
608 (intersection_rect.size.width.0 as f64 / target_rect.size.width.0 as f64) *
609 (intersection_rect.size.height.0 as f64 / target_rect.size.height.0 as f64)
610 };
611
612 // Step 13
613 // > Set thresholdIndex to the index of the first entry in observer.thresholds whose value is
614 // > greater than intersectionRatio, or the length of observer.thresholds if intersectionRatio is
615 // > greater than or equal to the last entry in observer.thresholds.
616 let threshold_index = self
617 .thresholds
618 .borrow()
619 .iter()
620 .position(|threshold| **threshold > intersection_ratio)
621 .unwrap_or(self.thresholds.borrow().len());
622
623 // If the index is 0, the first threshold value is greater
624 // than the observed ratio, so we're not actually matching yet.
625 // The spec differentiates between this case and the case where
626 // there is no intersection, but other browser engines do not.
627 let threshold_index = if is_intersecting && threshold_index > 0 {
628 ThresholdIndex::Matching(threshold_index)
629 } else {
630 ThresholdIndex::NotMatching
631 };
632
633 // Step 14
634 // > Let isVisible be the result of running the visibility algorithm on target.
635 // TODO: Implement visibility algorithm
636 let is_visible = false;
637
638 // We never report isIntersecting as true unless we have exceeded a threshold,
639 // which matches other browser eengines.
640 // See https://github.com/w3c/IntersectionObserver/issues/432 for background.
641 let is_intersecting = matches!(threshold_index, ThresholdIndex::Matching(..));
642
643 IntersectionObservationOutput::new_computed(
644 threshold_index,
645 is_intersecting,
646 target_rect,
647 intersection_rect,
648 intersection_ratio,
649 is_visible,
650 root_bounds,
651 )
652 }
653
654 /// Step 2.2.1-2.2.21 of <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>
655 pub(crate) fn update_intersection_observations_steps(
656 &self,
657 cx: &mut JSContext,
658 document: &Document,
659 time: CrossProcessInstant,
660 root_bounds: Option<Rect<Au, CSSPixel>>,
661 ) {
662 for target in &*self.observation_targets.borrow() {
663 // Step 1
664 // > Let registration be the IntersectionObserverRegistration record in target’s internal
665 // > [[RegisteredIntersectionObservers]] slot whose observer property is equal to observer.
666 let registration = target.get_intersection_observer_registration(self).unwrap();
667
668 // Step 2
669 // > If (time - registration.lastUpdateTime < observer.delay), skip further processing for target.
670 if time - registration.last_update_time.get() <
671 Duration::from_millis(self.delay.get().max(0) as u64)
672 {
673 return;
674 }
675
676 // Step 3
677 // > Set registration.lastUpdateTime to time.
678 registration.last_update_time.set(time);
679
680 // step 4-14
681 let intersection_output =
682 self.maybe_compute_intersection_output(cx, target, root_bounds);
683
684 // Step 15-17
685 // > 15. Let previousThresholdIndex be the registration’s previousThresholdIndex property.
686 // > 16. Let previousIsIntersecting be the registration’s previousIsIntersecting property.
687 // > 17. Let previousIsVisible be the registration’s previousIsVisible property.
688 let previous_threshold_index = registration.previous_threshold_index.get();
689 let previous_is_intersecting = registration.previous_is_intersecting.get();
690 let previous_is_visible = registration.previous_is_visible.get();
691
692 // Step 18
693 // > If thresholdIndex does not equal previousThresholdIndex, or
694 // > if isIntersecting does not equal previousIsIntersecting, or
695 // > if isVisible does not equal previousIsVisible,
696 // > queue an IntersectionObserverEntry, passing in observer, time, rootBounds,
697 // > targetRect, intersectionRect, isIntersecting, isVisible, and target.
698 if Some(intersection_output.threshold_index) != previous_threshold_index ||
699 intersection_output.is_intersecting != previous_is_intersecting ||
700 intersection_output.is_visible != previous_is_visible
701 {
702 // TODO(stevennovaryo): Per IntersectionObserverEntry interface, the rootBounds
703 // should be null for cross-origin-domain target.
704 self.queue_an_intersectionobserverentry(
705 cx,
706 document,
707 time,
708 intersection_output.root_bounds,
709 intersection_output.target_rect,
710 intersection_output.intersection_rect,
711 intersection_output.is_intersecting,
712 intersection_output.is_visible,
713 intersection_output.intersection_ratio,
714 target,
715 );
716 }
717
718 // Step 19-21
719 // > 19. Assign thresholdIndex to registration’s previousThresholdIndex property.
720 // > 20. Assign isIntersecting to registration’s previousIsIntersecting property.
721 // > 21. Assign isVisible to registration’s previousIsVisible property.
722 registration
723 .previous_threshold_index
724 .set(Some(intersection_output.threshold_index));
725 registration
726 .previous_is_intersecting
727 .set(intersection_output.is_intersecting);
728 registration
729 .previous_is_visible
730 .set(intersection_output.is_visible);
731 }
732 }
733
734 fn resolve_percentages_with_basis(
735 margin: &IntersectionObserverMargin,
736 containing_block: Rect<Au, CSSPixel>,
737 ) -> SideOffsets2D<Au, CSSPixel> {
738 let inner = &margin.0;
739 SideOffsets2D::new(
740 inner.0.to_used_value(containing_block.height()),
741 inner.1.to_used_value(containing_block.width()),
742 inner.2.to_used_value(containing_block.height()),
743 inner.3.to_used_value(containing_block.width()),
744 )
745 }
746}
747
748impl IntersectionObserverMethods<crate::DomTypeHolder> for IntersectionObserver {
749 /// > The root provided to the IntersectionObserver constructor, or null if none was provided.
750 ///
751 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-root>
752 fn GetRoot(&self) -> Option<ElementOrDocument> {
753 self.root.as_ref().map(|root| root.into())
754 }
755
756 /// > Offsets applied to the root intersection rectangle, effectively growing or
757 /// > shrinking the box that is used to calculate intersections. These offsets are only
758 /// > applied when handling same-origin-domain targets; for cross-origin-domain targets
759 /// > they are ignored.
760 ///
761 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin>
762 fn RootMargin(&self) -> DOMString {
763 self.root_margin.borrow().to_css_string().into()
764 }
765
766 /// > Offsets are applied to scrollports on the path from intersection root to target,
767 /// > effectively growing or shrinking the clip rects used to calculate intersections.
768 ///
769 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-scrollmargin>
770 fn ScrollMargin(&self) -> DOMString {
771 self.scroll_margin.borrow().to_css_string().into()
772 }
773
774 /// > A list of thresholds, sorted in increasing numeric order, where each threshold
775 /// > is a ratio of intersection area to bounding box area of an observed target.
776 /// > Notifications for a target are generated when any of the thresholds are crossed
777 /// > for that target. If no options.threshold was provided to the IntersectionObserver
778 /// > constructor, or the sequence is empty, the value of this attribute will be `[0]`.
779 ///
780 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-thresholds>
781 fn Thresholds(&self, cx: &mut JSContext, retval: MutableHandleValue) {
782 to_frozen_array(cx, &self.thresholds.borrow(), retval);
783 }
784
785 /// > A number indicating the minimum delay in milliseconds between notifications from
786 /// > this observer for a given target.
787 ///
788 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-delay>
789 fn Delay(&self) -> i32 {
790 self.delay.get()
791 }
792
793 /// > A boolean indicating whether this IntersectionObserver will track changes in a target’s visibility.
794 ///
795 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-trackvisibility>
796 fn TrackVisibility(&self) -> bool {
797 self.track_visibility.get()
798 }
799
800 /// > Run the observe a target Element algorithm, providing this and target.
801 ///
802 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observe>
803 fn Observe(&self, no_gc: &NoGC, target: &Element) {
804 self.observe_target_element(target, no_gc);
805 }
806
807 /// > Run the unobserve a target Element algorithm, providing this and target.
808 ///
809 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-unobserve>
810 fn Unobserve(&self, no_gc: &NoGC, target: &Element) {
811 self.unobserve_target_element(target, no_gc);
812 }
813
814 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-disconnect>
815 fn Disconnect(&self, no_gc: &NoGC) {
816 // > For each target in this’s internal [[ObservationTargets]] slot:
817 self.observation_targets.borrow().iter().for_each(|target| {
818 // > 1. Remove the IntersectionObserverRegistration record whose observer property is equal to
819 // > this from target’s internal [[RegisteredIntersectionObservers]] slot.
820 target.remove_intersection_observer(self, no_gc);
821 });
822 // > 2. Remove target from this’s internal [[ObservationTargets]] slot.
823 self.observation_targets.safe_borrow_mut(no_gc).clear();
824
825 // We should remove this observer from the event loop.
826 self.disconnect_from_owner();
827 }
828
829 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-takerecords>
830 fn TakeRecords(&self) -> Vec<DomRoot<IntersectionObserverEntry>> {
831 // Step 1-3.
832 self.queued_entries
833 .take()
834 .iter()
835 .map(|entry| entry.as_rooted())
836 .collect()
837 }
838
839 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-intersectionobserver>
840 fn Constructor(
841 cx: &mut JSContext,
842 window: &Window,
843 proto: Option<HandleObject>,
844 callback: RootedCallback<IntersectionObserverCallback>,
845 init: &IntersectionObserverInit,
846 ) -> Fallible<DomRoot<IntersectionObserver>> {
847 Self::new(cx, window, proto, callback, init)
848 }
849}
850
851#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
852enum ThresholdIndex {
853 NotMatching,
854 Matching(usize),
855}
856
857/// <https://w3c.github.io/IntersectionObserver/#intersectionobserverregistration>
858#[derive(Clone, JSTraceable, MallocSizeOf)]
859#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
860pub(crate) struct IntersectionObserverRegistration {
861 pub(crate) observer: Dom<IntersectionObserver>,
862 previous_threshold_index: Cell<Option<ThresholdIndex>>,
863 previous_is_intersecting: Cell<bool>,
864 #[no_trace]
865 last_update_time: Cell<CrossProcessInstant>,
866 previous_is_visible: Cell<bool>,
867}
868
869impl IntersectionObserverRegistration {
870 /// Initial value of [`IntersectionObserverRegistration`] according to
871 /// step 2 of <https://w3c.github.io/IntersectionObserver/#observe-target-element>.
872 /// > Let intersectionObserverRegistration be an IntersectionObserverRegistration record with
873 /// > an observer property set to observer, a previousThresholdIndex property set to -1,
874 /// > a previousIsIntersecting property set to false, and a previousIsVisible property set to false.
875 pub(crate) fn new_initial(observer: &IntersectionObserver) -> Self {
876 IntersectionObserverRegistration {
877 observer: Dom::from_ref(observer),
878 previous_threshold_index: Cell::new(None),
879 previous_is_intersecting: Cell::new(false),
880 last_update_time: Cell::new(CrossProcessInstant::epoch()),
881 previous_is_visible: Cell::new(false),
882 }
883 }
884}
885
886/// <https://w3c.github.io/IntersectionObserver/#parse-a-margin>
887fn parse_a_margin(value: Option<&DOMString>) -> Result<IntersectionObserverMargin, ()> {
888 // <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserverinit-rootmargin> &&
889 // <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserverinit-scrollmargin>
890 // > ... defaulting to "0px".
891 let value = match value {
892 Some(str) => &str.str(),
893 _ => "0px",
894 };
895
896 // Create necessary style ParserContext and utilize stylo's IntersectionObserverMargin
897 let mut parser = Parser::new(value);
898
899 let context = parser_context_for_anonymous_content(
900 CssRuleType::Style,
901 ParsingMode::DEFAULT,
902 &ANONYMOUS_CONTENT_URL_DATA,
903 );
904
905 parser
906 .parse_entirely(|p| IntersectionObserverMargin::parse(&context, p))
907 .map_err(|_| ())
908}
909
910/// In terms of intersection observer, we consider zero-area rectangles as long as the area is not negative.
911fn intersect_rectangle(
912 lhs: &Rect<Au, CSSPixel>,
913 rhs: &Rect<Au, CSSPixel>,
914) -> Option<Rect<Au, CSSPixel>> {
915 let box_result = lhs.to_box2d().intersection_unchecked(&rhs.to_box2d());
916 if box_result.is_negative() {
917 None
918 } else {
919 Some(box_result.to_rect())
920 }
921}
922
923/// Compute the intersection rectangle of the target [`Element`] returning the results of intersection in the coordinate
924/// space of the target's owning [`Document`]. Additionally, we assume that both the target and the root is connected.
925/// <https://w3c.github.io/IntersectionObserver/#compute-the-intersection>
926fn compute_the_intersection(
927 cx: &mut JSContext,
928 target: &Element,
929 root: &ElementOrDocument,
930 root_bounds: Rect<Au, CSSPixel>,
931 mut intersection_rect: Rect<Au, CSSPixel>,
932 scroll_margin: &IntersectionObserverMargin,
933) -> Option<Rect<Au, CSSPixel>> {
934 // > 1. Let intersectionRect be the result of getting the bounding box for target.
935 // We had delegated the computation of this to the caller of the function.
936
937 // > 2. Let container be the containing block of target.
938 let mut container = match target
939 .upcast::<Node>()
940 .containing_block_node_without_reflow()
941 {
942 Some(node) => ElementOrDocument::Element(DomRoot::downcast(node).unwrap()),
943 None => ElementOrDocument::Document(target.owner_document()),
944 };
945
946 // Total offsets gained from traversing through multiple navigables. We use this to map the coordinate space.
947 // TODO: We should store the product sum of transformation matrices instead. But this should be enough to handle
948 // scrolling, simple translation, and offset from containing block.
949 let mut total_inter_document_offset = Vector2D::zero();
950
951 // > 3. While container is not root:
952 while container != *root {
953 let containing_element = match container {
954 ElementOrDocument::Document(ref containing_document) => {
955 // > 3.1. If container is the document of a nested browsing context, update intersectionRect by clipping
956 // > to the viewport of the document, and update container to be the browsing context container of container.
957 if let Some(frame_container) = containing_document
958 .browsing_context()
959 .and_then(|window| window.frame_element().map(DomRoot::from_ref))
960 {
961 let viewport_rect = f32_rect_to_au_rect(Rect::from_size(
962 containing_document.window().viewport_details().size,
963 ));
964
965 intersection_rect = intersect_rectangle(&intersection_rect, &viewport_rect)?;
966
967 let current_offset = frame_container
968 .upcast::<Node>()
969 .padding_box(cx)
970 .unwrap()
971 .origin
972 .to_vector();
973 intersection_rect.origin += current_offset;
974 total_inter_document_offset += current_offset;
975
976 frame_container
977 } else {
978 // TODO: Theoritically, this shouldn't be reachable as we have ensured that the root is reachable
979 // in the previous steps. But we are still unable to iterate through cross-origin ancestor iframes,
980 // and we will need to stop the iteration for that case.
981 break;
982 }
983 },
984 ElementOrDocument::Element(ref root) => root.clone(),
985 };
986
987 // > 3.2. Map intersectionRect to the coordinate space of container.
988 // TODO(#35767): We don't map the coordinate space per each iteration yet, instead all the rectangles are
989 // in the viewport coordinate space. But this would cause the scroll margin calculation to be inaccurate
990 // with respect to transforms.
991
992 // > 3.3. If container is a scroll container, apply the IntersectionObserver’s [[scrollMargin]]
993 // > to the container’s clip rect as described in apply scroll margin to a scrollport.
994 // > 3.4. If container has a content clip or a css clip-path property, update intersectionRect
995 // > by applying container’s clip.
996 // TODO(#35767): handle `overflow: clip` and resolve clipping for x-axis and y-axis independently.
997 // Additionally, handle css `clip-path` as well.
998 if IntersectionObserver::has_content_clip(&containing_element) &&
999 let Some(container_padding_box) = containing_element
1000 .upcast::<Node>()
1001 .padding_box_without_reflow()
1002 {
1003 let container_padding_box =
1004 if containing_element.establishes_scroll_container_without_reflow() {
1005 let margin = IntersectionObserver::resolve_percentages_with_basis(
1006 scroll_margin,
1007 container_padding_box,
1008 );
1009 container_padding_box.outer_rect(margin)
1010 } else {
1011 container_padding_box
1012 };
1013
1014 intersection_rect = intersect_rectangle(&intersection_rect, &container_padding_box)?;
1015 }
1016
1017 // > 3.5. If container is the root element of a browsing context, update container to be the
1018 // > browsing context’s document; otherwise, update container to be the containing block
1019 // > of container.
1020 // Additionally, for a node that doesn't have an element that establishes its containing block, we should
1021 // refer to the browsing context's document.
1022 container = match containing_element
1023 .upcast::<Node>()
1024 .containing_block_node_without_reflow()
1025 .and_then(DomRoot::downcast::<Element>)
1026 {
1027 Some(element) => ElementOrDocument::Element(element),
1028 None => ElementOrDocument::Document(containing_element.owner_document()),
1029 };
1030 }
1031
1032 // Step 4
1033 // > Map intersectionRect to the coordinate space of root.
1034 // TODO(#35767): we don't map the coordinate space per each iteration yet, instead all the rectangles are
1035 // in the viewport coordinate space.
1036
1037 // Step 5
1038 // > Update intersectionRect by intersecting it with the root intersection rectangle.
1039 intersection_rect = intersect_rectangle(&intersection_rect, &root_bounds)?;
1040
1041 // Step 6
1042 // > Map intersectionRect to the coordinate space of the viewport of the document containing target.
1043 // Offset the intersectionRect back to the coordinate space of target's document.
1044 intersection_rect.origin -= total_inter_document_offset;
1045
1046 // Step 7
1047 // > Return intersectionRect.
1048 Some(intersection_rect)
1049}
1050
1051/// The values from computing step 2.2.4-2.2.14 in
1052/// <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>.
1053/// See [`IntersectionObserver::maybe_compute_intersection_output`].
1054struct IntersectionObservationOutput {
1055 pub(crate) threshold_index: ThresholdIndex,
1056 pub(crate) is_intersecting: bool,
1057 pub(crate) target_rect: Rect<Au, CSSPixel>,
1058 pub(crate) intersection_rect: Rect<Au, CSSPixel>,
1059 pub(crate) intersection_ratio: f64,
1060 pub(crate) is_visible: bool,
1061
1062 /// The root intersection rectangle [`IntersectionObserver::root_intersection_rectangle`].
1063 /// If the processing is skipped, computation should report the default zero value.
1064 pub(crate) root_bounds: Rect<Au, CSSPixel>,
1065}
1066
1067impl IntersectionObservationOutput {
1068 /// Default values according to
1069 /// <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>.
1070 /// Step 4.
1071 /// > Let:
1072 /// > - thresholdIndex be 0.
1073 /// > - isIntersecting be false.
1074 /// > - targetRect be a DOMRectReadOnly with x, y, width, and height set to 0.
1075 /// > - intersectionRect be a DOMRectReadOnly with x, y, width, and height set to 0.
1076 ///
1077 /// For fields that the default values is not directly mentioned, the values conformant
1078 /// to current browser implementation or WPT test is used instead.
1079 fn default_skipped() -> Self {
1080 Self {
1081 threshold_index: ThresholdIndex::NotMatching,
1082 is_intersecting: false,
1083 target_rect: Rect::zero(),
1084 intersection_rect: Rect::zero(),
1085 intersection_ratio: 0.,
1086 is_visible: false,
1087 root_bounds: Rect::zero(),
1088 }
1089 }
1090
1091 fn new_computed(
1092 threshold_index: ThresholdIndex,
1093 is_intersecting: bool,
1094 target_rect: Rect<Au, CSSPixel>,
1095 intersection_rect: Rect<Au, CSSPixel>,
1096 intersection_ratio: f64,
1097 is_visible: bool,
1098 root_bounds: Rect<Au, CSSPixel>,
1099 ) -> Self {
1100 Self {
1101 threshold_index,
1102 is_intersecting,
1103 target_rect,
1104 intersection_rect,
1105 intersection_ratio,
1106 is_visible,
1107 root_bounds,
1108 }
1109 }
1110}
1111
1112impl script_bindings::callback::OwnerWindow<crate::DomTypeHolder> for IntersectionObserver {}