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