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 cx: &mut JSContext,
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(cx, target) {
102 let target_depth = calculate_depth_for_node(cx, 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(cx, target, &ResizeObserverBoxOptions::Border_box);
182 let content_box_size = calculate_box_size(cx, target, &ResizeObserverBoxOptions::Content_box);
184
185 let device_pixel_content_box = calculate_box_size(
187 cx,
188 target,
189 &ResizeObserverBoxOptions::Device_pixel_content_box,
190 );
191
192 let last_size = match observation.observed_box {
197 ResizeObserverBoxOptions::Content_box => content_box_size,
198 ResizeObserverBoxOptions::Border_box => border_box_size,
199 ResizeObserverBoxOptions::Device_pixel_content_box => device_pixel_content_box,
200 };
201 let last_reported_size = ResizeObserverSizeImpl::new(last_size.width(), last_size.height());
202
203 {
204 let mut sizes = observation.last_reported_sizes.safe_borrow_mut(cx.no_gc());
205 if sizes.is_empty() {
206 sizes.push(last_reported_size);
207 } else {
208 sizes[0] = last_reported_size;
209 }
210 }
211
212 let use_padding = *target.namespace() != ns!(svg) || target.has_css_layout_box();
214 let (padding_top, padding_left) = if use_padding {
215 let padding = target.upcast::<Node>().padding().unwrap_or_default();
218 (padding.top, padding.left)
219 } else {
220 (Au::zero(), Au::zero())
223 };
224
225 let content_rect = DOMRectReadOnly::new(
227 cx,
228 window.upcast(),
229 None,
230 padding_left.to_f64_px(),
231 padding_top.to_f64_px(),
232 content_box_size.width(),
233 content_box_size.height(),
234 );
235
236 let border_box_size = ResizeObserverSize::new(
237 cx,
238 window,
239 ResizeObserverSizeImpl::new(border_box_size.width(), border_box_size.height()),
240 );
241 let content_box_size = ResizeObserverSize::new(
242 cx,
243 window,
244 ResizeObserverSizeImpl::new(content_box_size.width(), content_box_size.height()),
245 );
246 let device_pixel_content_box = ResizeObserverSize::new(
247 cx,
248 window,
249 ResizeObserverSizeImpl::new(
250 device_pixel_content_box.width(),
251 device_pixel_content_box.height(),
252 ),
253 );
254
255 ResizeObserverEntry::new(
258 cx,
259 window,
260 target,
261 &content_rect,
262 &[&*border_box_size],
263 &[&*content_box_size],
264 &[&*device_pixel_content_box],
265 )
266}
267
268impl ResizeObserverMethods<crate::DomTypeHolder> for ResizeObserver {
269 fn Constructor(
271 cx: &mut JSContext,
272 window: &Window,
273 proto: Option<HandleObject>,
274 callback: RootedCallback<ResizeObserverCallback>,
275 ) -> DomRoot<ResizeObserver> {
276 let rooted_observer = ResizeObserver::new(cx, window, proto, callback);
277 let document = window.Document();
278 document.add_resize_observer(cx, &rooted_observer);
279 rooted_observer
280 }
281
282 fn Observe(&self, no_gc: &NoGC, target: &Element, options: &ResizeObserverOptions) {
284 let is_present = self
286 .observation_targets
287 .borrow()
288 .iter()
289 .any(|(_obs, other)| &**other == target);
290 if is_present {
291 self.Unobserve(no_gc, target);
292 }
293
294 let resize_observation = ResizeObservation::new(options.box_);
297
298 self.observation_targets
300 .safe_borrow_mut(no_gc)
301 .push((resize_observation, Dom::from_ref(target)));
302 target
303 .owner_window()
304 .Document()
305 .add_rendering_update_reason(
306 RenderingUpdateReason::ResizeObserverStartedObservingTarget,
307 );
308 }
309
310 fn Unobserve(&self, no_gc: &NoGC, target: &Element) {
312 self.observation_targets
313 .safe_borrow_mut(no_gc)
314 .retain_mut(|(_obs, other)| !(&**other == target));
315 }
316
317 fn Disconnect(&self, no_gc: &NoGC) {
319 self.observation_targets.safe_borrow_mut(no_gc).clear();
320 }
321}
322
323#[derive(Copy, Clone, Default, MallocSizeOf, PartialEq)]
325enum ObservationState {
326 #[default]
327 Done,
328 Active,
330 Skipped,
332}
333
334#[derive(JSTraceable, MallocSizeOf)]
339struct ResizeObservation {
340 observed_box: ResizeObserverBoxOptions,
342 last_reported_sizes: DomRefCell<Vec<ResizeObserverSizeImpl>>,
344 #[no_trace]
346 state: Cell<ObservationState>,
347}
348
349impl ResizeObservation {
350 pub(crate) fn new(observed_box: ResizeObserverBoxOptions) -> ResizeObservation {
352 ResizeObservation {
353 observed_box,
354 last_reported_sizes: DomRefCell::new(vec![]),
355 state: Default::default(),
356 }
357 }
358
359 fn is_active(&self, cx: &mut JSContext, target: &Element) -> bool {
361 let Some(last_reported_size) = self.last_reported_sizes.borrow().first().copied() else {
362 return true;
363 };
364 let box_size = calculate_box_size(cx, target, &self.observed_box);
365 box_size.width() != last_reported_size.inline_size() ||
366 box_size.height() != last_reported_size.block_size()
367 }
368}
369
370fn calculate_depth_for_node(no_gc: &NoGC, target: &Element) -> ResizeObservationDepth {
372 let node = target.upcast::<Node>();
373 let depth = node
374 .inclusive_ancestors_in_flat_tree_unrooted(no_gc)
375 .count();
376 ResizeObservationDepth(depth)
377}
378
379fn calculate_box_size(
386 cx: &mut JSContext,
387 target: &Element,
388 observed_box: &ResizeObserverBoxOptions,
389) -> Rect<f64, CSSPixel> {
390 match observed_box {
391 ResizeObserverBoxOptions::Content_box => {
392 let content_box = target
395 .owner_window()
396 .box_area_query(cx, target.upcast(), BoxAreaType::Content, true)
397 .unwrap_or_else(Rect::zero);
398
399 Rect::new(
400 content_box.origin.map(|coordinate| coordinate.to_f64_px()),
401 Size2D::new(
402 content_box.size.width.to_f64_px(),
403 content_box.size.height.to_f64_px(),
404 ),
405 )
406 },
407 ResizeObserverBoxOptions::Border_box => {
408 let border_box = target
411 .owner_window()
412 .box_area_query(cx, target.upcast(), BoxAreaType::Border, true)
413 .unwrap_or_else(Rect::zero);
414
415 Rect::new(
416 border_box.origin.map(|coordinate| coordinate.to_f64_px()),
417 Size2D::new(
418 border_box.size.width.to_f64_px(),
419 border_box.size.height.to_f64_px(),
420 ),
421 )
422 },
423 ResizeObserverBoxOptions::Device_pixel_content_box => {
424 let device_pixel_ratio = target.owner_window().device_pixel_ratio().get() as f64;
425 let content_box = target
426 .owner_window()
427 .box_area_query(cx, target.upcast(), BoxAreaType::Content, true)
428 .unwrap_or_else(Rect::zero);
429
430 let to_device_px = |length: Au| (length.to_f64_px() * device_pixel_ratio).round();
431 Rect::new(
432 content_box.origin.map(to_device_px),
433 Size2D::new(
434 to_device_px(content_box.size.width),
435 to_device_px(content_box.size.height),
436 ),
437 )
438 },
439 }
440}
441
442impl script_bindings::callback::OwnerWindow<crate::DomTypeHolder> for ResizeObserver {}