script/dom/resizeobserver/
resizeobserver.rs1use std::cell::{Cell, RefCell};
6use std::rc::Rc;
7
8use app_units::Au;
9use dom_struct::dom_struct;
10use euclid::num::Zero;
11use euclid::{Rect, Size2D};
12use html5ever::ns;
13use js::context::{JSContext, NoGC};
14use js::rust::HandleObject;
15use layout_api::BoxAreaType;
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 #[conditional_malloc_size_of]
53 callback: Rc<ResizeObserverCallback>,
54
55 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(cx, observer, window, proto)
81 }
82
83 pub(crate) fn gather_active_resize_observations_at_depth(
87 &self,
88 no_gc: &NoGC,
89 depth: &ResizeObservationDepth,
90 has_active: &mut bool,
91 ) {
92 for (observation, target) in self.observation_targets.borrow().iter() {
97 observation.state.set(Default::default());
98
99 if observation.is_active(target) {
101 let target_depth = calculate_depth_for_node(no_gc, target);
103
104 if target_depth > *depth {
106 observation.state.set(ObservationState::Active);
107 *has_active = true;
108 }
109 else {
111 observation.state.set(ObservationState::Skipped);
112 }
113 }
114 }
115 }
116
117 pub(crate) fn broadcast_active_resize_observations(
119 &self,
120 cx: &mut JSContext,
121 shallowest_target_depth: &mut ResizeObservationDepth,
122 ) {
123 let mut has_active_observation_targets = false;
128
129 let mut entries: Vec<DomRoot<ResizeObserverEntry>> = Default::default();
131
132 for (observation, target) in self.observation_targets.borrow().iter() {
134 let ObservationState::Active = observation.state.get() else {
135 continue;
136 };
137 has_active_observation_targets = true;
138
139 let window = target.owner_window();
140 let entry = create_and_populate_a_resizeobserverentry(cx, &window, target, observation);
141 entries.push(entry);
142 observation.state.set(ObservationState::Done);
143
144 let target_depth = calculate_depth_for_node(cx.no_gc(), target);
145 if target_depth < *shallowest_target_depth {
146 *shallowest_target_depth = target_depth;
147 }
148 }
149
150 if !has_active_observation_targets {
151 return;
152 }
153
154 let _ = self
156 .callback
157 .Call_(cx, self, entries, self, ExceptionHandling::Report);
158
159 }
162
163 pub(crate) fn has_skipped_resize_observations(&self) -> bool {
165 self.observation_targets
166 .borrow()
167 .iter()
168 .any(|(observation, _)| observation.state.get() == ObservationState::Skipped)
169 }
170}
171
172fn create_and_populate_a_resizeobserverentry(
174 cx: &mut JSContext,
175 window: &Window,
176 target: &Element,
177 observation: &ResizeObservation,
178) -> DomRoot<ResizeObserverEntry> {
179 let border_box_size = calculate_box_size(target, &ResizeObserverBoxOptions::Border_box);
181 let content_box_size = calculate_box_size(target, &ResizeObserverBoxOptions::Content_box);
183
184 let device_pixel_content_box =
186 calculate_box_size(target, &ResizeObserverBoxOptions::Device_pixel_content_box);
187
188 let last_size = match observation.observed_box {
193 ResizeObserverBoxOptions::Content_box => content_box_size,
194 ResizeObserverBoxOptions::Border_box => border_box_size,
195 ResizeObserverBoxOptions::Device_pixel_content_box => device_pixel_content_box,
196 };
197 let last_reported_size = ResizeObserverSizeImpl::new(last_size.width(), last_size.height());
198
199 {
200 let mut sizes = observation.last_reported_sizes.borrow_mut();
201 if sizes.is_empty() {
202 sizes.push(last_reported_size);
203 } else {
204 sizes[0] = last_reported_size;
205 }
206 }
207
208 let use_padding = *target.namespace() != ns!(svg) || target.has_css_layout_box();
210 let (padding_top, padding_left) = if use_padding {
211 let padding = target.upcast::<Node>().padding().unwrap_or_default();
214 (padding.top, padding.left)
215 } else {
216 (Au::zero(), Au::zero())
219 };
220
221 let content_rect = DOMRectReadOnly::new(
223 cx,
224 window.upcast(),
225 None,
226 padding_left.to_f64_px(),
227 padding_top.to_f64_px(),
228 content_box_size.width(),
229 content_box_size.height(),
230 );
231
232 let border_box_size = ResizeObserverSize::new(
233 cx,
234 window,
235 ResizeObserverSizeImpl::new(border_box_size.width(), border_box_size.height()),
236 );
237 let content_box_size = ResizeObserverSize::new(
238 cx,
239 window,
240 ResizeObserverSizeImpl::new(content_box_size.width(), content_box_size.height()),
241 );
242 let device_pixel_content_box = ResizeObserverSize::new(
243 cx,
244 window,
245 ResizeObserverSizeImpl::new(
246 device_pixel_content_box.width(),
247 device_pixel_content_box.height(),
248 ),
249 );
250
251 ResizeObserverEntry::new(
254 cx,
255 window,
256 target,
257 &content_rect,
258 &[&*border_box_size],
259 &[&*content_box_size],
260 &[&*device_pixel_content_box],
261 )
262}
263
264impl ResizeObserverMethods<crate::DomTypeHolder> for ResizeObserver {
265 fn Constructor(
267 cx: &mut JSContext,
268 window: &Window,
269 proto: Option<HandleObject>,
270 callback: Rc<ResizeObserverCallback>,
271 ) -> DomRoot<ResizeObserver> {
272 let rooted_observer = ResizeObserver::new(cx, window, proto, callback);
273 let document = window.Document();
274 document.add_resize_observer(&rooted_observer);
275 rooted_observer
276 }
277
278 fn Observe(&self, target: &Element, options: &ResizeObserverOptions) {
280 let is_present = self
282 .observation_targets
283 .borrow()
284 .iter()
285 .any(|(_obs, other)| &**other == target);
286 if is_present {
287 self.Unobserve(target);
288 }
289
290 let resize_observation = ResizeObservation::new(options.box_);
293
294 self.observation_targets
296 .borrow_mut()
297 .push((resize_observation, Dom::from_ref(target)));
298 target
299 .owner_window()
300 .Document()
301 .add_rendering_update_reason(
302 RenderingUpdateReason::ResizeObserverStartedObservingTarget,
303 );
304 }
305
306 fn Unobserve(&self, target: &Element) {
308 self.observation_targets
309 .borrow_mut()
310 .retain_mut(|(_obs, other)| !(&**other == target));
311 }
312
313 fn Disconnect(&self) {
315 self.observation_targets.borrow_mut().clear();
316 }
317}
318
319#[derive(Copy, Clone, Default, MallocSizeOf, PartialEq)]
321enum ObservationState {
322 #[default]
323 Done,
324 Active,
326 Skipped,
328}
329
330#[derive(JSTraceable, MallocSizeOf)]
335struct ResizeObservation {
336 observed_box: ResizeObserverBoxOptions,
338 last_reported_sizes: RefCell<Vec<ResizeObserverSizeImpl>>,
340 #[no_trace]
342 state: Cell<ObservationState>,
343}
344
345impl ResizeObservation {
346 pub(crate) fn new(observed_box: ResizeObserverBoxOptions) -> ResizeObservation {
348 ResizeObservation {
349 observed_box,
350 last_reported_sizes: RefCell::new(vec![]),
351 state: Default::default(),
352 }
353 }
354
355 fn is_active(&self, target: &Element) -> bool {
357 let Some(last_reported_size) = self.last_reported_sizes.borrow().first().copied() else {
358 return true;
359 };
360 let box_size = calculate_box_size(target, &self.observed_box);
361 box_size.width() != last_reported_size.inline_size() ||
362 box_size.height() != last_reported_size.block_size()
363 }
364}
365
366fn calculate_depth_for_node(no_gc: &NoGC, target: &Element) -> ResizeObservationDepth {
368 let node = target.upcast::<Node>();
369 let depth = node
370 .inclusive_ancestors_in_flat_tree_unrooted(no_gc)
371 .count();
372 ResizeObservationDepth(depth)
373}
374
375fn calculate_box_size(
382 target: &Element,
383 observed_box: &ResizeObserverBoxOptions,
384) -> Rect<f64, CSSPixel> {
385 match observed_box {
386 ResizeObserverBoxOptions::Content_box => {
387 let content_box = target
390 .owner_window()
391 .box_area_query(target.upcast(), BoxAreaType::Content, true)
392 .unwrap_or_else(Rect::zero);
393
394 Rect::new(
395 content_box.origin.map(|coordinate| coordinate.to_f64_px()),
396 Size2D::new(
397 content_box.size.width.to_f64_px(),
398 content_box.size.height.to_f64_px(),
399 ),
400 )
401 },
402 ResizeObserverBoxOptions::Border_box => {
403 let border_box = target
406 .owner_window()
407 .box_area_query(target.upcast(), BoxAreaType::Border, true)
408 .unwrap_or_else(Rect::zero);
409
410 Rect::new(
411 border_box.origin.map(|coordinate| coordinate.to_f64_px()),
412 Size2D::new(
413 border_box.size.width.to_f64_px(),
414 border_box.size.height.to_f64_px(),
415 ),
416 )
417 },
418 ResizeObserverBoxOptions::Device_pixel_content_box => {
419 let device_pixel_ratio = target.owner_window().device_pixel_ratio().get() as f64;
420 let content_box = target
421 .owner_window()
422 .box_area_query(target.upcast(), BoxAreaType::Content, true)
423 .unwrap_or_else(Rect::zero);
424
425 let to_device_px = |length: Au| (length.to_f64_px() * device_pixel_ratio).round();
426 Rect::new(
427 content_box.origin.map(to_device_px),
428 Size2D::new(
429 to_device_px(content_box.size.width),
430 to_device_px(content_box.size.height),
431 ),
432 )
433 },
434 }
435}
436
437impl script_bindings::callback::OwnerWindow<crate::DomTypeHolder> for ResizeObserver {}