Skip to main content

script/dom/resizeobserver/
resizeobserver.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;
6
7use app_units::Au;
8use dom_struct::dom_struct;
9use euclid::num::Zero;
10use euclid::{Rect, Size2D};
11use html5ever::ns;
12use js::context::{JSContext, NoGC};
13use js::rust::HandleObject;
14use layout_api::BoxAreaType;
15use script_bindings::callback::{RootedCallback, TracedCallback};
16use script_bindings::cell::DomRefCell;
17use script_bindings::reflector::{Reflector, reflect_dom_object_with_proto};
18use style_traits::CSSPixel;
19
20use crate::dom::bindings::callback::ExceptionHandling;
21use crate::dom::bindings::codegen::Bindings::ResizeObserverBinding::{
22    ResizeObserverBoxOptions, ResizeObserverCallback, ResizeObserverMethods, ResizeObserverOptions,
23};
24use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
25use crate::dom::bindings::inheritance::Castable;
26use crate::dom::bindings::root::{Dom, DomRoot};
27use crate::dom::document::RenderingUpdateReason;
28use crate::dom::domrectreadonly::DOMRectReadOnly;
29use crate::dom::element::Element;
30use crate::dom::node::{Node, NodeTraits};
31use crate::dom::resizeobserverentry::ResizeObserverEntry;
32use crate::dom::resizeobserversize::{ResizeObserverSize, ResizeObserverSizeImpl};
33use crate::dom::window::Window;
34
35/// <https://drafts.csswg.org/resize-observer/#calculate-depth-for-node>
36#[derive(Debug, Default, PartialEq, PartialOrd)]
37pub(crate) struct ResizeObservationDepth(usize);
38
39impl ResizeObservationDepth {
40    pub(crate) fn max() -> ResizeObservationDepth {
41        ResizeObservationDepth(usize::MAX)
42    }
43}
44
45/// <https://drafts.csswg.org/resize-observer/#resize-observer-slots>
46/// See `ObservationState` for active and skipped observation targets.
47#[dom_struct]
48pub(crate) struct ResizeObserver {
49    reflector_: Reflector,
50
51    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-callback-slot>
52    callback: TracedCallback<ResizeObserverCallback>,
53
54    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-observationtargets-slot>
55    ///
56    /// This list simultaneously also represents the
57    /// [`[[activeTargets]]`](https://drafts.csswg.org/resize-observer/#dom-resizeobserver-activetargets-slot)
58    /// and [`[[skippedTargets]]`](https://drafts.csswg.org/resize-observer/#dom-resizeobserver-skippedtargets-slot)
59    /// internal slots.
60    observation_targets: DomRefCell<Vec<(ResizeObservation, Dom<Element>)>>,
61}
62
63impl ResizeObserver {
64    pub(crate) fn new_inherited(
65        callback: RootedCallback<ResizeObserverCallback>,
66    ) -> ResizeObserver {
67        ResizeObserver {
68            reflector_: Reflector::new(),
69            callback: callback.to_traced(),
70            observation_targets: Default::default(),
71        }
72    }
73
74    fn new(
75        cx: &mut JSContext,
76        window: &Window,
77        proto: Option<HandleObject>,
78        callback: RootedCallback<ResizeObserverCallback>,
79    ) -> DomRoot<ResizeObserver> {
80        let observer = Box::new(ResizeObserver::new_inherited(callback));
81        reflect_dom_object_with_proto(cx, observer, window, proto)
82    }
83
84    /// Step 2 of <https://drafts.csswg.org/resize-observer/#gather-active-observations-h>
85    ///
86    /// <https://drafts.csswg.org/resize-observer/#has-active-resize-observations>
87    pub(crate) fn gather_active_resize_observations_at_depth(
88        &self,
89        cx: &mut JSContext,
90        depth: &ResizeObservationDepth,
91        has_active: &mut bool,
92    ) {
93        // Step 2.1 Clear observer’s [[activeTargets]], and [[skippedTargets]].
94        // NOTE: This happens as part of Step 2.2
95
96        // Step 2.2 For each observation in observer.[[observationTargets]] run this step:
97        for (observation, target) in self.observation_targets.borrow().iter() {
98            observation.state.set(Default::default());
99
100            // Step 2.2.1 If observation.isActive() is true
101            if observation.is_active(cx, target) {
102                // Step 2.2.1.1 Let targetDepth be result of calculate depth for node for observation.target.
103                let target_depth = calculate_depth_for_node(cx, target);
104
105                // Step 2.2.1.2 If targetDepth is greater than depth then add observation to [[activeTargets]].
106                if target_depth > *depth {
107                    observation.state.set(ObservationState::Active);
108                    *has_active = true;
109                }
110                // Step 2.2.1.3 Else add observation to [[skippedTargets]].
111                else {
112                    observation.state.set(ObservationState::Skipped);
113                }
114            }
115        }
116    }
117
118    /// Step 2 of <https://drafts.csswg.org/resize-observer/#broadcast-active-resize-observations>
119    pub(crate) fn broadcast_active_resize_observations(
120        &self,
121        cx: &mut JSContext,
122        shallowest_target_depth: &mut ResizeObservationDepth,
123    ) {
124        // Step 2.1 If observer.[[activeTargets]] slot is empty, continue.
125        // NOTE: Due to the way we implement the activeTarges internal slot we can't easily
126        // know if it's empty. Instead we remember whether there were any active observation
127        // targets during the following traversal and return if there were none.
128        let mut has_active_observation_targets = false;
129
130        // Step 2.2 Let entries be an empty list of ResizeObserverEntryies.
131        let mut entries: Vec<DomRoot<ResizeObserverEntry>> = Default::default();
132
133        // Step 2.3 For each observation in [[activeTargets]] perform these steps:
134        for (observation, target) in self.observation_targets.borrow().iter() {
135            let ObservationState::Active = observation.state.get() else {
136                continue;
137            };
138            has_active_observation_targets = true;
139
140            let window = target.owner_window();
141            let entry = create_and_populate_a_resizeobserverentry(cx, &window, target, observation);
142            entries.push(entry);
143            observation.state.set(ObservationState::Done);
144
145            let target_depth = calculate_depth_for_node(cx.no_gc(), target);
146            if target_depth < *shallowest_target_depth {
147                *shallowest_target_depth = target_depth;
148            }
149        }
150
151        if !has_active_observation_targets {
152            return;
153        }
154
155        // Step 2.4 Invoke observer.[[callback]] with entries.
156        let _ = self
157            .callback
158            .Call_(cx, self, entries, self, ExceptionHandling::Report);
159
160        // Step 2.5 Clear observer.[[activeTargets]].
161        // NOTE: The observation state was modified in Step 2.2
162    }
163
164    /// <https://drafts.csswg.org/resize-observer/#has-skipped-observations-h>
165    pub(crate) fn has_skipped_resize_observations(&self) -> bool {
166        self.observation_targets
167            .borrow()
168            .iter()
169            .any(|(observation, _)| observation.state.get() == ObservationState::Skipped)
170    }
171}
172
173/// <https://drafts.csswg.org/resize-observer/#create-and-populate-a-resizeobserverentry>
174fn create_and_populate_a_resizeobserverentry(
175    cx: &mut JSContext,
176    window: &Window,
177    target: &Element,
178    observation: &ResizeObservation,
179) -> DomRoot<ResizeObserverEntry> {
180    // Step 3. Set this.borderBoxSize slot to result of calculating box size given target and observedBox of "border-box".
181    let border_box_size = calculate_box_size(cx, target, &ResizeObserverBoxOptions::Border_box);
182    // Step 4. Set this.contentBoxSize slot to result of calculating box size given target and observedBox of "content-box".
183    let content_box_size = calculate_box_size(cx, target, &ResizeObserverBoxOptions::Content_box);
184
185    // Step 5. Set this.devicePixelContentBoxSize slot to result of calculating box size given target and observedBox of "device-pixel-content-box".
186    let device_pixel_content_box = calculate_box_size(
187        cx,
188        target,
189        &ResizeObserverBoxOptions::Device_pixel_content_box,
190    );
191
192    // Note: this is safe because an observation is
193    // initialized with one reported size (zero).
194    // The spec plans to store multiple reported sizes,
195    // but for now there can be only one.
196    let last_size = match observation.observed_box {
197        ResizeObserverBoxOptions::Content_box => content_box_size,
198        ResizeObserverBoxOptions::Border_box => border_box_size,
199        ResizeObserverBoxOptions::Device_pixel_content_box => device_pixel_content_box,
200    };
201    let last_reported_size = ResizeObserverSizeImpl::new(last_size.width(), last_size.height());
202
203    {
204        let mut sizes = observation.last_reported_sizes.safe_borrow_mut(cx.no_gc());
205        if sizes.is_empty() {
206            sizes.push(last_reported_size);
207        } else {
208            sizes[0] = last_reported_size;
209        }
210    }
211
212    // Step 7. If target is not an SVG element or target is an SVG element with an associated CSS layout box do these steps:
213    let use_padding = *target.namespace() != ns!(svg) || target.has_css_layout_box();
214    let (padding_top, padding_left) = if use_padding {
215        // Step 7.1. Set this.contentRect.top to target.padding top.
216        // Step 7.2. Set this.contentRect.left to target.padding left.
217        let padding = target.upcast::<Node>().padding().unwrap_or_default();
218        (padding.top, padding.left)
219    } else {
220        // Step 8. If target is an SVG element without an associated CSS layout box do these steps:
221        // Step 8.1. Set this.contentRect.top and this.contentRect.left to 0.
222        (Au::zero(), Au::zero())
223    };
224
225    // Step 6. Set this.contentRect to logical this.contentBoxSize given target and observedBox of "content-box".
226    let content_rect = DOMRectReadOnly::new(
227        cx,
228        window.upcast(),
229        None,
230        padding_left.to_f64_px(),
231        padding_top.to_f64_px(),
232        content_box_size.width(),
233        content_box_size.height(),
234    );
235
236    let border_box_size = ResizeObserverSize::new(
237        cx,
238        window,
239        ResizeObserverSizeImpl::new(border_box_size.width(), border_box_size.height()),
240    );
241    let content_box_size = ResizeObserverSize::new(
242        cx,
243        window,
244        ResizeObserverSizeImpl::new(content_box_size.width(), content_box_size.height()),
245    );
246    let device_pixel_content_box = ResizeObserverSize::new(
247        cx,
248        window,
249        ResizeObserverSizeImpl::new(
250            device_pixel_content_box.width(),
251            device_pixel_content_box.height(),
252        ),
253    );
254
255    // Step 1. Let this be a new ResizeObserverEntry.
256    // Step 2. Set this.target slot to target.
257    ResizeObserverEntry::new(
258        cx,
259        window,
260        target,
261        &content_rect,
262        &[&*border_box_size],
263        &[&*content_box_size],
264        &[&*device_pixel_content_box],
265    )
266}
267
268impl ResizeObserverMethods<crate::DomTypeHolder> for ResizeObserver {
269    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-resizeobserver>
270    fn Constructor(
271        cx: &mut JSContext,
272        window: &Window,
273        proto: Option<HandleObject>,
274        callback: RootedCallback<ResizeObserverCallback>,
275    ) -> DomRoot<ResizeObserver> {
276        let rooted_observer = ResizeObserver::new(cx, window, proto, callback);
277        let document = window.Document();
278        document.add_resize_observer(cx, &rooted_observer);
279        rooted_observer
280    }
281
282    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-observe>
283    fn Observe(&self, no_gc: &NoGC, target: &Element, options: &ResizeObserverOptions) {
284        // Step 1. If target is in [[observationTargets]] slot, call unobserve() with argument target.
285        let is_present = self
286            .observation_targets
287            .borrow()
288            .iter()
289            .any(|(_obs, other)| &**other == target);
290        if is_present {
291            self.Unobserve(no_gc, target);
292        }
293
294        // Step 2. Let observedBox be the value of the box dictionary member of options.
295        // Step 3. Let resizeObservation be new ResizeObservation(target, observedBox).
296        let resize_observation = ResizeObservation::new(options.box_);
297
298        // Step 4. Add the resizeObservation to the [[observationTargets]] slot.
299        self.observation_targets
300            .safe_borrow_mut(no_gc)
301            .push((resize_observation, Dom::from_ref(target)));
302        target
303            .owner_window()
304            .Document()
305            .add_rendering_update_reason(
306                RenderingUpdateReason::ResizeObserverStartedObservingTarget,
307            );
308    }
309
310    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-unobserve>
311    fn Unobserve(&self, no_gc: &NoGC, target: &Element) {
312        self.observation_targets
313            .safe_borrow_mut(no_gc)
314            .retain_mut(|(_obs, other)| !(&**other == target));
315    }
316
317    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-disconnect>
318    fn Disconnect(&self, no_gc: &NoGC) {
319        self.observation_targets.safe_borrow_mut(no_gc).clear();
320    }
321}
322
323/// State machine equivalent of active and skipped observations.
324#[derive(Copy, Clone, Default, MallocSizeOf, PartialEq)]
325enum ObservationState {
326    #[default]
327    Done,
328    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-activetargets-slot>
329    Active,
330    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-skippedtargets-slot>
331    Skipped,
332}
333
334/// <https://drafts.csswg.org/resize-observer/#resizeobservation>
335///
336/// Note: `target` is kept out of here, to avoid having to root the `ResizeObservation`.
337/// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-target>
338#[derive(JSTraceable, MallocSizeOf)]
339struct ResizeObservation {
340    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-observedbox>
341    observed_box: ResizeObserverBoxOptions,
342    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-lastreportedsizes>
343    last_reported_sizes: DomRefCell<Vec<ResizeObserverSizeImpl>>,
344    /// State machine mimicking the "active" and "skipped" targets slots of the observer.
345    #[no_trace]
346    state: Cell<ObservationState>,
347}
348
349impl ResizeObservation {
350    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-resizeobservation>
351    pub(crate) fn new(observed_box: ResizeObserverBoxOptions) -> ResizeObservation {
352        ResizeObservation {
353            observed_box,
354            last_reported_sizes: DomRefCell::new(vec![]),
355            state: Default::default(),
356        }
357    }
358
359    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-isactive>
360    fn is_active(&self, cx: &mut JSContext, target: &Element) -> bool {
361        let Some(last_reported_size) = self.last_reported_sizes.borrow().first().copied() else {
362            return true;
363        };
364        let box_size = calculate_box_size(cx, target, &self.observed_box);
365        box_size.width() != last_reported_size.inline_size() ||
366            box_size.height() != last_reported_size.block_size()
367    }
368}
369
370/// <https://drafts.csswg.org/resize-observer/#calculate-depth-for-node>
371fn calculate_depth_for_node(no_gc: &NoGC, target: &Element) -> ResizeObservationDepth {
372    let node = target.upcast::<Node>();
373    let depth = node
374        .inclusive_ancestors_in_flat_tree_unrooted(no_gc)
375        .count();
376    ResizeObservationDepth(depth)
377}
378
379/// <https://drafts.csswg.org/resize-observer/#calculate-box-size>
380///
381/// The dimensions of the returned `Rect` depend on the type of box being observed.
382/// For `ResizeObserverBoxOptions::Content_box` and `ResizeObserverBoxOptions::Border_box`,
383/// the values will be in `px`. For `ResizeObserverBoxOptions::Device_pixel_content_box` they
384/// will be in integral device pixels.
385fn calculate_box_size(
386    cx: &mut JSContext,
387    target: &Element,
388    observed_box: &ResizeObserverBoxOptions,
389) -> Rect<f64, CSSPixel> {
390    match observed_box {
391        ResizeObserverBoxOptions::Content_box => {
392            // Note: only taking first fragment,
393            // but the spec will expand to cover all fragments.
394            let content_box = target
395                .owner_window()
396                .box_area_query(cx, target.upcast(), BoxAreaType::Content, true)
397                .unwrap_or_else(Rect::zero);
398
399            Rect::new(
400                content_box.origin.map(|coordinate| coordinate.to_f64_px()),
401                Size2D::new(
402                    content_box.size.width.to_f64_px(),
403                    content_box.size.height.to_f64_px(),
404                ),
405            )
406        },
407        ResizeObserverBoxOptions::Border_box => {
408            // Note: only taking first fragment,
409            // but the spec will expand to cover all fragments.
410            let border_box = target
411                .owner_window()
412                .box_area_query(cx, target.upcast(), BoxAreaType::Border, true)
413                .unwrap_or_else(Rect::zero);
414
415            Rect::new(
416                border_box.origin.map(|coordinate| coordinate.to_f64_px()),
417                Size2D::new(
418                    border_box.size.width.to_f64_px(),
419                    border_box.size.height.to_f64_px(),
420                ),
421            )
422        },
423        ResizeObserverBoxOptions::Device_pixel_content_box => {
424            let device_pixel_ratio = target.owner_window().device_pixel_ratio().get() as f64;
425            let content_box = target
426                .owner_window()
427                .box_area_query(cx, target.upcast(), BoxAreaType::Content, true)
428                .unwrap_or_else(Rect::zero);
429
430            let to_device_px = |length: Au| (length.to_f64_px() * device_pixel_ratio).round();
431            Rect::new(
432                content_box.origin.map(to_device_px),
433                Size2D::new(
434                    to_device_px(content_box.size.width),
435                    to_device_px(content_box.size.height),
436                ),
437            )
438        },
439    }
440}
441
442impl script_bindings::callback::OwnerWindow<crate::DomTypeHolder> for ResizeObserver {}