/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::rc::Rc; use app_units::Au; use dom_struct::dom_struct; use euclid::Size2D; use euclid::default::Rect; use euclid::num::Zero; use html5ever::ns; use js::rust::HandleObject; use layout_api::BoxAreaType; use crate::dom::bindings::callback::ExceptionHandling; use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::ResizeObserverBinding::{ ResizeObserverBoxOptions, ResizeObserverCallback, ResizeObserverMethods, ResizeObserverOptions, }; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto}; use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::document::RenderingUpdateReason; use crate::dom::domrectreadonly::DOMRectReadOnly; use crate::dom::element::Element; use crate::dom::node::{Node, NodeTraits}; use crate::dom::resizeobserverentry::ResizeObserverEntry; use crate::dom::resizeobserversize::{ResizeObserverSize, ResizeObserverSizeImpl}; use crate::dom::window::Window; use crate::script_runtime::CanGc; /// #[derive(Debug, Default, PartialEq, PartialOrd)] pub(crate) struct ResizeObservationDepth(usize); impl ResizeObservationDepth { pub(crate) fn max() -> ResizeObservationDepth { ResizeObservationDepth(usize::MAX) } } /// /// See `ObservationState` for active and skipped observation targets. #[dom_struct] pub(crate) struct ResizeObserver { reflector_: Reflector, /// #[conditional_malloc_size_of] callback: Rc, /// /// /// This list simultaneously also represents the /// [`[[activeTargets]]`](https://drafts.csswg.org/resize-observer/#dom-resizeobserver-activetargets-slot) /// and [`[[skippedTargets]]`](https://drafts.csswg.org/resize-observer/#dom-resizeobserver-skippedtargets-slot) /// internal slots. observation_targets: DomRefCell)>>, } impl ResizeObserver { pub(crate) fn new_inherited(callback: Rc) -> ResizeObserver { ResizeObserver { reflector_: Reflector::new(), callback, observation_targets: Default::default(), } } fn new( window: &Window, proto: Option, callback: Rc, can_gc: CanGc, ) -> DomRoot { let observer = Box::new(ResizeObserver::new_inherited(callback)); reflect_dom_object_with_proto(observer, window, proto, can_gc) } /// Step 2 of /// /// pub(crate) fn gather_active_resize_observations_at_depth( &self, depth: &ResizeObservationDepth, has_active: &mut bool, ) { // Step 2.1 Clear observer’s [[activeTargets]], and [[skippedTargets]]. // NOTE: This happens as part of Step 2.2 // Step 2.2 For each observation in observer.[[observationTargets]] run this step: for (observation, target) in self.observation_targets.borrow_mut().iter_mut() { observation.state = Default::default(); // Step 2.2.1 If observation.isActive() is true if observation.is_active(target) { // Step 2.2.1.1 Let targetDepth be result of calculate depth for node for observation.target. let target_depth = calculate_depth_for_node(target); // Step 2.2.1.2 If targetDepth is greater than depth then add observation to [[activeTargets]]. if target_depth > *depth { observation.state = ObservationState::Active; *has_active = true; } // Step 2.2.1.3 Else add observation to [[skippedTargets]]. else { observation.state = ObservationState::Skipped; } } } } /// Step 2 of pub(crate) fn broadcast_active_resize_observations( &self, shallowest_target_depth: &mut ResizeObservationDepth, can_gc: CanGc, ) { // Step 2.1 If observer.[[activeTargets]] slot is empty, continue. // NOTE: Due to the way we implement the activeTarges internal slot we can't easily // know if it's empty. Instead we remember whether there were any active observation // targets during the following traversal and return if there were none. let mut has_active_observation_targets = false; // Step 2.2 Let entries be an empty list of ResizeObserverEntryies. let mut entries: Vec> = Default::default(); // Step 2.3 For each observation in [[activeTargets]] perform these steps: for (observation, target) in self.observation_targets.borrow_mut().iter_mut() { let ObservationState::Active = observation.state else { continue; }; has_active_observation_targets = true; let window = target.owner_window(); let entry = create_and_populate_a_resizeobserverentry(&window, target, observation, can_gc); entries.push(entry); observation.state = ObservationState::Done; let target_depth = calculate_depth_for_node(target); if target_depth < *shallowest_target_depth { *shallowest_target_depth = target_depth; } } if !has_active_observation_targets { return; } // Step 2.4 Invoke observer.[[callback]] with entries. let _ = self .callback .Call_(self, entries, self, ExceptionHandling::Report, can_gc); // Step 2.5 Clear observer.[[activeTargets]]. // NOTE: The observation state was modified in Step 2.2 } /// pub(crate) fn has_skipped_resize_observations(&self) -> bool { self.observation_targets .borrow() .iter() .any(|(observation, _)| observation.state == ObservationState::Skipped) } } /// fn create_and_populate_a_resizeobserverentry( window: &Window, target: &Element, observation: &mut ResizeObservation, can_gc: CanGc, ) -> DomRoot { // Step 3. Set this.borderBoxSize slot to result of calculating box size given target and observedBox of "border-box". let border_box_size = calculate_box_size(target, &ResizeObserverBoxOptions::Border_box); // Step 4. Set this.contentBoxSize slot to result of calculating box size given target and observedBox of "content-box". let content_box_size = calculate_box_size(target, &ResizeObserverBoxOptions::Content_box); // Step 5. Set this.devicePixelContentBoxSize slot to result of calculating box size given target and observedBox of "device-pixel-content-box". let device_pixel_content_box = calculate_box_size(target, &ResizeObserverBoxOptions::Device_pixel_content_box); // Note: this is safe because an observation is // initialized with one reported size (zero). // The spec plans to store multiple reported sizes, // but for now there can be only one. let last_size = match observation.observed_box { ResizeObserverBoxOptions::Content_box => content_box_size, ResizeObserverBoxOptions::Border_box => border_box_size, ResizeObserverBoxOptions::Device_pixel_content_box => device_pixel_content_box, }; let last_reported_size = ResizeObserverSizeImpl::new(last_size.width(), last_size.height()); if observation.last_reported_sizes.is_empty() { observation.last_reported_sizes.push(last_reported_size); } else { observation.last_reported_sizes[0] = last_reported_size; } // Step 7. If target is not an SVG element or target is an SVG element with an associated CSS layout box do these steps: let use_padding = *target.namespace() != ns!(svg) || target.has_css_layout_box(); let (padding_top, padding_left) = if use_padding { // Step 7.1. Set this.contentRect.top to target.padding top. // Step 7.2. Set this.contentRect.left to target.padding left. let padding = target.upcast::().padding().unwrap_or_default(); (padding.top, padding.left) } else { // Step 8. If target is an SVG element without an associated CSS layout box do these steps: // Step 8.1. Set this.contentRect.top and this.contentRect.left to 0. (Au::zero(), Au::zero()) }; // Step 6. Set this.contentRect to logical this.contentBoxSize given target and observedBox of "content-box". let content_rect = DOMRectReadOnly::new( window.upcast(), None, padding_left.to_f64_px(), padding_top.to_f64_px(), content_box_size.width(), content_box_size.height(), can_gc, ); let border_box_size = ResizeObserverSize::new( window, ResizeObserverSizeImpl::new(border_box_size.width(), border_box_size.height()), can_gc, ); let content_box_size = ResizeObserverSize::new( window, ResizeObserverSizeImpl::new(content_box_size.width(), content_box_size.height()), can_gc, ); let device_pixel_content_box = ResizeObserverSize::new( window, ResizeObserverSizeImpl::new( device_pixel_content_box.width(), device_pixel_content_box.height(), ), can_gc, ); // Step 1. Let this be a new ResizeObserverEntry. // Step 2. Set this.target slot to target. ResizeObserverEntry::new( window, target, &content_rect, &[&*border_box_size], &[&*content_box_size], &[&*device_pixel_content_box], can_gc, ) } impl ResizeObserverMethods for ResizeObserver { /// fn Constructor( window: &Window, proto: Option, can_gc: CanGc, callback: Rc, ) -> DomRoot { let rooted_observer = ResizeObserver::new(window, proto, callback, can_gc); let document = window.Document(); document.add_resize_observer(&rooted_observer); rooted_observer } /// fn Observe(&self, target: &Element, options: &ResizeObserverOptions) { // Step 1. If target is in [[observationTargets]] slot, call unobserve() with argument target. let is_present = self .observation_targets .borrow() .iter() .any(|(_obs, other)| &**other == target); if is_present { self.Unobserve(target); } // Step 2. Let observedBox be the value of the box dictionary member of options. // Step 3. Let resizeObservation be new ResizeObservation(target, observedBox). let resize_observation = ResizeObservation::new(options.box_); // Step 4. Add the resizeObservation to the [[observationTargets]] slot. self.observation_targets .borrow_mut() .push((resize_observation, Dom::from_ref(target))); target .owner_window() .Document() .add_rendering_update_reason( RenderingUpdateReason::ResizeObserverStartedObservingTarget, ); } /// fn Unobserve(&self, target: &Element) { self.observation_targets .borrow_mut() .retain_mut(|(_obs, other)| !(&**other == target)); } /// fn Disconnect(&self) { self.observation_targets.borrow_mut().clear(); } } /// State machine equivalent of active and skipped observations. #[derive(Default, MallocSizeOf, PartialEq)] enum ObservationState { #[default] Done, /// Active, /// Skipped, } /// /// /// Note: `target` is kept out of here, to avoid having to root the `ResizeObservation`. /// #[derive(JSTraceable, MallocSizeOf)] struct ResizeObservation { /// observed_box: ResizeObserverBoxOptions, /// last_reported_sizes: Vec, /// State machine mimicking the "active" and "skipped" targets slots of the observer. #[no_trace] state: ObservationState, } impl ResizeObservation { /// pub(crate) fn new(observed_box: ResizeObserverBoxOptions) -> ResizeObservation { ResizeObservation { observed_box, last_reported_sizes: vec![], state: Default::default(), } } /// fn is_active(&self, target: &Element) -> bool { let Some(last_reported_size) = self.last_reported_sizes.first() else { return true; }; let box_size = calculate_box_size(target, &self.observed_box); box_size.width() != last_reported_size.inline_size() || box_size.height() != last_reported_size.block_size() } } /// fn calculate_depth_for_node(target: &Element) -> ResizeObservationDepth { let node = target.upcast::(); let depth = node.inclusive_ancestors_in_flat_tree().count(); ResizeObservationDepth(depth) } /// /// /// The dimensions of the returned `Rect` depend on the type of box being observed. /// For `ResizeObserverBoxOptions::Content_box` and `ResizeObserverBoxOptions::Border_box`, /// the values will be in `px`. For `ResizeObserverBoxOptions::Device_pixel_content_box` they /// will be in integral device pixels. fn calculate_box_size(target: &Element, observed_box: &ResizeObserverBoxOptions) -> Rect { match observed_box { ResizeObserverBoxOptions::Content_box => { // Note: only taking first fragment, // but the spec will expand to cover all fragments. let content_box = target .owner_window() .box_area_query(target.upcast(), BoxAreaType::Content, true) .unwrap_or_else(Rect::zero); Rect::new( content_box.origin.map(|coordinate| coordinate.to_f64_px()), Size2D::new( content_box.size.width.to_f64_px(), content_box.size.height.to_f64_px(), ), ) }, ResizeObserverBoxOptions::Border_box => { // Note: only taking first fragment, // but the spec will expand to cover all fragments. let border_box = target .owner_window() .box_area_query(target.upcast(), BoxAreaType::Border, true) .unwrap_or_else(Rect::zero); Rect::new( border_box.origin.map(|coordinate| coordinate.to_f64_px()), Size2D::new( border_box.size.width.to_f64_px(), border_box.size.height.to_f64_px(), ), ) }, ResizeObserverBoxOptions::Device_pixel_content_box => { let device_pixel_ratio = target.owner_window().device_pixel_ratio(); let content_box = target .owner_window() .box_area_query(target.upcast(), BoxAreaType::Content, true) .unwrap_or_else(Rect::zero); Rect::new( content_box .origin .map(|coordinate| coordinate.to_nearest_pixel(device_pixel_ratio.get()) as f64), Size2D::new( content_box .size .width .to_nearest_pixel(device_pixel_ratio.get()) as f64, content_box .size .height .to_nearest_pixel(device_pixel_ratio.get()) as f64, ), ) }, } }