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