Skip to main content

script/dom/intersectionobserver/
intersectionobserver.rs

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