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