Skip to main content

webrender/
spatial_tree.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 http://mozilla.org/MPL/2.0/. */
4
5use api::{ExternalScrollId, PropertyBinding, ReferenceFrameKind, TransformStyle, PropertyBindingId};
6use api::{APZScrollGeneration, HasScrollLinkedEffect, PipelineId, SampledScrollOffset};
7use api::units::*;
8use euclid::Transform3D;
9use crate::transform::TransformPalette;
10use crate::internal_types::{FastHashMap, FrameMemory};
11use crate::print_tree::{PrintableTree, PrintTree, PrintTreePrinter};
12use crate::scene::SceneProperties;
13use crate::spatial_node::{ReferenceFrameInfo, SpatialNode, SpatialNodeDescriptor, SpatialNodeType, StickyFrameInfo};
14use crate::spatial_node::{ScrollFrameKind, SceneSpatialNode, SpatialNodeInfo};
15use std::{ops, u32};
16use crate::util::{FastTransform, LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset, scale_factors};
17use smallvec::SmallVec;
18use crate::util::TransformedRectKind;
19use peek_poke::PeekPoke;
20
21
22/// An id that identifies coordinate systems in the SpatialTree. Each
23/// coordinate system has an id and those ids will be shared when the coordinates
24/// system are the same or are in the same axis-aligned space. This allows
25/// for optimizing mask generation.
26#[derive(Copy, Clone, PartialEq, PartialOrd)]
27#[cfg_attr(feature = "capture", derive(Serialize))]
28#[cfg_attr(feature = "replay", derive(Deserialize))]
29pub struct CoordinateSystemId(pub u32);
30
31impl std::fmt::Debug for CoordinateSystemId {
32    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
33        write!(f, "#{}", self.0)
34    }
35}
36
37/// A node in the hierarchy of coordinate system
38/// transforms.
39#[derive(Debug)]
40#[cfg_attr(feature = "capture", derive(Serialize))]
41#[cfg_attr(feature = "replay", derive(Deserialize))]
42pub struct CoordinateSystem {
43    pub transform: LayoutTransform,
44    pub world_transform: LayoutToWorldTransform,
45    pub should_flatten: bool,
46    pub parent: Option<CoordinateSystemId>,
47}
48
49impl CoordinateSystem {
50    fn root() -> Self {
51        CoordinateSystem {
52            transform: LayoutTransform::identity(),
53            world_transform: LayoutToWorldTransform::identity(),
54            should_flatten: false,
55            parent: None,
56        }
57    }
58}
59
60#[derive(Copy, Clone, Eq, Hash, MallocSizeOf, PartialEq, PeekPoke, Default)]
61#[cfg_attr(feature = "capture", derive(Serialize))]
62#[cfg_attr(feature = "replay", derive(Deserialize))]
63pub struct SpatialNodeIndex(pub u32);
64
65impl SpatialNodeIndex {
66    pub const INVALID: SpatialNodeIndex = SpatialNodeIndex(u32::MAX);
67
68    /// May be set on a cluster / picture during scene building if the spatial
69    /// node is not known at this time. It must be set to a valid value before
70    /// scene building is complete (by `finalize_picture`). In future, we could
71    /// make this type-safe with a wrapper type to ensure we know when a spatial
72    /// node index may have an unknown value.
73    pub const UNKNOWN: SpatialNodeIndex = SpatialNodeIndex(u32::MAX - 1);
74}
75
76impl std::fmt::Debug for SpatialNodeIndex {
77    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
78        if *self == Self::INVALID {
79            write!(f, "<invalid>")
80        } else if *self == Self::UNKNOWN {
81            write!(f, "<unknown>")
82        } else {
83            write!(f, "#{}", self.0)
84        }
85    }
86}
87
88// In some cases, the conversion from CSS pixels to device pixels can result in small
89// rounding errors when calculating the scrollable distance of a scroll frame. Apply
90// a small epsilon so that we don't detect these frames as "real" scroll frames.
91const MIN_SCROLLABLE_AMOUNT: f32 = 0.01;
92
93// The minimum size for a scroll frame for it to be considered for a scroll root.
94const MIN_SCROLL_ROOT_SIZE: f32 = 128.0;
95
96impl SpatialNodeIndex {
97    pub fn new(index: usize) -> Self {
98        debug_assert!(index < ::std::u32::MAX as usize);
99        SpatialNodeIndex(index as u32)
100    }
101}
102
103impl CoordinateSystemId {
104    pub fn root() -> Self {
105        CoordinateSystemId(0)
106    }
107}
108
109#[derive(Debug, Copy, Clone, PartialEq)]
110pub enum VisibleFace {
111    Front,
112    Back,
113}
114
115impl Default for VisibleFace {
116    fn default() -> Self {
117        VisibleFace::Front
118    }
119}
120
121impl ops::Not for VisibleFace {
122    type Output = Self;
123    fn not(self) -> Self {
124        match self {
125            VisibleFace::Front => VisibleFace::Back,
126            VisibleFace::Back => VisibleFace::Front,
127        }
128    }
129}
130
131/// Allows functions and methods to retrieve common information about
132/// a spatial node, whether during scene or frame building
133pub trait SpatialNodeContainer {
134    /// Get the common information for a given spatial node
135    fn get_node_info(&self, index: SpatialNodeIndex) -> SpatialNodeInfo;
136
137    fn get_snapping_info(
138        &self,
139        parent_index: Option<SpatialNodeIndex>
140    ) -> Option<ScaleOffset> {
141        match parent_index {
142            Some(parent_index) => {
143                let node_info = self.get_node_info(parent_index);
144                node_info.snapping_transform
145            }
146            None => {
147                Some(ScaleOffset::identity())
148            }
149        }
150    }
151}
152
153/// The representation of the spatial tree during scene building, which is
154/// mostly write-only, with a small number of queries for snapping,
155/// picture cache building.
156///
157/// Each `SceneBuilder::build` call calls `reset()` to start the tree fresh,
158/// then emits a complete list of `SpatialTreeUpdate::Insert` ops that the
159/// frame-side `SpatialTree::apply_updates` consumes verbatim.
160#[cfg_attr(feature = "capture", derive(Serialize))]
161#[cfg_attr(feature = "replay", derive(Deserialize))]
162pub struct SceneSpatialTree {
163    /// Nodes which determine the positions (offsets and transforms) for primitives
164    /// and clips.
165    spatial_nodes: Vec<SceneSpatialNode>,
166
167    root_reference_frame_index: SpatialNodeIndex,
168
169    updates: SpatialTreeUpdates,
170}
171
172impl SpatialNodeContainer for SceneSpatialTree {
173    fn get_node_info(&self, index: SpatialNodeIndex) -> SpatialNodeInfo {
174        let node = &self.spatial_nodes[index.0 as usize];
175
176        SpatialNodeInfo {
177            parent: node.parent,
178            node_type: &node.descriptor.node_type,
179            snapping_transform: node.snapping_transform,
180        }
181    }
182}
183
184impl SceneSpatialTree {
185    pub fn new() -> Self {
186        let mut tree = SceneSpatialTree {
187            spatial_nodes: Vec::new(),
188            root_reference_frame_index: SpatialNodeIndex(0),
189            updates: SpatialTreeUpdates::new(),
190        };
191
192        tree.add_root_reference_frame();
193
194        tree
195    }
196
197    /// Reset the tree to an empty state with just the root reference frame.
198    /// Called at the start of each scene build.
199    pub fn reset(&mut self) {
200        self.spatial_nodes.clear();
201        self.updates = SpatialTreeUpdates::new();
202        self.add_root_reference_frame();
203    }
204
205    fn add_root_reference_frame(&mut self) {
206        let node = SceneSpatialNode::new_reference_frame(
207            None,
208            TransformStyle::Flat,
209            PropertyBinding::Value(LayoutTransform::identity()),
210            ReferenceFrameKind::Transform {
211                should_snap: true,
212                is_2d_scale_translation: true,
213                paired_with_perspective: false,
214            },
215            LayoutVector2D::zero(),
216            PipelineId::dummy(),
217            true,
218            true,
219        );
220
221        let index = self.add_spatial_node(node);
222        debug_assert_eq!(index, SpatialNodeIndex(0));
223    }
224
225    pub fn is_root_coord_system(&self, index: SpatialNodeIndex) -> bool {
226        self.spatial_nodes[index.0 as usize].is_root_coord_system
227    }
228
229    /// Complete building this scene, return the updates to apply to the frame spatial tree
230    pub fn end_frame_and_get_pending_updates(&mut self) -> SpatialTreeUpdates {
231        self.updates.root_reference_frame_index = self.root_reference_frame_index;
232        std::mem::replace(&mut self.updates, SpatialTreeUpdates::new())
233    }
234
235    /// Check if a given spatial node is an ancestor of another spatial node.
236    pub fn is_ancestor(
237        &self,
238        maybe_parent: SpatialNodeIndex,
239        maybe_child: SpatialNodeIndex,
240    ) -> bool {
241        // Early out if same node
242        if maybe_parent == maybe_child {
243            return false;
244        }
245
246        let mut current_node = maybe_child;
247
248        while current_node != self.root_reference_frame_index {
249            let node = self.get_node_info(current_node);
250            current_node = node.parent.expect("bug: no parent");
251
252            if current_node == maybe_parent {
253                return true;
254            }
255        }
256
257        false
258    }
259
260    /// Find the spatial node that is the scroll root for a given spatial node.
261    /// A scroll root is the first spatial node when found travelling up the
262    /// spatial node tree that is an explicit scroll frame.
263    pub fn find_scroll_root(
264        &self,
265        spatial_node_index: SpatialNodeIndex,
266        allow_sticky_frames: bool,
267    ) -> SpatialNodeIndex {
268        let mut real_scroll_root = self.root_reference_frame_index;
269        let mut outermost_scroll_root = self.root_reference_frame_index;
270        let mut current_scroll_root_is_sticky = false;
271        let mut node_index = spatial_node_index;
272
273        while node_index != self.root_reference_frame_index {
274            let node = self.get_node_info(node_index);
275            match node.node_type {
276                SpatialNodeType::ReferenceFrame(ref info) => {
277                    match info.kind {
278                        ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } => {
279                            // We can handle scroll nodes that pass through a 2d scale/translation node
280                        }
281                        ReferenceFrameKind::Transform { is_2d_scale_translation: false, .. } |
282                        ReferenceFrameKind::Perspective { .. } => {
283                            // When a reference frame is encountered, forget any scroll roots
284                            // we have encountered, as they may end up with a non-axis-aligned transform.
285                            real_scroll_root = self.root_reference_frame_index;
286                            outermost_scroll_root = self.root_reference_frame_index;
287                            current_scroll_root_is_sticky = false;
288                        }
289                    }
290                }
291                SpatialNodeType::StickyFrame(..) => {
292                    // Though not a scroll frame, we optionally treat sticky frames as scroll roots
293                    // to ensure they are given a separate picture cache slice.
294                    if allow_sticky_frames {
295                        outermost_scroll_root = node_index;
296                        real_scroll_root = node_index;
297                        // Set this true so that we don't select an ancestor scroll frame as the scroll root
298                        // on a subsequent iteration.
299                        current_scroll_root_is_sticky = true;
300                    }
301                }
302                SpatialNodeType::ScrollFrame(ref info) => {
303                    match info.frame_kind {
304                        ScrollFrameKind::PipelineRoot { is_root_pipeline } => {
305                            // Once we encounter a pipeline root, there is no need to look further
306                            if is_root_pipeline {
307                                break;
308                            }
309                        }
310                        ScrollFrameKind::Explicit => {
311                            // Store the closest scroll root we find to the root, for use
312                            // later on, even if it's not actually scrollable.
313                            outermost_scroll_root = node_index;
314
315                            // If the previously identified scroll root is sticky then we don't
316                            // want to choose an ancestor scroll root, as we want the sticky item
317                            // to have its own picture cache slice.
318                            if !current_scroll_root_is_sticky {
319                                // If the scroll root has no scrollable area, we don't want to
320                                // consider it. This helps pages that have a nested scroll root
321                                // within a redundant scroll root to avoid selecting the wrong
322                                // reference spatial node for a picture cache.
323                                if info.scrollable_size.width > MIN_SCROLLABLE_AMOUNT ||
324                                   info.scrollable_size.height > MIN_SCROLLABLE_AMOUNT {
325                                    // Since we are skipping redundant scroll roots, we may end up
326                                    // selecting inner scroll roots that are very small. There is
327                                    // no performance benefit to creating a slice for these roots,
328                                    // as they are cheap to rasterize. The size comparison is in
329                                    // local-space, but makes for a reasonable estimate. The value
330                                    // is arbitrary, but is generally small enough to ignore things
331                                    // like scroll roots around text input elements.
332                                    if info.viewport_rect.width() > MIN_SCROLL_ROOT_SIZE &&
333                                       info.viewport_rect.height() > MIN_SCROLL_ROOT_SIZE {
334                                        // If we've found a root that is scrollable, and a reasonable
335                                        // size, select that as the current root for this node
336                                        real_scroll_root = node_index;
337                                    }
338                                }
339                            }
340                        }
341                    }
342                }
343            }
344            node_index = node.parent.expect("unable to find parent node");
345        }
346
347        // If we didn't find any real (scrollable) frames, then return the outermost
348        // redundant scroll frame. This is important so that we can correctly find
349        // the clips defined on the content which should be handled when drawing the
350        // picture cache tiles (by definition these clips are ancestors of the
351        // scroll root selected for the picture cache).
352        if real_scroll_root == self.root_reference_frame_index {
353            outermost_scroll_root
354        } else {
355            real_scroll_root
356        }
357    }
358
359    /// The root reference frame, which is the true root of the SpatialTree.
360    pub fn root_reference_frame_index(&self) -> SpatialNodeIndex {
361        self.root_reference_frame_index
362    }
363
364    fn add_spatial_node(
365        &mut self,
366        mut node: SceneSpatialNode,
367    ) -> SpatialNodeIndex {
368        let parent_info = self.get_snapping_info(node.parent);
369
370        node.snapping_transform = calculate_snapping_transform(
371            parent_info,
372            &node.descriptor.node_type,
373        );
374
375        let descriptor = node.descriptor.clone();
376        let parent = node.parent;
377
378        let index = self.spatial_nodes.len();
379        self.spatial_nodes.push(node);
380
381        self.updates.updates.push(SpatialTreeUpdate {
382            index,
383            descriptor,
384            parent,
385        });
386
387        SpatialNodeIndex(index as u32)
388    }
389
390    pub fn add_reference_frame(
391        &mut self,
392        parent_index: SpatialNodeIndex,
393        transform_style: TransformStyle,
394        source_transform: PropertyBinding<LayoutTransform>,
395        kind: ReferenceFrameKind,
396        origin_in_parent_reference_frame: LayoutVector2D,
397        pipeline_id: PipelineId,
398        is_pipeline_root: bool,
399    ) -> SpatialNodeIndex {
400        // Determine if this reference frame creates a new static coordinate system
401        let new_static_coord_system = match kind {
402            ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } => {
403                // Client has guaranteed this transform will only be axis-aligned
404                false
405            }
406            ReferenceFrameKind::Transform { is_2d_scale_translation: false, .. } | ReferenceFrameKind::Perspective { .. } => {
407                // Even if client hasn't promised it's an axis-aligned transform, we can still
408                // check this so long as the transform isn't animated (and thus could change to
409                // anything by APZ during frame building)
410                match source_transform {
411                    PropertyBinding::Value(m) => {
412                        !m.is_2d_scale_translation()
413                    }
414                    PropertyBinding::Binding(..) => {
415                        // Animated, so assume it may introduce a complex transform
416                        true
417                    }
418                }
419            }
420        };
421
422        let is_root_coord_system = !new_static_coord_system &&
423            self.spatial_nodes[parent_index.0 as usize].is_root_coord_system;
424
425        let node = SceneSpatialNode::new_reference_frame(
426            Some(parent_index),
427            transform_style,
428            source_transform,
429            kind,
430            origin_in_parent_reference_frame,
431            pipeline_id,
432            is_root_coord_system,
433            is_pipeline_root,
434        );
435        self.add_spatial_node(node)
436    }
437
438    pub fn add_scroll_frame(
439        &mut self,
440        parent_index: SpatialNodeIndex,
441        external_id: ExternalScrollId,
442        pipeline_id: PipelineId,
443        frame_rect: &LayoutRect,
444        content_size: &LayoutSize,
445        frame_kind: ScrollFrameKind,
446        external_scroll_offset: LayoutVector2D,
447        scroll_offset_generation: APZScrollGeneration,
448        has_scroll_linked_effect: HasScrollLinkedEffect,
449    ) -> SpatialNodeIndex {
450        // Scroll frames are only 2d translations - they can't introduce a new static coord system
451        let is_root_coord_system = self.spatial_nodes[parent_index.0 as usize].is_root_coord_system;
452
453        let node = SceneSpatialNode::new_scroll_frame(
454            pipeline_id,
455            parent_index,
456            external_id,
457            frame_rect,
458            content_size,
459            frame_kind,
460            external_scroll_offset,
461            scroll_offset_generation,
462            has_scroll_linked_effect,
463            is_root_coord_system,
464        );
465        self.add_spatial_node(node)
466    }
467
468    pub fn add_sticky_frame(
469        &mut self,
470        parent_index: SpatialNodeIndex,
471        sticky_frame_info: StickyFrameInfo,
472        pipeline_id: PipelineId,
473    ) -> SpatialNodeIndex {
474        // Sticky frames are only 2d translations - they can't introduce a new static coord system
475        let is_root_coord_system = self.spatial_nodes[parent_index.0 as usize].is_root_coord_system;
476
477        let node = SceneSpatialNode::new_sticky_frame(
478            parent_index,
479            sticky_frame_info,
480            pipeline_id,
481            is_root_coord_system,
482        );
483        self.add_spatial_node(node)
484    }
485}
486
487#[cfg_attr(feature = "capture", derive(Serialize))]
488#[cfg_attr(feature = "replay", derive(Deserialize))]
489pub struct SpatialTreeUpdate {
490    pub index: usize,
491    pub parent: Option<SpatialNodeIndex>,
492    pub descriptor: SpatialNodeDescriptor,
493}
494
495/// The full set of spatial nodes for the scene that just finished building.
496/// `apply_updates` consumes this by replacing the frame-side tree wholesale.
497#[cfg_attr(feature = "capture", derive(Serialize))]
498#[cfg_attr(feature = "replay", derive(Deserialize))]
499pub struct SpatialTreeUpdates {
500    root_reference_frame_index: SpatialNodeIndex,
501    updates: Vec<SpatialTreeUpdate>,
502}
503
504impl SpatialTreeUpdates {
505    fn new() -> Self {
506        SpatialTreeUpdates {
507            root_reference_frame_index: SpatialNodeIndex::INVALID,
508            updates: Vec::new(),
509        }
510    }
511}
512
513/// Represents the spatial tree during frame building, which is mostly
514/// read-only, apart from the tree update at the start of the frame
515#[cfg_attr(feature = "capture", derive(Serialize))]
516#[cfg_attr(feature = "replay", derive(Deserialize))]
517pub struct SpatialTree {
518    /// Nodes which determine the positions (offsets and transforms) for primitives
519    /// and clips.
520    spatial_nodes: Vec<SpatialNode>,
521
522    /// A list of transforms that establish new coordinate systems.
523    /// Spatial nodes only establish a new coordinate system when
524    /// they have a transform that is not a simple 2d translation.
525    coord_systems: Vec<CoordinateSystem>,
526
527    root_reference_frame_index: SpatialNodeIndex,
528
529    /// Stack of current state for each parent node while traversing and updating tree
530    update_state_stack: Vec<TransformUpdateState>,
531}
532
533#[derive(Clone)]
534#[cfg_attr(feature = "capture", derive(Serialize))]
535#[cfg_attr(feature = "replay", derive(Deserialize))]
536pub struct TransformUpdateState {
537    pub parent_reference_frame_transform: LayoutToWorldFastTransform,
538    pub parent_accumulated_scroll_offset: LayoutVector2D,
539    pub nearest_scrolling_ancestor_offset: LayoutVector2D,
540    pub nearest_scrolling_ancestor_viewport: LayoutRect,
541
542    /// An id for keeping track of the axis-aligned space of this node. This is used in
543    /// order to to track what kinds of clip optimizations can be done for a particular
544    /// display list item, since optimizations can usually only be done among
545    /// coordinate systems which are relatively axis aligned.
546    pub current_coordinate_system_id: CoordinateSystemId,
547
548    /// Scale and offset from the coordinate system that started this compatible coordinate system.
549    pub coordinate_system_relative_scale_offset: ScaleOffset,
550
551    /// True if this node is transformed by an invertible transform.  If not, display items
552    /// transformed by this node will not be displayed and display items not transformed by this
553    /// node will not be clipped by clips that are transformed by this node.
554    pub invertible: bool,
555
556    /// True if this node is a part of Preserve3D hierarchy.
557    pub preserves_3d: bool,
558
559    /// True if the any parent nodes are currently zooming
560    pub is_ancestor_or_self_zooming: bool,
561
562    /// Set to true if this state represents a scroll node with external id
563    pub external_id: Option<ExternalScrollId>,
564
565    /// The node scroll offset if this state is a scroll/sticky node. Zero if a reference frame.
566    pub scroll_offset: LayoutVector2D,
567}
568
569/// Transformation between two nodes in the spatial tree that can sometimes be
570/// encoded more efficiently than with a full matrix.
571#[derive(Debug, Clone)]
572pub enum CoordinateSpaceMapping<Src, Dst> {
573    Local,
574    ScaleOffset(ScaleOffset),
575    Transform(Transform3D<f32, Src, Dst>),
576}
577
578impl<Src, Dst> CoordinateSpaceMapping<Src, Dst> {
579    pub fn into_transform(self) -> Transform3D<f32, Src, Dst> {
580        match self {
581            CoordinateSpaceMapping::Local => Transform3D::identity(),
582            CoordinateSpaceMapping::ScaleOffset(scale_offset) => scale_offset.to_transform(),
583            CoordinateSpaceMapping::Transform(transform) => transform,
584        }
585    }
586
587    pub fn into_fast_transform(self) -> FastTransform<Src, Dst> {
588        match self {
589            CoordinateSpaceMapping::Local => FastTransform::identity(),
590            CoordinateSpaceMapping::ScaleOffset(scale_offset) => FastTransform::with_scale_offset(scale_offset),
591            CoordinateSpaceMapping::Transform(transform) => FastTransform::with_transform(transform),
592        }
593    }
594
595    pub fn is_perspective(&self) -> bool {
596        match *self {
597            CoordinateSpaceMapping::Local |
598            CoordinateSpaceMapping::ScaleOffset(_) => false,
599            CoordinateSpaceMapping::Transform(ref transform) => transform.has_perspective_component(),
600        }
601    }
602
603    pub fn is_2d_axis_aligned(&self) -> bool {
604        match *self {
605            CoordinateSpaceMapping::Local |
606            CoordinateSpaceMapping::ScaleOffset(_) => true,
607            CoordinateSpaceMapping::Transform(ref transform) => transform.preserves_2d_axis_alignment(),
608        }
609    }
610
611    pub fn is_2d_scale_translation(&self) -> bool {
612        match *self {
613            CoordinateSpaceMapping::Local |
614            CoordinateSpaceMapping::ScaleOffset(_) => true,
615            CoordinateSpaceMapping::Transform(ref transform) => transform.is_2d_scale_translation(),
616        }
617    }
618
619    pub fn scale_factors(&self) -> (f32, f32) {
620        match *self {
621            CoordinateSpaceMapping::Local => (1.0, 1.0),
622            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => (scale_offset.scale.x.abs(), scale_offset.scale.y.abs()),
623            CoordinateSpaceMapping::Transform(ref transform) => scale_factors(transform),
624        }
625    }
626
627    pub fn inverse(&self) -> Option<CoordinateSpaceMapping<Dst, Src>> {
628        match *self {
629            CoordinateSpaceMapping::Local => Some(CoordinateSpaceMapping::Local),
630            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
631                Some(CoordinateSpaceMapping::ScaleOffset(scale_offset.inverse()))
632            }
633            CoordinateSpaceMapping::Transform(ref transform) => {
634                transform.inverse().map(CoordinateSpaceMapping::Transform)
635            }
636        }
637    }
638
639    pub fn as_2d_scale_offset(&self) -> Option<ScaleOffset> {
640        Some(match *self {
641            CoordinateSpaceMapping::Local => ScaleOffset::identity(),
642            CoordinateSpaceMapping::ScaleOffset(transfrom) => transfrom,
643            CoordinateSpaceMapping::Transform(ref transform) => {
644                if !transform.is_2d_scale_translation() {
645                    return None
646                }
647                ScaleOffset::new(transform.m11, transform.m22, transform.m41, transform.m42)
648            }
649        })
650    }
651}
652
653enum TransformScroll {
654    Scrolled,
655    Unscrolled,
656}
657
658impl SpatialNodeContainer for SpatialTree {
659    fn get_node_info(&self, index: SpatialNodeIndex) -> SpatialNodeInfo {
660        let node = self.get_spatial_node(index);
661
662        SpatialNodeInfo {
663            parent: node.parent,
664            node_type: &node.node_type,
665            snapping_transform: node.snapping_transform,
666        }
667    }
668}
669
670impl SpatialTree {
671    pub fn new() -> Self {
672        SpatialTree {
673            spatial_nodes: Vec::new(),
674            coord_systems: Vec::new(),
675            root_reference_frame_index: SpatialNodeIndex::INVALID,
676            update_state_stack: Vec::new(),
677        }
678    }
679
680    fn visit_node_impl_mut<F>(
681        &mut self,
682        index: SpatialNodeIndex,
683        f: &mut F,
684    ) where F: FnMut(SpatialNodeIndex, &mut SpatialNode) {
685        let mut child_indices: SmallVec<[SpatialNodeIndex; 8]> = SmallVec::new();
686
687        let node = self.get_spatial_node_mut(index);
688        f(index, node);
689        child_indices.extend_from_slice(&node.children);
690
691        for child_index in child_indices {
692            self.visit_node_impl_mut(child_index, f);
693        }
694    }
695
696    fn visit_node_impl<F>(
697        &self,
698        index: SpatialNodeIndex,
699        f: &mut F,
700    ) where F: FnMut(SpatialNodeIndex, &SpatialNode) {
701        let node = self.get_spatial_node(index);
702
703        f(index, node);
704
705        for child_index in &node.children {
706            self.visit_node_impl(*child_index, f);
707        }
708    }
709
710    /// Visit all nodes from the root of the tree, invoking a closure on each one
711    pub fn visit_nodes<F>(&self, mut f: F) where F: FnMut(SpatialNodeIndex, &SpatialNode) {
712        if self.root_reference_frame_index == SpatialNodeIndex::INVALID {
713            return;
714        }
715
716        self.visit_node_impl(self.root_reference_frame_index, &mut f);
717    }
718
719    /// Visit all nodes from the root of the tree, invoking a closure on each one
720    pub fn visit_nodes_mut<F>(&mut self, mut f: F) where F: FnMut(SpatialNodeIndex, &mut SpatialNode) {
721        if self.root_reference_frame_index == SpatialNodeIndex::INVALID {
722            return;
723        }
724
725        self.visit_node_impl_mut(self.root_reference_frame_index, &mut f);
726    }
727
728    /// Replace this tree with the contents of a freshly-built scene.
729    pub fn apply_updates(
730        &mut self,
731        updates: SpatialTreeUpdates,
732    ) {
733        self.root_reference_frame_index = updates.root_reference_frame_index;
734        self.spatial_nodes.clear();
735
736        for SpatialTreeUpdate { index, parent, descriptor } in updates.updates {
737            debug_assert_eq!(index, self.spatial_nodes.len());
738
739            if let Some(parent) = parent {
740                self.get_spatial_node_mut(parent).add_child(SpatialNodeIndex(index as u32));
741            }
742
743            self.spatial_nodes.push(SpatialNode {
744                viewport_transform: ScaleOffset::identity(),
745                content_transform: ScaleOffset::identity(),
746                snapping_transform: None,
747                coordinate_system_id: CoordinateSystemId(0),
748                transform_kind: TransformedRectKind::AxisAligned,
749                parent,
750                children: Vec::new(),
751                pipeline_id: descriptor.pipeline_id,
752                node_type: descriptor.node_type,
753                invertible: true,
754                is_async_zooming: false,
755                is_ancestor_or_self_zooming: false,
756            });
757        }
758
759        self.visit_nodes_mut(|_, node| {
760            match node.node_type {
761                SpatialNodeType::ScrollFrame(ref mut info) => {
762                    info.offsets = vec![SampledScrollOffset{
763                        offset: -info.external_scroll_offset,
764                        generation: info.offset_generation,
765                    }];
766                }
767                SpatialNodeType::StickyFrame(ref mut info) => {
768                    info.current_offset = LayoutVector2D::zero();
769                }
770                SpatialNodeType::ReferenceFrame(..) => {}
771            }
772        });
773    }
774
775    pub fn get_last_sampled_scroll_offsets(
776        &self,
777    ) -> FastHashMap<ExternalScrollId, Vec<SampledScrollOffset>> {
778        let mut result = FastHashMap::default();
779        self.visit_nodes(|_, node| {
780            if let SpatialNodeType::ScrollFrame(ref scrolling) = node.node_type {
781                result.insert(scrolling.external_id, scrolling.offsets.clone());
782            }
783        });
784        result
785    }
786
787    pub fn apply_last_sampled_scroll_offsets(
788        &mut self,
789        last_sampled_offsets: FastHashMap<ExternalScrollId, Vec<SampledScrollOffset>>,
790    ) {
791        self.visit_nodes_mut(|_, node| {
792            if let SpatialNodeType::ScrollFrame(ref mut scrolling) = node.node_type {
793                if let Some(offsets) = last_sampled_offsets.get(&scrolling.external_id) {
794                    scrolling.offsets = offsets.clone();
795                }
796            }
797        });
798    }
799
800    pub fn get_spatial_node(&self, index: SpatialNodeIndex) -> &SpatialNode {
801        &self.spatial_nodes[index.0 as usize]
802    }
803
804    pub fn get_spatial_node_mut(&mut self, index: SpatialNodeIndex) -> &mut SpatialNode {
805        &mut self.spatial_nodes[index.0 as usize]
806    }
807
808    /// Get total number of spatial nodes
809    pub fn spatial_node_count(&self) -> usize {
810        self.spatial_nodes.len()
811    }
812
813    pub fn find_spatial_node_by_anim_id(
814        &self,
815        id: PropertyBindingId,
816    ) -> Option<SpatialNodeIndex> {
817        let mut node_index = None;
818
819        self.visit_nodes(|index, node| {
820            if node.is_transform_bound_to_property(id) {
821                debug_assert!(node_index.is_none());        // Multiple nodes with same anim id
822                node_index = Some(index);
823            }
824        });
825
826        node_index
827    }
828
829    /// Calculate the relative transform from `child_index` to `parent_index`.
830    /// This method will panic if the nodes are not connected!
831    pub fn get_relative_transform(
832        &self,
833        child_index: SpatialNodeIndex,
834        parent_index: SpatialNodeIndex,
835    ) -> CoordinateSpaceMapping<LayoutPixel, LayoutPixel> {
836        self.get_relative_transform_with_face(child_index, parent_index, None)
837    }
838
839    /// Calculate the relative transform from `child_index` to `parent_index`.
840    /// This method will panic if the nodes are not connected!
841    /// Also, switch the visible face to `Back` if at any stage where the
842    /// combined transform is flattened, we see the back face.
843    pub fn get_relative_transform_with_face(
844        &self,
845        child_index: SpatialNodeIndex,
846        parent_index: SpatialNodeIndex,
847        mut visible_face: Option<&mut VisibleFace>,
848    ) -> CoordinateSpaceMapping<LayoutPixel, LayoutPixel> {
849        if child_index == parent_index {
850            return CoordinateSpaceMapping::Local;
851        }
852
853        let child = self.get_spatial_node(child_index);
854        let parent = self.get_spatial_node(parent_index);
855
856        // TODO(gw): We expect this never to fail, but it's possible that it might due to
857        //           either (a) a bug in WR / Gecko, or (b) some obscure real-world content
858        //           that we're unaware of. If we ever hit this, please open a bug with any
859        //           repro steps!
860        assert!(
861            child.coordinate_system_id.0 >= parent.coordinate_system_id.0,
862            "bug: this is an unexpected case - please open a bug and talk to #gfx team!",
863        );
864
865        if child.coordinate_system_id == parent.coordinate_system_id {
866            let scale_offset = child.content_transform.then(&parent.content_transform.inverse());
867
868            // Optimization - detect identity scale-offsets and treat them as
869            // local to skip following math
870            if scale_offset.is_identity() {
871                return CoordinateSpaceMapping::Local;
872            }
873
874            return CoordinateSpaceMapping::ScaleOffset(scale_offset);
875        }
876
877        let mut coordinate_system_id = child.coordinate_system_id;
878        let mut transform = child.content_transform.to_transform();
879
880        // we need to update the associated parameters of a transform in two cases:
881        // 1) when the flattening happens, so that we don't lose that original 3D aspects
882        // 2) when we reach the end of iteration, so that our result is up to date
883
884        while coordinate_system_id != parent.coordinate_system_id {
885            let coord_system = &self.coord_systems[coordinate_system_id.0 as usize];
886
887            if coord_system.should_flatten {
888                if let Some(ref mut face) = visible_face {
889                    if transform.is_backface_visible() {
890                        **face = VisibleFace::Back;
891                    }
892                }
893                transform.flatten_z_output();
894            }
895
896            coordinate_system_id = coord_system.parent.expect("invalid parent!");
897            transform = transform.then(&coord_system.transform);
898        }
899
900        transform = transform.then(
901            &parent.content_transform
902                .inverse()
903                .to_transform(),
904        );
905        if let Some(face) = visible_face {
906            if transform.is_backface_visible() {
907                *face = VisibleFace::Back;
908            }
909        }
910
911        CoordinateSpaceMapping::Transform(transform)
912    }
913
914    /// Returns true if both supplied spatial nodes are in the same coordinate system
915    /// (implies the relative transform produce axis-aligned rects).
916    pub fn is_matching_coord_system(
917        &self,
918        index0: SpatialNodeIndex,
919        index1: SpatialNodeIndex,
920    ) -> bool {
921        let node0 = self.get_spatial_node(index0);
922        let node1 = self.get_spatial_node(index1);
923
924        node0.coordinate_system_id == node1.coordinate_system_id
925    }
926
927    fn get_world_transform_impl(
928        &self,
929        index: SpatialNodeIndex,
930        scroll: TransformScroll,
931    ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
932        let child = self.get_spatial_node(index);
933
934        if child.coordinate_system_id.0 == 0 {
935            if index == self.root_reference_frame_index {
936                CoordinateSpaceMapping::Local
937            } else {
938              match scroll {
939                TransformScroll::Scrolled => CoordinateSpaceMapping::ScaleOffset(child.content_transform),
940                TransformScroll::Unscrolled => CoordinateSpaceMapping::ScaleOffset(child.viewport_transform),
941              }
942            }
943        } else {
944            let system = &self.coord_systems[child.coordinate_system_id.0 as usize];
945            let scale_offset = match scroll {
946                TransformScroll::Scrolled => &child.content_transform,
947                TransformScroll::Unscrolled => &child.viewport_transform,
948            };
949            let transform = scale_offset
950                .to_transform()
951                .then(&system.world_transform);
952
953            CoordinateSpaceMapping::Transform(transform)
954        }
955    }
956
957    /// Calculate the relative transform from `index` to the root.
958    pub fn get_world_transform(
959        &self,
960        index: SpatialNodeIndex,
961    ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
962        self.get_world_transform_impl(index, TransformScroll::Scrolled)
963    }
964
965    /// Calculate the relative transform from `index` to the root.
966    /// Unlike `get_world_transform`, this variant doesn't account for the local scroll offset.
967    pub fn get_world_viewport_transform(
968        &self,
969        index: SpatialNodeIndex,
970    ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
971        self.get_world_transform_impl(index, TransformScroll::Unscrolled)
972    }
973
974    /// The root reference frame, which is the true root of the SpatialTree.
975    pub fn root_reference_frame_index(&self) -> SpatialNodeIndex {
976        self.root_reference_frame_index
977    }
978
979    pub fn set_scroll_offsets(
980        &mut self,
981        id: ExternalScrollId,
982        offsets: Vec<SampledScrollOffset>,
983    ) -> bool {
984        let mut did_change = false;
985
986        self.visit_nodes_mut(|_, node| {
987            if node.matches_external_id(id) {
988                did_change |= node.set_scroll_offsets(offsets.clone());
989            }
990        });
991
992        did_change
993    }
994
995    pub fn update_tree(
996        &mut self,
997        scene_properties: &SceneProperties,
998    ) {
999        if self.root_reference_frame_index == SpatialNodeIndex::INVALID {
1000            return;
1001        }
1002
1003        profile_scope!("update_tree");
1004        self.coord_systems.clear();
1005        self.coord_systems.push(CoordinateSystem::root());
1006
1007        let root_node_index = self.root_reference_frame_index();
1008        assert!(self.update_state_stack.is_empty());
1009
1010        let state = TransformUpdateState {
1011            parent_reference_frame_transform: LayoutVector2D::zero().into(),
1012            parent_accumulated_scroll_offset: LayoutVector2D::zero(),
1013            nearest_scrolling_ancestor_offset: LayoutVector2D::zero(),
1014            nearest_scrolling_ancestor_viewport: LayoutRect::zero(),
1015            current_coordinate_system_id: CoordinateSystemId::root(),
1016            coordinate_system_relative_scale_offset: ScaleOffset::identity(),
1017            invertible: true,
1018            preserves_3d: false,
1019            is_ancestor_or_self_zooming: false,
1020            external_id: None,
1021            scroll_offset: LayoutVector2D::zero(),
1022        };
1023        self.update_state_stack.push(state);
1024
1025        self.update_node(
1026            root_node_index,
1027            scene_properties,
1028        );
1029
1030        self.update_state_stack.pop().unwrap();
1031    }
1032
1033    fn update_node(
1034        &mut self,
1035        node_index: SpatialNodeIndex,
1036        scene_properties: &SceneProperties,
1037    ) {
1038        let parent_index = self.get_spatial_node(node_index).parent;
1039        let parent_info = self.get_snapping_info(parent_index);
1040
1041        let node = &mut self.spatial_nodes[node_index.0 as usize];
1042
1043        node.snapping_transform = calculate_snapping_transform(
1044            parent_info,
1045            &node.node_type,
1046        );
1047
1048        node.update(
1049            &self.update_state_stack,
1050            &mut self.coord_systems,
1051            scene_properties,
1052        );
1053
1054        if !node.children.is_empty() {
1055            let mut child_state = self.update_state_stack.last().unwrap().clone();
1056            node.prepare_state_for_children(&mut child_state);
1057            self.update_state_stack.push(child_state);
1058
1059            let mut child_indices: SmallVec<[SpatialNodeIndex; 8]> = SmallVec::new();
1060            child_indices.extend_from_slice(&node.children);
1061
1062            for child_index in child_indices {
1063                self.update_node(
1064                    child_index,
1065                    scene_properties,
1066                );
1067            }
1068
1069            self.update_state_stack.pop().unwrap();
1070        }
1071    }
1072
1073    pub fn build_transform_palette(&self, memory: &FrameMemory) -> TransformPalette {
1074        profile_scope!("build_transform_palette");
1075        TransformPalette::new(self.spatial_nodes.len(), memory)
1076    }
1077
1078    fn print_node<T: PrintTreePrinter>(
1079        &self,
1080        index: SpatialNodeIndex,
1081        pt: &mut T,
1082    ) {
1083        let node = self.get_spatial_node(index);
1084        match node.node_type {
1085            SpatialNodeType::StickyFrame(ref sticky_frame_info) => {
1086                pt.new_level(format!("StickyFrame"));
1087                pt.add_item(format!("sticky info: {:?}", sticky_frame_info));
1088            }
1089            SpatialNodeType::ScrollFrame(ref scrolling_info) => {
1090                pt.new_level(format!("ScrollFrame"));
1091                pt.add_item(format!("viewport: {:?}", scrolling_info.viewport_rect));
1092                pt.add_item(format!("scrollable_size: {:?}", scrolling_info.scrollable_size));
1093                pt.add_item(format!("scroll offset: {:?}", scrolling_info.offset()));
1094                pt.add_item(format!("external_scroll_offset: {:?}", scrolling_info.external_scroll_offset));
1095                pt.add_item(format!("offset generation: {:?}", scrolling_info.offset_generation));
1096                if scrolling_info.has_scroll_linked_effect == HasScrollLinkedEffect::Yes {
1097                    pt.add_item("has scroll-linked effect".to_string());
1098                }
1099                pt.add_item(format!("kind: {:?}", scrolling_info.frame_kind));
1100            }
1101            SpatialNodeType::ReferenceFrame(ref info) => {
1102                pt.new_level(format!("ReferenceFrame"));
1103                pt.add_item(format!("kind: {:?}", info.kind));
1104                pt.add_item(format!("transform_style: {:?}", info.transform_style));
1105                pt.add_item(format!("source_transform: {:?}", info.source_transform));
1106                pt.add_item(format!("origin_in_parent_reference_frame: {:?}", info.origin_in_parent_reference_frame));
1107            }
1108        }
1109
1110        pt.add_item(format!("index: {:?}", index));
1111        pt.add_item(format!("content_transform: {:?}", node.content_transform));
1112        pt.add_item(format!("viewport_transform: {:?}", node.viewport_transform));
1113        pt.add_item(format!("snapping_transform: {:?}", node.snapping_transform));
1114        pt.add_item(format!("coordinate_system_id: {:?}", node.coordinate_system_id));
1115
1116        for child_index in &node.children {
1117            self.print_node(*child_index, pt);
1118        }
1119
1120        pt.end_level();
1121    }
1122
1123    /// Get the visible face of the transfrom from the specified node to its parent.
1124    pub fn get_local_visible_face(&self, node_index: SpatialNodeIndex) -> VisibleFace {
1125        let node = self.get_spatial_node(node_index);
1126        let mut face = VisibleFace::Front;
1127        if let Some(mut parent_index) = node.parent {
1128            // Check if the parent is perspective. In CSS, a stacking context may
1129            // have both perspective and a regular transformation. Gecko translates the
1130            // perspective into a different `nsDisplayPerspective` and `nsDisplayTransform` items.
1131            // On WebRender side, we end up with 2 different reference frames:
1132            // one has kind of "transform", and it's parented to another of "perspective":
1133            // https://searchfox.org/mozilla-central/rev/72c7cef167829b6f1e24cae216fa261934c455fc/layout/generic/nsIFrame.cpp#3716
1134            if let SpatialNodeType::ReferenceFrame(ReferenceFrameInfo { kind: ReferenceFrameKind::Transform {
1135                paired_with_perspective: true,
1136                ..
1137            }, .. }) = node.node_type {
1138                let parent = self.get_spatial_node(parent_index);
1139                match parent.node_type {
1140                    SpatialNodeType::ReferenceFrame(ReferenceFrameInfo {
1141                        kind: ReferenceFrameKind::Perspective { .. },
1142                        ..
1143                    }) => {
1144                        parent_index = parent.parent.unwrap();
1145                    }
1146                    _ => {
1147                        log::error!("Unexpected parent {:?} is not perspective", parent_index);
1148                    }
1149                }
1150            }
1151
1152            self.get_relative_transform_with_face(node_index, parent_index, Some(&mut face));
1153        }
1154        face
1155    }
1156
1157    #[allow(dead_code)]
1158    pub fn print_to_string(&self) -> String {
1159        let mut result = String::new();
1160
1161        if self.root_reference_frame_index != SpatialNodeIndex::INVALID {
1162            let mut buf = Vec::<u8>::new();
1163            {
1164                let mut pt = PrintTree::new_with_sink("spatial tree", &mut buf);
1165                self.print_with(&mut pt);
1166            }
1167            result = std::str::from_utf8(&buf).unwrap_or("(Tree printer emitted non-utf8)").to_string();
1168        }
1169
1170        result
1171    }
1172
1173    #[allow(dead_code)]
1174    pub fn print(&self) {
1175        let result = self.print_to_string();
1176        // If running in Gecko, set RUST_LOG=webrender::spatial_tree=debug
1177        // to get this logging to be emitted to stderr/logcat.
1178        debug!("{}", result);
1179    }
1180}
1181
1182impl PrintableTree for SpatialTree {
1183    fn print_with<T: PrintTreePrinter>(&self, pt: &mut T) {
1184        if self.root_reference_frame_index != SpatialNodeIndex::INVALID {
1185            self.print_node(self.root_reference_frame_index(), pt);
1186        }
1187    }
1188}
1189
1190/// Calculate the accumulated external scroll offset for a given spatial node.
1191pub fn get_external_scroll_offset<S: SpatialNodeContainer>(
1192    spatial_tree: &S,
1193    node_index: SpatialNodeIndex,
1194) -> LayoutVector2D {
1195    let mut offset = LayoutVector2D::zero();
1196    let mut current_node = Some(node_index);
1197
1198    while let Some(node_index) = current_node {
1199        let node_info = spatial_tree.get_node_info(node_index);
1200
1201        match node_info.node_type {
1202            SpatialNodeType::ScrollFrame(ref scrolling) => {
1203                offset += scrolling.external_scroll_offset;
1204            }
1205            SpatialNodeType::StickyFrame(ref sticky) => {
1206                // Remove the sticky offset that was applied in the
1207                // content process, so that primitive interning
1208                // sees stable values, and doesn't invalidate unnecessarily.
1209                offset -= sticky.previously_applied_offset;
1210            }
1211            SpatialNodeType::ReferenceFrame(..) => {
1212                // External scroll offsets are not propagated across
1213                // reference frames.
1214                break;
1215            }
1216        }
1217
1218        current_node = node_info.parent;
1219    }
1220
1221    offset
1222}
1223
1224fn calculate_snapping_transform(
1225    parent_scale_offset: Option<ScaleOffset>,
1226    node_type: &SpatialNodeType,
1227) -> Option<ScaleOffset> {
1228    // We need to incorporate the parent scale/offset with the child.
1229    // If the parent does not have a scale/offset, then we know we are
1230    // not 2d axis aligned and thus do not need to snap its children
1231    // either.
1232    let parent_scale_offset = match parent_scale_offset {
1233        Some(transform) => transform,
1234        None => return None,
1235    };
1236
1237    let scale_offset = match node_type {
1238        SpatialNodeType::ReferenceFrame(ref info) => {
1239            let origin_offset = info.origin_in_parent_reference_frame;
1240
1241            match info.source_transform {
1242                PropertyBinding::Value(ref value) => {
1243                    // We can only get a ScaleOffset if the transform is 2d axis
1244                    // aligned.
1245                    match ScaleOffset::from_transform(value) {
1246                        Some(scale_offset) => {
1247                            scale_offset.then(&ScaleOffset::from_offset(origin_offset.to_untyped()))
1248                        }
1249                        None => return None,
1250                    }
1251                }
1252
1253                // Assume animations start at the identity transform for snapping purposes.
1254                // We still want to incorporate the reference frame offset however.
1255                // TODO(aosmond): Is there a better known starting point?
1256                PropertyBinding::Binding(..) => {
1257                    ScaleOffset::from_offset(origin_offset.to_untyped())
1258                }
1259            }
1260        }
1261        _ => ScaleOffset::identity(),
1262    };
1263
1264    Some(scale_offset.then(&parent_scale_offset))
1265}
1266
1267#[cfg(test)]
1268fn add_reference_frame(
1269    cst: &mut SceneSpatialTree,
1270    parent: SpatialNodeIndex,
1271    transform: LayoutTransform,
1272    origin_in_parent_reference_frame: LayoutVector2D,
1273) -> SpatialNodeIndex {
1274    cst.add_reference_frame(
1275        parent,
1276        TransformStyle::Preserve3D,
1277        PropertyBinding::Value(transform),
1278        ReferenceFrameKind::Transform {
1279            is_2d_scale_translation: false,
1280            should_snap: false,
1281            paired_with_perspective: false,
1282        },
1283        origin_in_parent_reference_frame,
1284        PipelineId::dummy(),
1285        false,
1286    )
1287}
1288
1289#[cfg(test)]
1290fn test_pt(
1291    px: f32,
1292    py: f32,
1293    cst: &SpatialTree,
1294    child: SpatialNodeIndex,
1295    parent: SpatialNodeIndex,
1296    expected_x: f32,
1297    expected_y: f32,
1298) {
1299    use euclid::approxeq::ApproxEq;
1300    const EPSILON: f32 = 0.0001;
1301
1302    let p = LayoutPoint::new(px, py);
1303    let m = cst.get_relative_transform(child, parent).into_transform();
1304    let pt = m.transform_point2d(p).unwrap();
1305    assert!(pt.x.approx_eq_eps(&expected_x, &EPSILON) &&
1306            pt.y.approx_eq_eps(&expected_y, &EPSILON),
1307            "p: {:?} -> {:?}\nm={:?}",
1308            p, pt, m,
1309            );
1310}
1311
1312#[test]
1313fn test_cst_simple_translation() {
1314    // Basic translations only
1315
1316    let mut cst = SceneSpatialTree::new();
1317    let root_reference_frame_index = cst.root_reference_frame_index();
1318
1319    let root = add_reference_frame(
1320        &mut cst,
1321        root_reference_frame_index,
1322        LayoutTransform::identity(),
1323        LayoutVector2D::zero(),
1324    );
1325
1326    let child1 = add_reference_frame(
1327        &mut cst,
1328        root,
1329        LayoutTransform::translation(100.0, 0.0, 0.0),
1330        LayoutVector2D::zero(),
1331    );
1332
1333    let child2 = add_reference_frame(
1334        &mut cst,
1335        child1,
1336        LayoutTransform::translation(0.0, 50.0, 0.0),
1337        LayoutVector2D::zero(),
1338    );
1339
1340    let child3 = add_reference_frame(
1341        &mut cst,
1342        child2,
1343        LayoutTransform::translation(200.0, 200.0, 0.0),
1344        LayoutVector2D::zero(),
1345    );
1346
1347    let mut st = SpatialTree::new();
1348    st.apply_updates(cst.end_frame_and_get_pending_updates());
1349    st.update_tree(&SceneProperties::new());
1350
1351    test_pt(100.0, 100.0, &st, child1, root, 200.0, 100.0);
1352    test_pt(100.0, 100.0, &st, child2, root, 200.0, 150.0);
1353    test_pt(100.0, 100.0, &st, child2, child1, 100.0, 150.0);
1354    test_pt(100.0, 100.0, &st, child3, root, 400.0, 350.0);
1355}
1356
1357#[test]
1358fn test_cst_simple_scale() {
1359    // Basic scale only
1360
1361    let mut cst = SceneSpatialTree::new();
1362    let root_reference_frame_index = cst.root_reference_frame_index();
1363
1364    let root = add_reference_frame(
1365        &mut cst,
1366        root_reference_frame_index,
1367        LayoutTransform::identity(),
1368        LayoutVector2D::zero(),
1369    );
1370
1371    let child1 = add_reference_frame(
1372        &mut cst,
1373        root,
1374        LayoutTransform::scale(4.0, 1.0, 1.0),
1375        LayoutVector2D::zero(),
1376    );
1377
1378    let child2 = add_reference_frame(
1379        &mut cst,
1380        child1,
1381        LayoutTransform::scale(1.0, 2.0, 1.0),
1382        LayoutVector2D::zero(),
1383    );
1384
1385    let child3 = add_reference_frame(
1386        &mut cst,
1387        child2,
1388        LayoutTransform::scale(2.0, 2.0, 1.0),
1389        LayoutVector2D::zero(),
1390    );
1391
1392    let mut st = SpatialTree::new();
1393    st.apply_updates(cst.end_frame_and_get_pending_updates());
1394    st.update_tree(&SceneProperties::new());
1395
1396    test_pt(100.0, 100.0, &st, child1, root, 400.0, 100.0);
1397    test_pt(100.0, 100.0, &st, child2, root, 400.0, 200.0);
1398    test_pt(100.0, 100.0, &st, child3, root, 800.0, 400.0);
1399    test_pt(100.0, 100.0, &st, child2, child1, 100.0, 200.0);
1400    test_pt(100.0, 100.0, &st, child3, child1, 200.0, 400.0);
1401}
1402
1403#[test]
1404fn test_cst_scale_translation() {
1405    // Scale + translation
1406
1407    let mut cst = SceneSpatialTree::new();
1408    let root_reference_frame_index = cst.root_reference_frame_index();
1409
1410    let root = add_reference_frame(
1411        &mut cst,
1412        root_reference_frame_index,
1413        LayoutTransform::identity(),
1414        LayoutVector2D::zero(),
1415    );
1416
1417    let child1 = add_reference_frame(
1418        &mut cst,
1419        root,
1420        LayoutTransform::translation(100.0, 50.0, 0.0),
1421        LayoutVector2D::zero(),
1422    );
1423
1424    let child2 = add_reference_frame(
1425        &mut cst,
1426        child1,
1427        LayoutTransform::scale(2.0, 4.0, 1.0),
1428        LayoutVector2D::zero(),
1429    );
1430
1431    let child3 = add_reference_frame(
1432        &mut cst,
1433        child2,
1434        LayoutTransform::translation(200.0, -100.0, 0.0),
1435        LayoutVector2D::zero(),
1436    );
1437
1438    let child4 = add_reference_frame(
1439        &mut cst,
1440        child3,
1441        LayoutTransform::scale(3.0, 2.0, 1.0),
1442        LayoutVector2D::zero(),
1443    );
1444
1445    let mut st = SpatialTree::new();
1446    st.apply_updates(cst.end_frame_and_get_pending_updates());
1447    st.update_tree(&SceneProperties::new());
1448
1449    test_pt(100.0, 100.0, &st, child1, root, 200.0, 150.0);
1450    test_pt(100.0, 100.0, &st, child2, root, 300.0, 450.0);
1451    test_pt(100.0, 100.0, &st, child4, root, 1100.0, 450.0);
1452
1453    test_pt(0.0, 0.0, &st, child4, child1, 400.0, -400.0);
1454    test_pt(100.0, 100.0, &st, child4, child1, 1000.0, 400.0);
1455    test_pt(100.0, 100.0, &st, child2, child1, 200.0, 400.0);
1456
1457    test_pt(100.0, 100.0, &st, child3, child1, 600.0, 0.0);
1458}
1459
1460#[test]
1461fn test_cst_translation_rotate() {
1462    // Rotation + translation
1463    use euclid::Angle;
1464
1465    let mut cst = SceneSpatialTree::new();
1466    let root_reference_frame_index = cst.root_reference_frame_index();
1467
1468    let root = add_reference_frame(
1469        &mut cst,
1470        root_reference_frame_index,
1471        LayoutTransform::identity(),
1472        LayoutVector2D::zero(),
1473    );
1474
1475    let child1 = add_reference_frame(
1476        &mut cst,
1477        root,
1478        LayoutTransform::rotation(0.0, 0.0, 1.0, Angle::degrees(-90.0)),
1479        LayoutVector2D::zero(),
1480    );
1481
1482    let mut st = SpatialTree::new();
1483    st.apply_updates(cst.end_frame_and_get_pending_updates());
1484    st.update_tree(&SceneProperties::new());
1485
1486    test_pt(100.0, 0.0, &st, child1, root, 0.0, -100.0);
1487}
1488
1489#[test]
1490fn test_is_ancestor1() {
1491    let mut st = SceneSpatialTree::new();
1492    let root_reference_frame_index = st.root_reference_frame_index();
1493
1494    let root = add_reference_frame(
1495        &mut st,
1496        root_reference_frame_index,
1497        LayoutTransform::identity(),
1498        LayoutVector2D::zero(),
1499    );
1500
1501    let child1_0 = add_reference_frame(
1502        &mut st,
1503        root,
1504        LayoutTransform::identity(),
1505        LayoutVector2D::zero(),
1506    );
1507
1508    let child1_1 = add_reference_frame(
1509        &mut st,
1510        child1_0,
1511        LayoutTransform::identity(),
1512        LayoutVector2D::zero(),
1513    );
1514
1515    let child2 = add_reference_frame(
1516        &mut st,
1517        root,
1518        LayoutTransform::identity(),
1519        LayoutVector2D::zero(),
1520    );
1521
1522    assert!(!st.is_ancestor(root, root));
1523    assert!(!st.is_ancestor(child1_0, child1_0));
1524    assert!(!st.is_ancestor(child1_1, child1_1));
1525    assert!(!st.is_ancestor(child2, child2));
1526
1527    assert!(st.is_ancestor(root, child1_0));
1528    assert!(st.is_ancestor(root, child1_1));
1529    assert!(st.is_ancestor(child1_0, child1_1));
1530
1531    assert!(!st.is_ancestor(child1_0, root));
1532    assert!(!st.is_ancestor(child1_1, root));
1533    assert!(!st.is_ancestor(child1_1, child1_0));
1534
1535    assert!(st.is_ancestor(root, child2));
1536    assert!(!st.is_ancestor(child2, root));
1537
1538    assert!(!st.is_ancestor(child1_0, child2));
1539    assert!(!st.is_ancestor(child1_1, child2));
1540    assert!(!st.is_ancestor(child2, child1_0));
1541    assert!(!st.is_ancestor(child2, child1_1));
1542}
1543
1544/// Tests that we select the correct scroll root in the simple case.
1545#[test]
1546fn test_find_scroll_root_simple() {
1547    let mut st = SceneSpatialTree::new();
1548
1549    let root = st.add_reference_frame(
1550        st.root_reference_frame_index(),
1551        TransformStyle::Flat,
1552        PropertyBinding::Value(LayoutTransform::identity()),
1553        ReferenceFrameKind::Transform {
1554            is_2d_scale_translation: true,
1555            should_snap: true,
1556            paired_with_perspective: false,
1557        },
1558        LayoutVector2D::new(0.0, 0.0),
1559        PipelineId::dummy(),
1560        false,
1561    );
1562
1563    let scroll = st.add_scroll_frame(
1564        root,
1565        ExternalScrollId(1, PipelineId::dummy()),
1566        PipelineId::dummy(),
1567        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1568        &LayoutSize::new(800.0, 400.0),
1569        ScrollFrameKind::Explicit,
1570        LayoutVector2D::new(0.0, 0.0),
1571        APZScrollGeneration::default(),
1572        HasScrollLinkedEffect::No,
1573    );
1574
1575    assert_eq!(st.find_scroll_root(scroll, true), scroll);
1576}
1577
1578/// Tests that we select the root scroll frame rather than the subframe if both are scrollable.
1579#[test]
1580fn test_find_scroll_root_sub_scroll_frame() {
1581    let mut st = SceneSpatialTree::new();
1582
1583    let root = st.add_reference_frame(
1584        st.root_reference_frame_index(),
1585        TransformStyle::Flat,
1586        PropertyBinding::Value(LayoutTransform::identity()),
1587        ReferenceFrameKind::Transform {
1588            is_2d_scale_translation: true,
1589            should_snap: true,
1590            paired_with_perspective: false,
1591        },
1592        LayoutVector2D::new(0.0, 0.0),
1593        PipelineId::dummy(),
1594        false,
1595    );
1596
1597    let root_scroll = st.add_scroll_frame(
1598        root,
1599        ExternalScrollId(1, PipelineId::dummy()),
1600        PipelineId::dummy(),
1601        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1602        &LayoutSize::new(800.0, 400.0),
1603        ScrollFrameKind::Explicit,
1604        LayoutVector2D::new(0.0, 0.0),
1605        APZScrollGeneration::default(),
1606        HasScrollLinkedEffect::No,
1607    );
1608
1609    let sub_scroll = st.add_scroll_frame(
1610        root_scroll,
1611        ExternalScrollId(1, PipelineId::dummy()),
1612        PipelineId::dummy(),
1613        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1614        &LayoutSize::new(800.0, 400.0),
1615        ScrollFrameKind::Explicit,
1616        LayoutVector2D::new(0.0, 0.0),
1617        APZScrollGeneration::default(),
1618        HasScrollLinkedEffect::No,
1619    );
1620
1621    assert_eq!(st.find_scroll_root(sub_scroll, true), root_scroll);
1622}
1623
1624/// Tests that we select the sub scroll frame when the root scroll frame is not scrollable.
1625#[test]
1626fn test_find_scroll_root_not_scrollable() {
1627    let mut st = SceneSpatialTree::new();
1628
1629    let root = st.add_reference_frame(
1630        st.root_reference_frame_index(),
1631        TransformStyle::Flat,
1632        PropertyBinding::Value(LayoutTransform::identity()),
1633        ReferenceFrameKind::Transform {
1634            is_2d_scale_translation: true,
1635            should_snap: true,
1636            paired_with_perspective: false,
1637        },
1638        LayoutVector2D::new(0.0, 0.0),
1639        PipelineId::dummy(),
1640        false,
1641    );
1642
1643    let root_scroll = st.add_scroll_frame(
1644        root,
1645        ExternalScrollId(1, PipelineId::dummy()),
1646        PipelineId::dummy(),
1647        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1648        &LayoutSize::new(400.0, 400.0),
1649        ScrollFrameKind::Explicit,
1650        LayoutVector2D::new(0.0, 0.0),
1651        APZScrollGeneration::default(),
1652        HasScrollLinkedEffect::No,
1653    );
1654
1655    let sub_scroll = st.add_scroll_frame(
1656        root_scroll,
1657        ExternalScrollId(1, PipelineId::dummy()),
1658        PipelineId::dummy(),
1659        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1660        &LayoutSize::new(800.0, 400.0),
1661        ScrollFrameKind::Explicit,
1662        LayoutVector2D::new(0.0, 0.0),
1663        APZScrollGeneration::default(),
1664        HasScrollLinkedEffect::No,
1665    );
1666
1667    assert_eq!(st.find_scroll_root(sub_scroll, true), sub_scroll);
1668}
1669
1670/// Tests that we select the sub scroll frame when the root scroll frame is too small.
1671#[test]
1672fn test_find_scroll_root_too_small() {
1673    let mut st = SceneSpatialTree::new();
1674
1675    let root = st.add_reference_frame(
1676        st.root_reference_frame_index(),
1677        TransformStyle::Flat,
1678        PropertyBinding::Value(LayoutTransform::identity()),
1679        ReferenceFrameKind::Transform {
1680            is_2d_scale_translation: true,
1681            should_snap: true,
1682            paired_with_perspective: false,
1683        },
1684        LayoutVector2D::new(0.0, 0.0),
1685        PipelineId::dummy(),
1686        false,
1687    );
1688
1689    let root_scroll = st.add_scroll_frame(
1690        root,
1691        ExternalScrollId(1, PipelineId::dummy()),
1692        PipelineId::dummy(),
1693        &LayoutRect::from_size(LayoutSize::new(MIN_SCROLL_ROOT_SIZE, MIN_SCROLL_ROOT_SIZE)),
1694        &LayoutSize::new(1000.0, 1000.0),
1695        ScrollFrameKind::Explicit,
1696        LayoutVector2D::new(0.0, 0.0),
1697        APZScrollGeneration::default(),
1698        HasScrollLinkedEffect::No,
1699    );
1700
1701    let sub_scroll = st.add_scroll_frame(
1702        root_scroll,
1703        ExternalScrollId(1, PipelineId::dummy()),
1704        PipelineId::dummy(),
1705        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1706        &LayoutSize::new(800.0, 400.0),
1707        ScrollFrameKind::Explicit,
1708        LayoutVector2D::new(0.0, 0.0),
1709        APZScrollGeneration::default(),
1710        HasScrollLinkedEffect::No,
1711    );
1712
1713    assert_eq!(st.find_scroll_root(sub_scroll, true), sub_scroll);
1714}
1715
1716/// Tests that we select the root scroll node, even if it is not scrollable,
1717/// when encountering a non-axis-aligned transform.
1718#[test]
1719fn test_find_scroll_root_perspective() {
1720    let mut st = SceneSpatialTree::new();
1721
1722    let root = st.add_reference_frame(
1723        st.root_reference_frame_index(),
1724        TransformStyle::Flat,
1725        PropertyBinding::Value(LayoutTransform::identity()),
1726        ReferenceFrameKind::Transform {
1727            is_2d_scale_translation: true,
1728            should_snap: true,
1729            paired_with_perspective: false,
1730        },
1731        LayoutVector2D::new(0.0, 0.0),
1732        PipelineId::dummy(),
1733        false,
1734    );
1735
1736    let root_scroll = st.add_scroll_frame(
1737        root,
1738        ExternalScrollId(1, PipelineId::dummy()),
1739        PipelineId::dummy(),
1740        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1741        &LayoutSize::new(400.0, 400.0),
1742        ScrollFrameKind::Explicit,
1743        LayoutVector2D::new(0.0, 0.0),
1744        APZScrollGeneration::default(),
1745        HasScrollLinkedEffect::No,
1746    );
1747
1748    let perspective = st.add_reference_frame(
1749        root_scroll,
1750        TransformStyle::Flat,
1751        PropertyBinding::Value(LayoutTransform::identity()),
1752        ReferenceFrameKind::Perspective {
1753            scrolling_relative_to: None,
1754        },
1755        LayoutVector2D::new(0.0, 0.0),
1756        PipelineId::dummy(),
1757        false,
1758    );
1759
1760    let sub_scroll = st.add_scroll_frame(
1761        perspective,
1762        ExternalScrollId(1, PipelineId::dummy()),
1763        PipelineId::dummy(),
1764        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1765        &LayoutSize::new(800.0, 400.0),
1766        ScrollFrameKind::Explicit,
1767        LayoutVector2D::new(0.0, 0.0),
1768        APZScrollGeneration::default(),
1769        HasScrollLinkedEffect::No,
1770    );
1771
1772    assert_eq!(st.find_scroll_root(sub_scroll, true), root_scroll);
1773}
1774
1775/// Tests that encountering a 2D scale or translation transform does not prevent
1776/// us from selecting the sub scroll frame if the root scroll frame is unscrollable.
1777#[test]
1778fn test_find_scroll_root_2d_scale() {
1779    let mut st = SceneSpatialTree::new();
1780
1781    let root = st.add_reference_frame(
1782        st.root_reference_frame_index(),
1783        TransformStyle::Flat,
1784        PropertyBinding::Value(LayoutTransform::identity()),
1785        ReferenceFrameKind::Transform {
1786            is_2d_scale_translation: true,
1787            should_snap: true,
1788            paired_with_perspective: false,
1789        },
1790        LayoutVector2D::new(0.0, 0.0),
1791        PipelineId::dummy(),
1792        false,
1793    );
1794
1795    let root_scroll = st.add_scroll_frame(
1796        root,
1797        ExternalScrollId(1, PipelineId::dummy()),
1798        PipelineId::dummy(),
1799        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1800        &LayoutSize::new(400.0, 400.0),
1801        ScrollFrameKind::Explicit,
1802        LayoutVector2D::new(0.0, 0.0),
1803        APZScrollGeneration::default(),
1804        HasScrollLinkedEffect::No,
1805    );
1806
1807    let scale = st.add_reference_frame(
1808        root_scroll,
1809        TransformStyle::Flat,
1810        PropertyBinding::Value(LayoutTransform::identity()),
1811        ReferenceFrameKind::Transform {
1812            is_2d_scale_translation: true,
1813            should_snap: false,
1814            paired_with_perspective: false,
1815        },
1816        LayoutVector2D::new(0.0, 0.0),
1817        PipelineId::dummy(),
1818        false,
1819    );
1820
1821    let sub_scroll = st.add_scroll_frame(
1822        scale,
1823        ExternalScrollId(1, PipelineId::dummy()),
1824        PipelineId::dummy(),
1825        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1826        &LayoutSize::new(800.0, 400.0),
1827        ScrollFrameKind::Explicit,
1828        LayoutVector2D::new(0.0, 0.0),
1829        APZScrollGeneration::default(),
1830        HasScrollLinkedEffect::No,
1831    );
1832
1833    assert_eq!(st.find_scroll_root(sub_scroll, true), sub_scroll);
1834}
1835
1836/// Tests that a sticky spatial node is chosen as the scroll root rather than
1837/// its parent scroll frame
1838#[test]
1839fn test_find_scroll_root_sticky() {
1840    let mut st = SceneSpatialTree::new();
1841
1842    let root = st.add_reference_frame(
1843        st.root_reference_frame_index(),
1844        TransformStyle::Flat,
1845        PropertyBinding::Value(LayoutTransform::identity()),
1846        ReferenceFrameKind::Transform {
1847            is_2d_scale_translation: true,
1848            should_snap: true,
1849            paired_with_perspective: false,
1850        },
1851        LayoutVector2D::new(0.0, 0.0),
1852        PipelineId::dummy(),
1853        false,
1854    );
1855
1856    let scroll = st.add_scroll_frame(
1857        root,
1858        ExternalScrollId(1, PipelineId::dummy()),
1859        PipelineId::dummy(),
1860        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1861        &LayoutSize::new(400.0, 800.0),
1862        ScrollFrameKind::Explicit,
1863        LayoutVector2D::new(0.0, 0.0),
1864        APZScrollGeneration::default(),
1865        HasScrollLinkedEffect::No,
1866    );
1867
1868    let sticky = st.add_sticky_frame(
1869        scroll,
1870        StickyFrameInfo {
1871            frame_rect: LayoutRect::from_size(LayoutSize::new(400.0, 100.0)),
1872            margins: euclid::SideOffsets2D::new(Some(0.0), None, None, None),
1873            vertical_offset_bounds: api::StickyOffsetBounds::new(0.0, 0.0),
1874            horizontal_offset_bounds: api::StickyOffsetBounds::new(0.0, 0.0),
1875            previously_applied_offset: LayoutVector2D::zero(),
1876            current_offset: LayoutVector2D::zero(),
1877            transform: None
1878        },
1879        PipelineId::dummy(),
1880    );
1881
1882    assert_eq!(st.find_scroll_root(sticky, true), sticky);
1883    assert_eq!(st.find_scroll_root(sticky, false), scroll);
1884}
1885
1886#[test]
1887fn test_world_transforms() {
1888  // Create a spatial tree with a scroll frame node with scroll offset (0, 200).
1889  let mut cst = SceneSpatialTree::new();
1890  let scroll = cst.add_scroll_frame(
1891      cst.root_reference_frame_index(),
1892      ExternalScrollId(1, PipelineId::dummy()),
1893      PipelineId::dummy(),
1894      &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1895      &LayoutSize::new(400.0, 800.0),
1896      ScrollFrameKind::Explicit,
1897      LayoutVector2D::new(0.0, 200.0),
1898      APZScrollGeneration::default(),
1899      HasScrollLinkedEffect::No);
1900
1901  let mut st = SpatialTree::new();
1902  st.apply_updates(cst.end_frame_and_get_pending_updates());
1903  st.update_tree(&SceneProperties::new());
1904
1905  // The node's world transform should reflect the scroll offset,
1906  // e.g. here it should be (0, -200) to reflect that the content has been
1907  // scrolled up by 200px.
1908  assert_eq!(
1909      st.get_world_transform(scroll).into_transform(),
1910      LayoutToWorldTransform::translation(0.0, -200.0, 0.0));
1911
1912  // The node's world viewport transform only reflects enclosing scrolling
1913  // or transforms. Here we don't have any, so it should be the identity.
1914  assert_eq!(
1915      st.get_world_viewport_transform(scroll).into_transform(),
1916      LayoutToWorldTransform::identity());
1917}
1918
1919/// Tests that a spatial node that is async zooming and all of its descendants
1920/// are correctly marked as having themselves an ancestor that is zooming.
1921#[test]
1922fn test_is_ancestor_or_self_zooming() {
1923    let mut cst = SceneSpatialTree::new();
1924    let root_reference_frame_index = cst.root_reference_frame_index();
1925
1926    let root = add_reference_frame(
1927        &mut cst,
1928        root_reference_frame_index,
1929        LayoutTransform::identity(),
1930        LayoutVector2D::zero(),
1931    );
1932    let child1 = add_reference_frame(
1933        &mut cst,
1934        root,
1935        LayoutTransform::identity(),
1936        LayoutVector2D::zero(),
1937    );
1938    let child2 = add_reference_frame(
1939        &mut cst,
1940        child1,
1941        LayoutTransform::identity(),
1942        LayoutVector2D::zero(),
1943    );
1944
1945    let mut st = SpatialTree::new();
1946    st.apply_updates(cst.end_frame_and_get_pending_updates());
1947
1948    // Mark the root node as async zooming
1949    st.get_spatial_node_mut(root).is_async_zooming = true;
1950    st.update_tree(&SceneProperties::new());
1951
1952    // Ensure that the root node and all descendants are marked as having
1953    // themselves or an ancestor zooming
1954    assert!(st.get_spatial_node(root).is_ancestor_or_self_zooming);
1955    assert!(st.get_spatial_node(child1).is_ancestor_or_self_zooming);
1956    assert!(st.get_spatial_node(child2).is_ancestor_or_self_zooming);
1957}