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::rc::Rc;
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;
13use js::rust::HandleObject;
14use layout_api::BoxAreaType;
15use script_bindings::cell::DomRefCell;
16use script_bindings::reflector::{Reflector, reflect_dom_object_with_proto_and_cx};
17use style_traits::CSSPixel;
18
19use crate::dom::bindings::callback::ExceptionHandling;
20use crate::dom::bindings::codegen::Bindings::ResizeObserverBinding::{
21    ResizeObserverBoxOptions, ResizeObserverCallback, ResizeObserverMethods, ResizeObserverOptions,
22};
23use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
24use crate::dom::bindings::inheritance::Castable;
25use crate::dom::bindings::root::{Dom, DomRoot};
26use crate::dom::document::RenderingUpdateReason;
27use crate::dom::domrectreadonly::DOMRectReadOnly;
28use crate::dom::element::Element;
29use crate::dom::node::{Node, NodeTraits};
30use crate::dom::resizeobserverentry::ResizeObserverEntry;
31use crate::dom::resizeobserversize::{ResizeObserverSize, ResizeObserverSizeImpl};
32use crate::dom::window::Window;
33use crate::script_runtime::CanGc;
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_and_cx(observer, window, proto, cx)
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        depth: &ResizeObservationDepth,
89        has_active: &mut bool,
90    ) {
91        // Step 2.1 Clear observer’s [[activeTargets]], and [[skippedTargets]].
92        // NOTE: This happens as part of Step 2.2
93
94        // Step 2.2 For each observation in observer.[[observationTargets]] run this step:
95        for (observation, target) in self.observation_targets.borrow_mut().iter_mut() {
96            observation.state = Default::default();
97
98            // Step 2.2.1 If observation.isActive() is true
99            if observation.is_active(target) {
100                // Step 2.2.1.1 Let targetDepth be result of calculate depth for node for observation.target.
101                let target_depth = calculate_depth_for_node(target);
102
103                // Step 2.2.1.2 If targetDepth is greater than depth then add observation to [[activeTargets]].
104                if target_depth > *depth {
105                    observation.state = ObservationState::Active;
106                    *has_active = true;
107                }
108                // Step 2.2.1.3 Else add observation to [[skippedTargets]].
109                else {
110                    observation.state = ObservationState::Skipped;
111                }
112            }
113        }
114    }
115
116    /// Step 2 of <https://drafts.csswg.org/resize-observer/#broadcast-active-resize-observations>
117    pub(crate) fn broadcast_active_resize_observations(
118        &self,
119        cx: &mut JSContext,
120        shallowest_target_depth: &mut ResizeObservationDepth,
121    ) {
122        // Step 2.1 If observer.[[activeTargets]] slot is empty, continue.
123        // NOTE: Due to the way we implement the activeTarges internal slot we can't easily
124        // know if it's empty. Instead we remember whether there were any active observation
125        // targets during the following traversal and return if there were none.
126        let mut has_active_observation_targets = false;
127
128        // Step 2.2 Let entries be an empty list of ResizeObserverEntryies.
129        let mut entries: Vec<DomRoot<ResizeObserverEntry>> = Default::default();
130
131        // Step 2.3 For each observation in [[activeTargets]] perform these steps:
132        for (observation, target) in self.observation_targets.borrow_mut().iter_mut() {
133            let ObservationState::Active = observation.state else {
134                continue;
135            };
136            has_active_observation_targets = true;
137
138            let window = target.owner_window();
139            let entry = create_and_populate_a_resizeobserverentry(cx, &window, target, observation);
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_(cx, self, entries, self, ExceptionHandling::Report);
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    cx: &mut JSContext,
174    window: &Window,
175    target: &Element,
176    observation: &mut ResizeObservation,
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        cx,
219        window.upcast(),
220        None,
221        padding_left.to_f64_px(),
222        padding_top.to_f64_px(),
223        content_box_size.width(),
224        content_box_size.height(),
225    );
226
227    let border_box_size = ResizeObserverSize::new(
228        window,
229        ResizeObserverSizeImpl::new(border_box_size.width(), border_box_size.height()),
230        CanGc::from_cx(cx),
231    );
232    let content_box_size = ResizeObserverSize::new(
233        window,
234        ResizeObserverSizeImpl::new(content_box_size.width(), content_box_size.height()),
235        CanGc::from_cx(cx),
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        CanGc::from_cx(cx),
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        CanGc::from_cx(cx),
256    )
257}
258
259impl ResizeObserverMethods<crate::DomTypeHolder> for ResizeObserver {
260    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-resizeobserver>
261    fn Constructor(
262        cx: &mut JSContext,
263        window: &Window,
264        proto: Option<HandleObject>,
265        callback: Rc<ResizeObserverCallback>,
266    ) -> DomRoot<ResizeObserver> {
267        let rooted_observer = ResizeObserver::new(cx, window, proto, callback);
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(
375    target: &Element,
376    observed_box: &ResizeObserverBoxOptions,
377) -> Rect<f64, CSSPixel> {
378    match observed_box {
379        ResizeObserverBoxOptions::Content_box => {
380            // Note: only taking first fragment,
381            // but the spec will expand to cover all fragments.
382            let content_box = target
383                .owner_window()
384                .box_area_query(target.upcast(), BoxAreaType::Content, true)
385                .unwrap_or_else(Rect::zero);
386
387            Rect::new(
388                content_box.origin.map(|coordinate| coordinate.to_f64_px()),
389                Size2D::new(
390                    content_box.size.width.to_f64_px(),
391                    content_box.size.height.to_f64_px(),
392                ),
393            )
394        },
395        ResizeObserverBoxOptions::Border_box => {
396            // Note: only taking first fragment,
397            // but the spec will expand to cover all fragments.
398            let border_box = target
399                .owner_window()
400                .box_area_query(target.upcast(), BoxAreaType::Border, true)
401                .unwrap_or_else(Rect::zero);
402
403            Rect::new(
404                border_box.origin.map(|coordinate| coordinate.to_f64_px()),
405                Size2D::new(
406                    border_box.size.width.to_f64_px(),
407                    border_box.size.height.to_f64_px(),
408                ),
409            )
410        },
411        ResizeObserverBoxOptions::Device_pixel_content_box => {
412            let device_pixel_ratio = target.owner_window().device_pixel_ratio();
413            let content_box = target
414                .owner_window()
415                .box_area_query(target.upcast(), BoxAreaType::Content, true)
416                .unwrap_or_else(Rect::zero);
417
418            Rect::new(
419                content_box
420                    .origin
421                    .map(|coordinate| coordinate.to_nearest_pixel(device_pixel_ratio.get()) as f64),
422                Size2D::new(
423                    content_box
424                        .size
425                        .width
426                        .to_nearest_pixel(device_pixel_ratio.get()) as f64,
427                    content_box
428                        .size
429                        .height
430                        .to_nearest_pixel(device_pixel_ratio.get()) as f64,
431                ),
432            )
433        },
434    }
435}