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