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::default::Rect;
10use js::rust::HandleObject;
11
12use crate::dom::bindings::callback::ExceptionHandling;
13use crate::dom::bindings::cell::DomRefCell;
14use crate::dom::bindings::codegen::Bindings::ResizeObserverBinding::{
15    ResizeObserverBoxOptions, ResizeObserverCallback, ResizeObserverMethods, ResizeObserverOptions,
16};
17use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
18use crate::dom::bindings::inheritance::Castable;
19use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
20use crate::dom::bindings::root::{Dom, DomRoot};
21use crate::dom::domrectreadonly::DOMRectReadOnly;
22use crate::dom::element::Element;
23use crate::dom::node::{Node, NodeTraits};
24use crate::dom::resizeobserverentry::ResizeObserverEntry;
25use crate::dom::resizeobserversize::{ResizeObserverSize, ResizeObserverSizeImpl};
26use crate::dom::window::Window;
27use crate::script_runtime::CanGc;
28
29/// <https://drafts.csswg.org/resize-observer/#calculate-depth-for-node>
30#[derive(Debug, Default, PartialEq, PartialOrd)]
31pub(crate) struct ResizeObservationDepth(usize);
32
33impl ResizeObservationDepth {
34    pub(crate) fn max() -> ResizeObservationDepth {
35        ResizeObservationDepth(usize::MAX)
36    }
37}
38
39/// <https://drafts.csswg.org/resize-observer/#resize-observer-slots>
40/// See `ObservationState` for active and skipped observation targets.
41#[dom_struct]
42pub(crate) struct ResizeObserver {
43    reflector_: Reflector,
44
45    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-callback-slot>
46    #[ignore_malloc_size_of = "Rc are hard"]
47    callback: Rc<ResizeObserverCallback>,
48
49    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-observationtargets-slot>
50    ///
51    /// This list simultaneously also represents the
52    /// [`[[activeTargets]]`](https://drafts.csswg.org/resize-observer/#dom-resizeobserver-activetargets-slot)
53    /// and [`[[skippedTargets]]`](https://drafts.csswg.org/resize-observer/#dom-resizeobserver-skippedtargets-slot)
54    /// internal slots.
55    observation_targets: DomRefCell<Vec<(ResizeObservation, Dom<Element>)>>,
56}
57
58impl ResizeObserver {
59    pub(crate) fn new_inherited(callback: Rc<ResizeObserverCallback>) -> ResizeObserver {
60        ResizeObserver {
61            reflector_: Reflector::new(),
62            callback,
63            observation_targets: Default::default(),
64        }
65    }
66
67    fn new(
68        window: &Window,
69        proto: Option<HandleObject>,
70        callback: Rc<ResizeObserverCallback>,
71        can_gc: CanGc,
72    ) -> DomRoot<ResizeObserver> {
73        let observer = Box::new(ResizeObserver::new_inherited(callback));
74        reflect_dom_object_with_proto(observer, window, proto, can_gc)
75    }
76
77    /// Step 2 of <https://drafts.csswg.org/resize-observer/#gather-active-observations-h>
78    ///
79    /// <https://drafts.csswg.org/resize-observer/#has-active-resize-observations>
80    pub(crate) fn gather_active_resize_observations_at_depth(
81        &self,
82        depth: &ResizeObservationDepth,
83        has_active: &mut bool,
84    ) {
85        // Step 2.1 Clear observer’s [[activeTargets]], and [[skippedTargets]].
86        // NOTE: This happens as part of Step 2.2
87
88        // Step 2.2 For each observation in observer.[[observationTargets]] run this step:
89        for (observation, target) in self.observation_targets.borrow_mut().iter_mut() {
90            observation.state = Default::default();
91
92            // Step 2.2.1 If observation.isActive() is true
93            if let Some(size) = observation.is_active(target) {
94                // Step 2.2.1.1 Let targetDepth be result of calculate depth for node for observation.target.
95                let target_depth = calculate_depth_for_node(target);
96
97                // Step 2.2.1.2 If targetDepth is greater than depth then add observation to [[activeTargets]].
98                if target_depth > *depth {
99                    observation.state = ObservationState::Active(size);
100                    *has_active = true;
101                }
102                // Step 2.2.1.3 Else add observation to [[skippedTargets]].
103                else {
104                    observation.state = ObservationState::Skipped;
105                }
106            }
107        }
108    }
109
110    /// Step 2 of <https://drafts.csswg.org/resize-observer/#broadcast-active-resize-observations>
111    pub(crate) fn broadcast_active_resize_observations(
112        &self,
113        shallowest_target_depth: &mut ResizeObservationDepth,
114        can_gc: CanGc,
115    ) {
116        // Step 2.1 If observer.[[activeTargets]] slot is empty, continue.
117        // NOTE: Due to the way we implement the activeTarges internal slot we can't easily
118        // know if it's empty. Instead we remember whether there were any active observation
119        // targets during the following traversal and return if there were none.
120        let mut has_active_observation_targets = false;
121
122        // Step 2.2 Let entries be an empty list of ResizeObserverEntryies.
123        let mut entries: Vec<DomRoot<ResizeObserverEntry>> = Default::default();
124
125        // Step 2.3 For each observation in [[activeTargets]] perform these steps:
126        for (observation, target) in self.observation_targets.borrow_mut().iter_mut() {
127            let box_size = {
128                let ObservationState::Active(box_size) = observation.state else {
129                    continue;
130                };
131                box_size
132            };
133            has_active_observation_targets = true;
134
135            // #create-and-populate-a-resizeobserverentry
136
137            // Note: only calculating content box size.
138            let width = box_size.width().to_f64_px();
139            let height = box_size.height().to_f64_px();
140            let size_impl = ResizeObserverSizeImpl::new(width, height);
141            let window = target.owner_window();
142            let observer_size = ResizeObserverSize::new(&window, size_impl, can_gc);
143
144            // Note: content rect is built from content box size.
145            let content_rect = DOMRectReadOnly::new(
146                window.upcast(),
147                None,
148                box_size.origin.x.to_f64_px(),
149                box_size.origin.y.to_f64_px(),
150                width,
151                height,
152                can_gc,
153            );
154            let entry = ResizeObserverEntry::new(
155                &window,
156                target,
157                &content_rect,
158                &[],
159                &[&*observer_size],
160                &[],
161                can_gc,
162            );
163            entries.push(entry);
164
165            // Note: this is safe because an observation is
166            // initialized with one reported size (zero).
167            // The spec plans to store multiple reported sizes,
168            // but for now there can be only one.
169            observation.last_reported_sizes[0] = size_impl;
170            observation.state = ObservationState::Done;
171            let target_depth = calculate_depth_for_node(target);
172            if target_depth < *shallowest_target_depth {
173                *shallowest_target_depth = target_depth;
174            }
175        }
176
177        if !has_active_observation_targets {
178            return;
179        }
180
181        // Step 2.4 Invoke observer.[[callback]] with entries.
182        let _ = self
183            .callback
184            .Call_(self, entries, self, ExceptionHandling::Report, can_gc);
185
186        // Step 2.5 Clear observer.[[activeTargets]].
187        // NOTE: The observation state was modified in Step 2.2
188    }
189
190    /// <https://drafts.csswg.org/resize-observer/#has-skipped-observations-h>
191    pub(crate) fn has_skipped_resize_observations(&self) -> bool {
192        self.observation_targets
193            .borrow()
194            .iter()
195            .any(|(observation, _)| observation.state == ObservationState::Skipped)
196    }
197}
198
199impl ResizeObserverMethods<crate::DomTypeHolder> for ResizeObserver {
200    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-resizeobserver>
201    fn Constructor(
202        window: &Window,
203        proto: Option<HandleObject>,
204        can_gc: CanGc,
205        callback: Rc<ResizeObserverCallback>,
206    ) -> DomRoot<ResizeObserver> {
207        let rooted_observer = ResizeObserver::new(window, proto, callback, can_gc);
208        let document = window.Document();
209        document.add_resize_observer(&rooted_observer);
210        rooted_observer
211    }
212
213    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-observe>
214    fn Observe(&self, target: &Element, options: &ResizeObserverOptions) {
215        let is_present = self
216            .observation_targets
217            .borrow()
218            .iter()
219            .any(|(_obs, other)| &**other == target);
220        if is_present {
221            self.Unobserve(target);
222        }
223
224        let resize_observation = ResizeObservation::new(options.box_);
225
226        self.observation_targets
227            .borrow_mut()
228            .push((resize_observation, Dom::from_ref(target)));
229        target
230            .owner_window()
231            .Document()
232            .set_resize_observer_started_observing_target(true);
233    }
234
235    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-unobserve>
236    fn Unobserve(&self, target: &Element) {
237        self.observation_targets
238            .borrow_mut()
239            .retain_mut(|(_obs, other)| !(&**other == target));
240    }
241
242    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-disconnect>
243    fn Disconnect(&self) {
244        self.observation_targets.borrow_mut().clear();
245    }
246}
247
248/// State machine equivalent of active and skipped observations.
249#[derive(Default, MallocSizeOf, PartialEq)]
250enum ObservationState {
251    #[default]
252    Done,
253    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-activetargets-slot>
254    /// With the result of the box size calculated when setting the state to active,
255    /// in order to avoid recalculating it in the subsequent broadcast.
256    Active(Rect<Au>),
257    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobserver-skippedtargets-slot>
258    Skipped,
259}
260
261/// <https://drafts.csswg.org/resize-observer/#resizeobservation>
262///
263/// Note: `target` is kept out of here, to avoid having to root the `ResizeObservation`.
264/// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-target>
265#[derive(JSTraceable, MallocSizeOf)]
266struct ResizeObservation {
267    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-observedbox>
268    observed_box: ResizeObserverBoxOptions,
269    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-lastreportedsizes>
270    last_reported_sizes: Vec<ResizeObserverSizeImpl>,
271    /// State machine mimicking the "active" and "skipped" targets slots of the observer.
272    #[no_trace]
273    state: ObservationState,
274}
275
276impl ResizeObservation {
277    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-resizeobservation>
278    pub(crate) fn new(observed_box: ResizeObserverBoxOptions) -> ResizeObservation {
279        let size_impl = ResizeObserverSizeImpl::new(0.0, 0.0);
280        ResizeObservation {
281            observed_box,
282            last_reported_sizes: vec![size_impl],
283            state: Default::default(),
284        }
285    }
286
287    /// <https://drafts.csswg.org/resize-observer/#dom-resizeobservation-isactive>
288    /// Returning an optional calculated size, instead of a boolean,
289    /// to avoid recalculating the size in the subsequent broadcast.
290    fn is_active(&self, target: &Element) -> Option<Rect<Au>> {
291        let last_reported_size = self.last_reported_sizes[0];
292        let box_size = calculate_box_size(target, &self.observed_box);
293        let is_active = box_size.width().to_f64_px() != last_reported_size.inline_size() ||
294            box_size.height().to_f64_px() != last_reported_size.block_size();
295        if is_active { Some(box_size) } else { None }
296    }
297}
298
299/// <https://drafts.csswg.org/resize-observer/#calculate-depth-for-node>
300fn calculate_depth_for_node(target: &Element) -> ResizeObservationDepth {
301    let node = target.upcast::<Node>();
302    let depth = node.inclusive_ancestors_in_flat_tree().count() - 1;
303    ResizeObservationDepth(depth)
304}
305
306/// <https://drafts.csswg.org/resize-observer/#calculate-box-size>
307fn calculate_box_size(target: &Element, observed_box: &ResizeObserverBoxOptions) -> Rect<Au> {
308    match observed_box {
309        ResizeObserverBoxOptions::Content_box => {
310            // Note: only taking first fragment,
311            // but the spec will expand to cover all fragments.
312            target
313                .upcast::<Node>()
314                .border_boxes()
315                .pop()
316                .unwrap_or_else(Rect::zero)
317        },
318        // TODO(#31182): add support for border box, and device pixel size, calculations.
319        _ => Rect::zero(),
320    }
321}