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