script/dom/resizeobserver/
resizeobserver.rs1use std::cell::Cell;
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, NoGC};
13use js::rust::HandleObject;
14use layout_api::BoxAreaType;
15use script_bindings::callback::{RootedCallback, TracedCallback};
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#[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#[dom_struct]
48pub(crate) struct ResizeObserver {
49 reflector_: Reflector,
50
51 callback: TracedCallback<ResizeObserverCallback>,
53
54 observation_targets: DomRefCell<Vec<(ResizeObservation, Dom<Element>)>>,
61}
62
63impl ResizeObserver {
64 pub(crate) fn new_inherited(
65 callback: RootedCallback<ResizeObserverCallback>,
66 ) -> ResizeObserver {
67 ResizeObserver {
68 reflector_: Reflector::new(),
69 callback: callback.to_traced(),
70 observation_targets: Default::default(),
71 }
72 }
73
74 fn new(
75 cx: &mut JSContext,
76 window: &Window,
77 proto: Option<HandleObject>,
78 callback: RootedCallback<ResizeObserverCallback>,
79 ) -> DomRoot<ResizeObserver> {
80 let observer = Box::new(ResizeObserver::new_inherited(callback));
81 reflect_dom_object_with_proto(cx, observer, window, proto)
82 }
83
84 pub(crate) fn gather_active_resize_observations_at_depth(
88 &self,
89 no_gc: &NoGC,
90 depth: &ResizeObservationDepth,
91 has_active: &mut bool,
92 ) {
93 for (observation, target) in self.observation_targets.borrow().iter() {
98 observation.state.set(Default::default());
99
100 if observation.is_active(target) {
102 let target_depth = calculate_depth_for_node(no_gc, target);
104
105 if target_depth > *depth {
107 observation.state.set(ObservationState::Active);
108 *has_active = true;
109 }
110 else {
112 observation.state.set(ObservationState::Skipped);
113 }
114 }
115 }
116 }
117
118 pub(crate) fn broadcast_active_resize_observations(
120 &self,
121 cx: &mut JSContext,
122 shallowest_target_depth: &mut ResizeObservationDepth,
123 ) {
124 let mut has_active_observation_targets = false;
129
130 let mut entries: Vec<DomRoot<ResizeObserverEntry>> = Default::default();
132
133 for (observation, target) in self.observation_targets.borrow().iter() {
135 let ObservationState::Active = observation.state.get() else {
136 continue;
137 };
138 has_active_observation_targets = true;
139
140 let window = target.owner_window();
141 let entry = create_and_populate_a_resizeobserverentry(cx, &window, target, observation);
142 entries.push(entry);
143 observation.state.set(ObservationState::Done);
144
145 let target_depth = calculate_depth_for_node(cx.no_gc(), target);
146 if target_depth < *shallowest_target_depth {
147 *shallowest_target_depth = target_depth;
148 }
149 }
150
151 if !has_active_observation_targets {
152 return;
153 }
154
155 let _ = self
157 .callback
158 .Call_(cx, self, entries, self, ExceptionHandling::Report);
159
160 }
163
164 pub(crate) fn has_skipped_resize_observations(&self) -> bool {
166 self.observation_targets
167 .borrow()
168 .iter()
169 .any(|(observation, _)| observation.state.get() == ObservationState::Skipped)
170 }
171}
172
173fn create_and_populate_a_resizeobserverentry(
175 cx: &mut JSContext,
176 window: &Window,
177 target: &Element,
178 observation: &ResizeObservation,
179) -> DomRoot<ResizeObserverEntry> {
180 let border_box_size = calculate_box_size(target, &ResizeObserverBoxOptions::Border_box);
182 let content_box_size = calculate_box_size(target, &ResizeObserverBoxOptions::Content_box);
184
185 let device_pixel_content_box =
187 calculate_box_size(target, &ResizeObserverBoxOptions::Device_pixel_content_box);
188
189 let last_size = match observation.observed_box {
194 ResizeObserverBoxOptions::Content_box => content_box_size,
195 ResizeObserverBoxOptions::Border_box => border_box_size,
196 ResizeObserverBoxOptions::Device_pixel_content_box => device_pixel_content_box,
197 };
198 let last_reported_size = ResizeObserverSizeImpl::new(last_size.width(), last_size.height());
199
200 {
201 let mut sizes = observation.last_reported_sizes.safe_borrow_mut(cx.no_gc());
202 if sizes.is_empty() {
203 sizes.push(last_reported_size);
204 } else {
205 sizes[0] = last_reported_size;
206 }
207 }
208
209 let use_padding = *target.namespace() != ns!(svg) || target.has_css_layout_box();
211 let (padding_top, padding_left) = if use_padding {
212 let padding = target.upcast::<Node>().padding().unwrap_or_default();
215 (padding.top, padding.left)
216 } else {
217 (Au::zero(), Au::zero())
220 };
221
222 let content_rect = DOMRectReadOnly::new(
224 cx,
225 window.upcast(),
226 None,
227 padding_left.to_f64_px(),
228 padding_top.to_f64_px(),
229 content_box_size.width(),
230 content_box_size.height(),
231 );
232
233 let border_box_size = ResizeObserverSize::new(
234 cx,
235 window,
236 ResizeObserverSizeImpl::new(border_box_size.width(), border_box_size.height()),
237 );
238 let content_box_size = ResizeObserverSize::new(
239 cx,
240 window,
241 ResizeObserverSizeImpl::new(content_box_size.width(), content_box_size.height()),
242 );
243 let device_pixel_content_box = ResizeObserverSize::new(
244 cx,
245 window,
246 ResizeObserverSizeImpl::new(
247 device_pixel_content_box.width(),
248 device_pixel_content_box.height(),
249 ),
250 );
251
252 ResizeObserverEntry::new(
255 cx,
256 window,
257 target,
258 &content_rect,
259 &[&*border_box_size],
260 &[&*content_box_size],
261 &[&*device_pixel_content_box],
262 )
263}
264
265impl ResizeObserverMethods<crate::DomTypeHolder> for ResizeObserver {
266 fn Constructor(
268 cx: &mut JSContext,
269 window: &Window,
270 proto: Option<HandleObject>,
271 callback: RootedCallback<ResizeObserverCallback>,
272 ) -> DomRoot<ResizeObserver> {
273 let rooted_observer = ResizeObserver::new(cx, window, proto, callback);
274 let document = window.Document();
275 document.add_resize_observer(&rooted_observer);
276 rooted_observer
277 }
278
279 fn Observe(&self, no_gc: &NoGC, target: &Element, options: &ResizeObserverOptions) {
281 let is_present = self
283 .observation_targets
284 .borrow()
285 .iter()
286 .any(|(_obs, other)| &**other == target);
287 if is_present {
288 self.Unobserve(no_gc, target);
289 }
290
291 let resize_observation = ResizeObservation::new(options.box_);
294
295 self.observation_targets
297 .safe_borrow_mut(no_gc)
298 .push((resize_observation, Dom::from_ref(target)));
299 target
300 .owner_window()
301 .Document()
302 .add_rendering_update_reason(
303 RenderingUpdateReason::ResizeObserverStartedObservingTarget,
304 );
305 }
306
307 fn Unobserve(&self, no_gc: &NoGC, target: &Element) {
309 self.observation_targets
310 .safe_borrow_mut(no_gc)
311 .retain_mut(|(_obs, other)| !(&**other == target));
312 }
313
314 fn Disconnect(&self, no_gc: &NoGC) {
316 self.observation_targets.safe_borrow_mut(no_gc).clear();
317 }
318}
319
320#[derive(Copy, Clone, Default, MallocSizeOf, PartialEq)]
322enum ObservationState {
323 #[default]
324 Done,
325 Active,
327 Skipped,
329}
330
331#[derive(JSTraceable, MallocSizeOf)]
336struct ResizeObservation {
337 observed_box: ResizeObserverBoxOptions,
339 last_reported_sizes: DomRefCell<Vec<ResizeObserverSizeImpl>>,
341 #[no_trace]
343 state: Cell<ObservationState>,
344}
345
346impl ResizeObservation {
347 pub(crate) fn new(observed_box: ResizeObserverBoxOptions) -> ResizeObservation {
349 ResizeObservation {
350 observed_box,
351 last_reported_sizes: DomRefCell::new(vec![]),
352 state: Default::default(),
353 }
354 }
355
356 fn is_active(&self, target: &Element) -> bool {
358 let Some(last_reported_size) = self.last_reported_sizes.borrow().first().copied() else {
359 return true;
360 };
361 let box_size = calculate_box_size(target, &self.observed_box);
362 box_size.width() != last_reported_size.inline_size() ||
363 box_size.height() != last_reported_size.block_size()
364 }
365}
366
367fn calculate_depth_for_node(no_gc: &NoGC, target: &Element) -> ResizeObservationDepth {
369 let node = target.upcast::<Node>();
370 let depth = node
371 .inclusive_ancestors_in_flat_tree_unrooted(no_gc)
372 .count();
373 ResizeObservationDepth(depth)
374}
375
376fn calculate_box_size(
383 target: &Element,
384 observed_box: &ResizeObserverBoxOptions,
385) -> Rect<f64, CSSPixel> {
386 match observed_box {
387 ResizeObserverBoxOptions::Content_box => {
388 let content_box = target
391 .owner_window()
392 .box_area_query(target.upcast(), BoxAreaType::Content, true)
393 .unwrap_or_else(Rect::zero);
394
395 Rect::new(
396 content_box.origin.map(|coordinate| coordinate.to_f64_px()),
397 Size2D::new(
398 content_box.size.width.to_f64_px(),
399 content_box.size.height.to_f64_px(),
400 ),
401 )
402 },
403 ResizeObserverBoxOptions::Border_box => {
404 let border_box = target
407 .owner_window()
408 .box_area_query(target.upcast(), BoxAreaType::Border, true)
409 .unwrap_or_else(Rect::zero);
410
411 Rect::new(
412 border_box.origin.map(|coordinate| coordinate.to_f64_px()),
413 Size2D::new(
414 border_box.size.width.to_f64_px(),
415 border_box.size.height.to_f64_px(),
416 ),
417 )
418 },
419 ResizeObserverBoxOptions::Device_pixel_content_box => {
420 let device_pixel_ratio = target.owner_window().device_pixel_ratio().get() as f64;
421 let content_box = target
422 .owner_window()
423 .box_area_query(target.upcast(), BoxAreaType::Content, true)
424 .unwrap_or_else(Rect::zero);
425
426 let to_device_px = |length: Au| (length.to_f64_px() * device_pixel_ratio).round();
427 Rect::new(
428 content_box.origin.map(to_device_px),
429 Size2D::new(
430 to_device_px(content_box.size.width),
431 to_device_px(content_box.size.height),
432 ),
433 )
434 },
435 }
436}
437
438impl script_bindings::callback::OwnerWindow<crate::DomTypeHolder> for ResizeObserver {}