Skip to main content

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