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