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        no_gc: &NoGC,
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(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(no_gc, 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(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(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 =
187        calculate_box_size(target, &ResizeObserverBoxOptions::Device_pixel_content_box);
188
189    // Note: this is safe because an observation is
190    // initialized with one reported size (zero).
191    // The spec plans to store multiple reported sizes,
192    // but for now there can be only one.
193    let last_size = match observation.observed_box {
194        ResizeObserverBoxOptions::Content_box => content_box_size,
195        ResizeObserverBoxOptions::Border_box => border_box_size,
196        ResizeObserverBoxOptions::Device_pixel_content_box => device_pixel_content_box,
197    };
198    let last_reported_size = ResizeObserverSizeImpl::new(last_size.width(), last_size.height());
199
200    {
201        let mut sizes = observation.last_reported_sizes.safe_borrow_mut(cx.no_gc());
202        if sizes.is_empty() {
203            sizes.push(last_reported_size);
204        } else {
205            sizes[0] = last_reported_size;
206        }
207    }
208
209    // Step 7. If target is not an SVG element or target is an SVG element with an associated CSS layout box do these steps:
210    let use_padding = *target.namespace() != ns!(svg) || target.has_css_layout_box();
211    let (padding_top, padding_left) = if use_padding {
212        // Step 7.1. Set this.contentRect.top to target.padding top.
213        // Step 7.2. Set this.contentRect.left to target.padding left.
214        let padding = target.upcast::<Node>().padding().unwrap_or_default();
215        (padding.top, padding.left)
216    } else {
217        // Step 8. If target is an SVG element without an associated CSS layout box do these steps:
218        // Step 8.1. Set this.contentRect.top and this.contentRect.left to 0.
219        (Au::zero(), Au::zero())
220    };
221
222    // Step 6. Set this.contentRect to logical this.contentBoxSize given target and observedBox of "content-box".
223    let content_rect = DOMRectReadOnly::new(
224        cx,
225        window.upcast(),
226        None,
227        padding_left.to_f64_px(),
228        padding_top.to_f64_px(),
229        content_box_size.width(),
230        content_box_size.height(),
231    );
232
233    let border_box_size = ResizeObserverSize::new(
234        cx,
235        window,
236        ResizeObserverSizeImpl::new(border_box_size.width(), border_box_size.height()),
237    );
238    let content_box_size = ResizeObserverSize::new(
239        cx,
240        window,
241        ResizeObserverSizeImpl::new(content_box_size.width(), content_box_size.height()),
242    );
243    let device_pixel_content_box = ResizeObserverSize::new(
244        cx,
245        window,
246        ResizeObserverSizeImpl::new(
247            device_pixel_content_box.width(),
248            device_pixel_content_box.height(),
249        ),
250    );
251
252    // Step 1. Let this be a new ResizeObserverEntry.
253    // Step 2. Set this.target slot to target.
254    ResizeObserverEntry::new(
255        cx,
256        window,
257        target,
258        &content_rect,
259        &[&*border_box_size],
260        &[&*content_box_size],
261        &[&*device_pixel_content_box],
262    )
263}
264
265impl ResizeObserverMethods<crate::DomTypeHolder> for ResizeObserver {
266    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-resizeobserver>
267    fn Constructor(
268        cx: &mut JSContext,
269        window: &Window,
270        proto: Option<HandleObject>,
271        callback: RootedCallback<ResizeObserverCallback>,
272    ) -> DomRoot<ResizeObserver> {
273        let rooted_observer = ResizeObserver::new(cx, window, proto, callback);
274        let document = window.Document();
275        document.add_resize_observer(&rooted_observer);
276        rooted_observer
277    }
278
279    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-observe>
280    fn Observe(&self, no_gc: &NoGC, target: &Element, options: &ResizeObserverOptions) {
281        // Step 1. If target is in [[observationTargets]] slot, call unobserve() with argument target.
282        let is_present = self
283            .observation_targets
284            .borrow()
285            .iter()
286            .any(|(_obs, other)| &**other == target);
287        if is_present {
288            self.Unobserve(no_gc, target);
289        }
290
291        // Step 2. Let observedBox be the value of the box dictionary member of options.
292        // Step 3. Let resizeObservation be new ResizeObservation(target, observedBox).
293        let resize_observation = ResizeObservation::new(options.box_);
294
295        // Step 4. Add the resizeObservation to the [[observationTargets]] slot.
296        self.observation_targets
297            .safe_borrow_mut(no_gc)
298            .push((resize_observation, Dom::from_ref(target)));
299        target
300            .owner_window()
301            .Document()
302            .add_rendering_update_reason(
303                RenderingUpdateReason::ResizeObserverStartedObservingTarget,
304            );
305    }
306
307    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-unobserve>
308    fn Unobserve(&self, no_gc: &NoGC, target: &Element) {
309        self.observation_targets
310            .safe_borrow_mut(no_gc)
311            .retain_mut(|(_obs, other)| !(&**other == target));
312    }
313
314    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-disconnect>
315    fn Disconnect(&self, no_gc: &NoGC) {
316        self.observation_targets.safe_borrow_mut(no_gc).clear();
317    }
318}
319
320/// State machine equivalent of active and skipped observations.
321#[derive(Copy, Clone, Default, MallocSizeOf, PartialEq)]
322enum ObservationState {
323    #[default]
324    Done,
325    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-activetargets-slot>
326    Active,
327    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-skippedtargets-slot>
328    Skipped,
329}
330
331/// <https://drafts.csswg.org/resize-observer/#resizeobservation>
332///
333/// Note: `target` is kept out of here, to avoid having to root the `ResizeObservation`.
334/// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-target>
335#[derive(JSTraceable, MallocSizeOf)]
336struct ResizeObservation {
337    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-observedbox>
338    observed_box: ResizeObserverBoxOptions,
339    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-lastreportedsizes>
340    last_reported_sizes: DomRefCell<Vec<ResizeObserverSizeImpl>>,
341    /// State machine mimicking the "active" and "skipped" targets slots of the observer.
342    #[no_trace]
343    state: Cell<ObservationState>,
344}
345
346impl ResizeObservation {
347    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-resizeobservation>
348    pub(crate) fn new(observed_box: ResizeObserverBoxOptions) -> ResizeObservation {
349        ResizeObservation {
350            observed_box,
351            last_reported_sizes: DomRefCell::new(vec![]),
352            state: Default::default(),
353        }
354    }
355
356    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-isactive>
357    fn is_active(&self, target: &Element) -> bool {
358        let Some(last_reported_size) = self.last_reported_sizes.borrow().first().copied() else {
359            return true;
360        };
361        let box_size = calculate_box_size(target, &self.observed_box);
362        box_size.width() != last_reported_size.inline_size() ||
363            box_size.height() != last_reported_size.block_size()
364    }
365}
366
367/// <https://drafts.csswg.org/resize-observer/#calculate-depth-for-node>
368fn calculate_depth_for_node(no_gc: &NoGC, target: &Element) -> ResizeObservationDepth {
369    let node = target.upcast::<Node>();
370    let depth = node
371        .inclusive_ancestors_in_flat_tree_unrooted(no_gc)
372        .count();
373    ResizeObservationDepth(depth)
374}
375
376/// <https://drafts.csswg.org/resize-observer/#calculate-box-size>
377///
378/// The dimensions of the returned `Rect` depend on the type of box being observed.
379/// For `ResizeObserverBoxOptions::Content_box` and `ResizeObserverBoxOptions::Border_box`,
380/// the values will be in `px`. For `ResizeObserverBoxOptions::Device_pixel_content_box` they
381/// will be in integral device pixels.
382fn calculate_box_size(
383    target: &Element,
384    observed_box: &ResizeObserverBoxOptions,
385) -> Rect<f64, CSSPixel> {
386    match observed_box {
387        ResizeObserverBoxOptions::Content_box => {
388            // Note: only taking first fragment,
389            // but the spec will expand to cover all fragments.
390            let content_box = target
391                .owner_window()
392                .box_area_query(target.upcast(), BoxAreaType::Content, true)
393                .unwrap_or_else(Rect::zero);
394
395            Rect::new(
396                content_box.origin.map(|coordinate| coordinate.to_f64_px()),
397                Size2D::new(
398                    content_box.size.width.to_f64_px(),
399                    content_box.size.height.to_f64_px(),
400                ),
401            )
402        },
403        ResizeObserverBoxOptions::Border_box => {
404            // Note: only taking first fragment,
405            // but the spec will expand to cover all fragments.
406            let border_box = target
407                .owner_window()
408                .box_area_query(target.upcast(), BoxAreaType::Border, true)
409                .unwrap_or_else(Rect::zero);
410
411            Rect::new(
412                border_box.origin.map(|coordinate| coordinate.to_f64_px()),
413                Size2D::new(
414                    border_box.size.width.to_f64_px(),
415                    border_box.size.height.to_f64_px(),
416                ),
417            )
418        },
419        ResizeObserverBoxOptions::Device_pixel_content_box => {
420            let device_pixel_ratio = target.owner_window().device_pixel_ratio().get() as f64;
421            let content_box = target
422                .owner_window()
423                .box_area_query(target.upcast(), BoxAreaType::Content, true)
424                .unwrap_or_else(Rect::zero);
425
426            let to_device_px = |length: Au| (length.to_f64_px() * device_pixel_ratio).round();
427            Rect::new(
428                content_box.origin.map(to_device_px),
429                Size2D::new(
430                    to_device_px(content_box.size.width),
431                    to_device_px(content_box.size.height),
432                ),
433            )
434        },
435    }
436}
437
438impl script_bindings::callback::OwnerWindow<crate::DomTypeHolder> for ResizeObserver {}