azul_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};
6use api::{PipelineId, ScrollClamping, ScrollNodeState, ScrollSensitivity};
7use api::units::*;
8use euclid::Transform3D;
9use crate::gpu_types::TransformPalette;
10use crate::internal_types::{FastHashMap, FastHashSet};
11use crate::print_tree::{PrintableTree, PrintTree, PrintTreePrinter};
12use crate::scene::SceneProperties;
13use crate::spatial_node::{ScrollFrameInfo, SpatialNode, SpatialNodeType, StickyFrameInfo, ScrollFrameKind};
14use std::{ops, u32};
15use crate::util::{FastTransform, LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset, scale_factors};
16
17pub type ScrollStates = FastHashMap<ExternalScrollId, ScrollFrameInfo>;
18
19/// An id that identifies coordinate systems in the SpatialTree. Each
20/// coordinate system has an id and those ids will be shared when the coordinates
21/// system are the same or are in the same axis-aligned space. This allows
22/// for optimizing mask generation.
23#[derive(Debug, Copy, Clone, PartialEq)]
24#[cfg_attr(feature = "capture", derive(Serialize))]
25#[cfg_attr(feature = "replay", derive(Deserialize))]
26pub struct CoordinateSystemId(pub u32);
27
28#[derive(Debug, Copy, Clone, PartialEq)]
29pub struct StaticCoordinateSystemId(pub u32);
30
31impl StaticCoordinateSystemId {
32    pub const ROOT: StaticCoordinateSystemId = StaticCoordinateSystemId(0);
33}
34
35/// A node in the hierarchy of coordinate system
36/// transforms.
37#[derive(Debug)]
38pub struct CoordinateSystem {
39    pub transform: LayoutTransform,
40    pub world_transform: LayoutToWorldTransform,
41    pub should_flatten: bool,
42    pub parent: Option<CoordinateSystemId>,
43}
44
45impl CoordinateSystem {
46    fn root() -> Self {
47        CoordinateSystem {
48            transform: LayoutTransform::identity(),
49            world_transform: LayoutToWorldTransform::identity(),
50            should_flatten: false,
51            parent: None,
52        }
53    }
54}
55
56#[derive(Debug, Copy, Clone, Eq, Hash, MallocSizeOf, PartialEq, PartialOrd, Ord)]
57#[cfg_attr(feature = "capture", derive(Serialize))]
58#[cfg_attr(feature = "replay", derive(Deserialize))]
59pub struct SpatialNodeIndex(pub u32);
60
61impl SpatialNodeIndex {
62    pub const INVALID: SpatialNodeIndex = SpatialNodeIndex(u32::MAX);
63}
64
65//Note: these have to match ROOT_REFERENCE_FRAME_SPATIAL_ID and ROOT_SCROLL_NODE_SPATIAL_ID
66pub const ROOT_SPATIAL_NODE_INDEX: SpatialNodeIndex = SpatialNodeIndex(0);
67const TOPMOST_SCROLL_NODE_INDEX: SpatialNodeIndex = SpatialNodeIndex(1);
68
69// In some cases, the conversion from CSS pixels to device pixels can result in small
70// rounding errors when calculating the scrollable distance of a scroll frame. Apply
71// a small epsilon so that we don't detect these frames as "real" scroll frames.
72const MIN_SCROLLABLE_AMOUNT: f32 = 0.01;
73
74// The minimum size for a scroll frame for it to be considered for a scroll root.
75const MIN_SCROLL_ROOT_SIZE: f32 = 128.0;
76
77impl SpatialNodeIndex {
78    pub fn new(index: usize) -> Self {
79        debug_assert!(index < ::std::u32::MAX as usize);
80        SpatialNodeIndex(index as u32)
81    }
82}
83
84impl CoordinateSystemId {
85    pub fn root() -> Self {
86        CoordinateSystemId(0)
87    }
88}
89
90#[derive(Debug, Copy, Clone, PartialEq)]
91pub enum VisibleFace {
92    Front,
93    Back,
94}
95
96impl Default for VisibleFace {
97    fn default() -> Self {
98        VisibleFace::Front
99    }
100}
101
102impl ops::Not for VisibleFace {
103    type Output = Self;
104    fn not(self) -> Self {
105        match self {
106            VisibleFace::Front => VisibleFace::Back,
107            VisibleFace::Back => VisibleFace::Front,
108        }
109    }
110}
111
112pub struct SpatialTree {
113    /// Nodes which determine the positions (offsets and transforms) for primitives
114    /// and clips.
115    pub spatial_nodes: Vec<SpatialNode>,
116
117    /// A list of transforms that establish new coordinate systems.
118    /// Spatial nodes only establish a new coordinate system when
119    /// they have a transform that is not a simple 2d translation.
120    coord_systems: Vec<CoordinateSystem>,
121
122    pub pending_scroll_offsets: FastHashMap<ExternalScrollId, (LayoutPoint, ScrollClamping)>,
123
124    /// A set of pipelines which should be discarded the next time this
125    /// tree is drained.
126    pub pipelines_to_discard: FastHashSet<PipelineId>,
127
128    /// Temporary stack of nodes to update when traversing the tree.
129    nodes_to_update: Vec<(SpatialNodeIndex, TransformUpdateState)>,
130
131    /// Next id to assign when creating a new static coordinate system
132    next_static_coord_system_id: u32,
133}
134
135#[derive(Clone)]
136pub struct TransformUpdateState {
137    pub parent_reference_frame_transform: LayoutToWorldFastTransform,
138    pub parent_accumulated_scroll_offset: LayoutVector2D,
139    pub nearest_scrolling_ancestor_offset: LayoutVector2D,
140    pub nearest_scrolling_ancestor_viewport: LayoutRect,
141
142    /// An id for keeping track of the axis-aligned space of this node. This is used in
143    /// order to to track what kinds of clip optimizations can be done for a particular
144    /// display list item, since optimizations can usually only be done among
145    /// coordinate systems which are relatively axis aligned.
146    pub current_coordinate_system_id: CoordinateSystemId,
147
148    /// Scale and offset from the coordinate system that started this compatible coordinate system.
149    pub coordinate_system_relative_scale_offset: ScaleOffset,
150
151    /// True if this node is transformed by an invertible transform.  If not, display items
152    /// transformed by this node will not be displayed and display items not transformed by this
153    /// node will not be clipped by clips that are transformed by this node.
154    pub invertible: bool,
155
156    /// True if this node is a part of Preserve3D hierarchy.
157    pub preserves_3d: bool,
158}
159
160
161/// Transformation between two nodes in the spatial tree that can sometimes be
162/// encoded more efficiently than with a full matrix.
163#[derive(Debug, Clone)]
164pub enum CoordinateSpaceMapping<Src, Dst> {
165    Local,
166    ScaleOffset(ScaleOffset),
167    Transform(Transform3D<f32, Src, Dst>),
168}
169
170impl<Src, Dst> CoordinateSpaceMapping<Src, Dst> {
171    pub fn into_transform(self) -> Transform3D<f32, Src, Dst> {
172        match self {
173            CoordinateSpaceMapping::Local => Transform3D::identity(),
174            CoordinateSpaceMapping::ScaleOffset(scale_offset) => scale_offset.to_transform(),
175            CoordinateSpaceMapping::Transform(transform) => transform,
176        }
177    }
178
179    pub fn into_fast_transform(self) -> FastTransform<Src, Dst> {
180        match self {
181            CoordinateSpaceMapping::Local => FastTransform::identity(),
182            CoordinateSpaceMapping::ScaleOffset(scale_offset) => FastTransform::with_scale_offset(scale_offset),
183            CoordinateSpaceMapping::Transform(transform) => FastTransform::with_transform(transform),
184        }
185    }
186
187    pub fn is_perspective(&self) -> bool {
188        match *self {
189            CoordinateSpaceMapping::Local |
190            CoordinateSpaceMapping::ScaleOffset(_) => false,
191            CoordinateSpaceMapping::Transform(ref transform) => transform.has_perspective_component(),
192        }
193    }
194
195    pub fn is_2d_axis_aligned(&self) -> bool {
196        match *self {
197            CoordinateSpaceMapping::Local |
198            CoordinateSpaceMapping::ScaleOffset(_) => true,
199            CoordinateSpaceMapping::Transform(ref transform) => transform.preserves_2d_axis_alignment(),
200        }
201    }
202
203    pub fn scale_factors(&self) -> (f32, f32) {
204        match *self {
205            CoordinateSpaceMapping::Local => (1.0, 1.0),
206            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => (scale_offset.scale.x.abs(), scale_offset.scale.y.abs()),
207            CoordinateSpaceMapping::Transform(ref transform) => scale_factors(transform),
208        }
209    }
210
211    pub fn inverse(&self) -> Option<CoordinateSpaceMapping<Dst, Src>> {
212        match *self {
213            CoordinateSpaceMapping::Local => Some(CoordinateSpaceMapping::Local),
214            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
215                Some(CoordinateSpaceMapping::ScaleOffset(scale_offset.inverse()))
216            }
217            CoordinateSpaceMapping::Transform(ref transform) => {
218                transform.inverse().map(CoordinateSpaceMapping::Transform)
219            }
220        }
221    }
222}
223
224enum TransformScroll {
225    Scrolled,
226    Unscrolled,
227}
228
229impl SpatialTree {
230    pub fn new() -> Self {
231        SpatialTree {
232            spatial_nodes: Vec::new(),
233            coord_systems: Vec::new(),
234            pending_scroll_offsets: FastHashMap::default(),
235            pipelines_to_discard: FastHashSet::default(),
236            nodes_to_update: Vec::new(),
237            next_static_coord_system_id: 0,
238        }
239    }
240
241    /// Calculate the accumulated external scroll offset for
242    /// a given spatial node.
243    pub fn external_scroll_offset(&self, node_index: SpatialNodeIndex) -> LayoutVector2D {
244        let mut offset = LayoutVector2D::zero();
245        let mut current_node = Some(node_index);
246
247        while let Some(node_index) = current_node {
248            let node = &self.spatial_nodes[node_index.0 as usize];
249
250            match node.node_type {
251                SpatialNodeType::ScrollFrame(ref scrolling) => {
252                    offset += scrolling.external_scroll_offset;
253                }
254                SpatialNodeType::StickyFrame(..) => {
255                    // Doesn't provide any external scroll offset
256                }
257                SpatialNodeType::ReferenceFrame(..) => {
258                    // External scroll offsets are not propagated across
259                    // reference frames.
260                    break;
261                }
262            }
263
264            current_node = node.parent;
265        }
266
267        offset
268    }
269
270    /// Calculate the relative transform from `child_index` to `parent_index`.
271    /// This method will panic if the nodes are not connected!
272    pub fn get_relative_transform(
273        &self,
274        child_index: SpatialNodeIndex,
275        parent_index: SpatialNodeIndex,
276    ) -> CoordinateSpaceMapping<LayoutPixel, LayoutPixel> {
277        self.get_relative_transform_with_face(child_index, parent_index, None)
278    }
279
280    /// Calculate the relative transform from `child_index` to `parent_index`.
281    /// This method will panic if the nodes are not connected!
282    /// Also, switch the visible face to `Back` if at any stage where the
283    /// combined transform is flattened, we see the back face.
284    pub fn get_relative_transform_with_face(
285        &self,
286        child_index: SpatialNodeIndex,
287        parent_index: SpatialNodeIndex,
288        mut visible_face: Option<&mut VisibleFace>,
289    ) -> CoordinateSpaceMapping<LayoutPixel, LayoutPixel> {
290        if child_index == parent_index {
291            return CoordinateSpaceMapping::Local;
292        }
293
294        let child = &self.spatial_nodes[child_index.0 as usize];
295        let parent = &self.spatial_nodes[parent_index.0 as usize];
296
297        if child.coordinate_system_id == parent.coordinate_system_id {
298            let scale_offset = parent.content_transform
299                .inverse()
300                .accumulate(&child.content_transform);
301            return CoordinateSpaceMapping::ScaleOffset(scale_offset);
302        }
303
304        if child_index.0 < parent_index.0 {
305            warn!("Unexpected transform queried from {:?} to {:?}, please call the graphics team!", child_index, parent_index);
306            let child_cs = &self.coord_systems[child.coordinate_system_id.0 as usize];
307            let child_transform = child.content_transform
308                .to_transform::<LayoutPixel, LayoutPixel>()
309                .then(&child_cs.world_transform);
310            let parent_cs = &self.coord_systems[parent.coordinate_system_id.0 as usize];
311            let parent_transform = parent.content_transform
312                .to_transform()
313                .then(&parent_cs.world_transform);
314
315            let result = parent_transform
316                .inverse()
317                .unwrap_or_default()
318                .then(&child_transform)
319                .with_source::<LayoutPixel>()
320                .with_destination::<LayoutPixel>();
321
322            if let Some(face) = visible_face {
323                if result.is_backface_visible() {
324                    *face = VisibleFace::Back;
325                }
326            }
327            return CoordinateSpaceMapping::Transform(result);
328        }
329
330        let mut coordinate_system_id = child.coordinate_system_id;
331        let mut transform = child.content_transform.to_transform();
332
333        // we need to update the associated parameters of a transform in two cases:
334        // 1) when the flattening happens, so that we don't lose that original 3D aspects
335        // 2) when we reach the end of iteration, so that our result is up to date
336
337        while coordinate_system_id != parent.coordinate_system_id {
338            let coord_system = &self.coord_systems[coordinate_system_id.0 as usize];
339
340            if coord_system.should_flatten {
341                if let Some(ref mut face) = visible_face {
342                    if transform.is_backface_visible() {
343                        **face = VisibleFace::Back;
344                    }
345                }
346                transform.flatten_z_output();
347            }
348
349            coordinate_system_id = coord_system.parent.expect("invalid parent!");
350            transform = transform.then(&coord_system.transform);
351        }
352
353        transform = transform.then(
354            &parent.content_transform
355                .inverse()
356                .to_transform(),
357        );
358        if let Some(face) = visible_face {
359            if transform.is_backface_visible() {
360                *face = VisibleFace::Back;
361            }
362        }
363
364        CoordinateSpaceMapping::Transform(transform)
365    }
366
367    pub fn is_relative_transform_complex(
368        &self,
369        child_index: SpatialNodeIndex,
370        parent_index: SpatialNodeIndex,
371    ) -> bool {
372        if child_index == parent_index {
373            return false;
374        }
375
376        let child = &self.spatial_nodes[child_index.0 as usize];
377        let parent = &self.spatial_nodes[parent_index.0 as usize];
378
379        child.coordinate_system_id != parent.coordinate_system_id
380    }
381
382    fn get_world_transform_impl(
383        &self,
384        index: SpatialNodeIndex,
385        scroll: TransformScroll,
386    ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
387        let child = &self.spatial_nodes[index.0 as usize];
388
389        if child.coordinate_system_id.0 == 0 {
390            if index == ROOT_SPATIAL_NODE_INDEX {
391                CoordinateSpaceMapping::Local
392            } else {
393                CoordinateSpaceMapping::ScaleOffset(child.content_transform)
394            }
395        } else {
396            let system = &self.coord_systems[child.coordinate_system_id.0 as usize];
397            let scale_offset = match scroll {
398                TransformScroll::Scrolled => &child.content_transform,
399                TransformScroll::Unscrolled => &child.viewport_transform,
400            };
401            let transform = scale_offset
402                .to_transform()
403                .then(&system.world_transform);
404
405            CoordinateSpaceMapping::Transform(transform)
406        }
407    }
408
409    /// Calculate the relative transform from `index` to the root.
410    pub fn get_world_transform(
411        &self,
412        index: SpatialNodeIndex,
413    ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
414        self.get_world_transform_impl(index, TransformScroll::Scrolled)
415    }
416
417    /// Calculate the relative transform from `index` to the root.
418    /// Unlike `get_world_transform`, this variant doesn't account for the local scroll offset.
419    pub fn get_world_viewport_transform(
420        &self,
421        index: SpatialNodeIndex,
422    ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
423        self.get_world_transform_impl(index, TransformScroll::Unscrolled)
424    }
425
426    /// The root reference frame, which is the true root of the SpatialTree. Initially
427    /// this ID is not valid, which is indicated by ```spatial_nodes``` being empty.
428    pub fn root_reference_frame_index(&self) -> SpatialNodeIndex {
429        // TODO(mrobinson): We should eventually make this impossible to misuse.
430        debug_assert!(!self.spatial_nodes.is_empty());
431        ROOT_SPATIAL_NODE_INDEX
432    }
433
434    /// The root scroll node which is the first child of the root reference frame.
435    /// Initially this ID is not valid, which is indicated by ```spatial_nodes``` being empty.
436    pub fn topmost_scroll_node_index(&self) -> SpatialNodeIndex {
437        // TODO(mrobinson): We should eventually make this impossible to misuse.
438        debug_assert!(self.spatial_nodes.len() >= 1);
439        TOPMOST_SCROLL_NODE_INDEX
440    }
441
442    pub fn get_scroll_node_state(&self) -> Vec<ScrollNodeState> {
443        let mut result = vec![];
444        for node in &self.spatial_nodes {
445            if let SpatialNodeType::ScrollFrame(info) = node.node_type {
446                result.push(ScrollNodeState {
447                    id: info.external_id,
448                    scroll_offset: info.offset - info.external_scroll_offset,
449                })
450            }
451        }
452        result
453    }
454
455    pub fn drain(&mut self) -> ScrollStates {
456        let mut scroll_states = FastHashMap::default();
457        for old_node in &mut self.spatial_nodes.drain(..) {
458            if self.pipelines_to_discard.contains(&old_node.pipeline_id) {
459                continue;
460            }
461
462            match old_node.node_type {
463                SpatialNodeType::ScrollFrame(info) => {
464                    scroll_states.insert(info.external_id, info);
465                }
466                _ => {}
467            }
468        }
469
470        self.coord_systems.clear();
471        self.pipelines_to_discard.clear();
472        scroll_states
473    }
474
475    pub fn scroll_node(
476        &mut self,
477        origin: LayoutPoint,
478        id: ExternalScrollId,
479        clamp: ScrollClamping
480    ) -> bool {
481        for node in &mut self.spatial_nodes {
482            if node.matches_external_id(id) {
483                return node.set_scroll_origin(&origin, clamp);
484            }
485        }
486
487        self.pending_scroll_offsets.insert(id, (origin, clamp));
488        false
489    }
490
491    pub fn update_tree(
492        &mut self,
493        scene_properties: &SceneProperties,
494    ) {
495        if self.spatial_nodes.is_empty() {
496            return;
497        }
498
499        profile_scope!("update_tree");
500        self.coord_systems.clear();
501        self.coord_systems.push(CoordinateSystem::root());
502
503        let root_node_index = self.root_reference_frame_index();
504        let state = TransformUpdateState {
505            parent_reference_frame_transform: LayoutVector2D::zero().into(),
506            parent_accumulated_scroll_offset: LayoutVector2D::zero(),
507            nearest_scrolling_ancestor_offset: LayoutVector2D::zero(),
508            nearest_scrolling_ancestor_viewport: LayoutRect::zero(),
509            current_coordinate_system_id: CoordinateSystemId::root(),
510            coordinate_system_relative_scale_offset: ScaleOffset::identity(),
511            invertible: true,
512            preserves_3d: false,
513        };
514        debug_assert!(self.nodes_to_update.is_empty());
515        self.nodes_to_update.push((root_node_index, state));
516
517        while let Some((node_index, mut state)) = self.nodes_to_update.pop() {
518            let (previous, following) = self.spatial_nodes.split_at_mut(node_index.0 as usize);
519            let node = match following.get_mut(0) {
520                Some(node) => node,
521                None => continue,
522            };
523
524            node.update(&mut state, &mut self.coord_systems, scene_properties, &*previous);
525
526            if !node.children.is_empty() {
527                node.prepare_state_for_children(&mut state);
528                self.nodes_to_update.extend(node.children
529                    .iter()
530                    .rev()
531                    .map(|child_index| (*child_index, state.clone()))
532                );
533            }
534        }
535    }
536
537    pub fn build_transform_palette(&self) -> TransformPalette {
538        profile_scope!("build_transform_palette");
539        let mut palette = TransformPalette::new(self.spatial_nodes.len());
540        //Note: getting the world transform of a node is O(1) operation
541        for i in 0 .. self.spatial_nodes.len() {
542            let index = SpatialNodeIndex(i as u32);
543            let world_transform = self.get_world_transform(index).into_transform();
544            palette.set_world_transform(index, world_transform);
545        }
546        palette
547    }
548
549    pub fn finalize_and_apply_pending_scroll_offsets(&mut self, old_states: ScrollStates) {
550        for node in &mut self.spatial_nodes {
551            let external_id = match node.node_type {
552                SpatialNodeType::ScrollFrame(ScrollFrameInfo { external_id, ..}) => external_id,
553                _ => continue,
554            };
555
556            if let Some(scrolling_state) = old_states.get(&external_id) {
557                node.apply_old_scrolling_state(scrolling_state);
558            }
559
560            if let Some((offset, clamping)) = self.pending_scroll_offsets.remove(&external_id) {
561                node.set_scroll_origin(&offset, clamping);
562            }
563        }
564    }
565
566    /// Get the static coordinate system for a given spatial node index
567    pub fn get_static_coordinate_system_id(&self, node_index: SpatialNodeIndex) -> StaticCoordinateSystemId {
568        self.spatial_nodes[node_index.0 as usize].static_coordinate_system_id
569    }
570
571    pub fn add_scroll_frame(
572        &mut self,
573        parent_index: SpatialNodeIndex,
574        external_id: ExternalScrollId,
575        pipeline_id: PipelineId,
576        frame_rect: &LayoutRect,
577        content_size: &LayoutSize,
578        scroll_sensitivity: ScrollSensitivity,
579        frame_kind: ScrollFrameKind,
580        external_scroll_offset: LayoutVector2D,
581    ) -> SpatialNodeIndex {
582        // Scroll frames are only 2d translations - they can't introduce a new static coord system
583        let static_coordinate_system_id = self.get_static_coordinate_system_id(parent_index);
584
585        let node = SpatialNode::new_scroll_frame(
586            pipeline_id,
587            parent_index,
588            external_id,
589            frame_rect,
590            content_size,
591            scroll_sensitivity,
592            frame_kind,
593            external_scroll_offset,
594            static_coordinate_system_id,
595        );
596        self.add_spatial_node(node)
597    }
598
599    pub fn add_reference_frame(
600        &mut self,
601        parent_index: Option<SpatialNodeIndex>,
602        transform_style: TransformStyle,
603        source_transform: PropertyBinding<LayoutTransform>,
604        kind: ReferenceFrameKind,
605        origin_in_parent_reference_frame: LayoutVector2D,
606        pipeline_id: PipelineId,
607    ) -> SpatialNodeIndex {
608
609        // Determine if this reference frame creates a new static coordinate system
610        let new_static_coord_system = match parent_index {
611            Some(..) => {
612                match kind {
613                    ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } => {
614                        // Client has guaranteed this transform will only be axis-aligned
615                        false
616                    }
617                    ReferenceFrameKind::Transform { is_2d_scale_translation: false, .. } | ReferenceFrameKind::Perspective { .. } => {
618                        // Even if client hasn't promised it's an axis-aligned transform, we can still
619                        // check this so long as the transform isn't animated (and thus could change to
620                        // anything by APZ during frame building)
621                        match source_transform {
622                            PropertyBinding::Value(m) => {
623                                !m.is_2d_scale_translation()
624                            }
625                            PropertyBinding::Binding(..) => {
626                                // Animated, so assume it may introduce a complex transform
627                                true
628                            }
629                        }
630                    }
631                }
632            }
633            None => {
634                // The root reference frame always creates a new static coord system
635                true
636            }
637        };
638
639        let static_coordinate_system_id = if new_static_coord_system {
640            let id = StaticCoordinateSystemId(self.next_static_coord_system_id);
641            self.next_static_coord_system_id += 1;
642            id
643        } else {
644            self.get_static_coordinate_system_id(parent_index.unwrap())
645        };
646
647        let node = SpatialNode::new_reference_frame(
648            parent_index,
649            transform_style,
650            source_transform,
651            kind,
652            origin_in_parent_reference_frame,
653            pipeline_id,
654            static_coordinate_system_id,
655        );
656        self.add_spatial_node(node)
657    }
658
659    pub fn add_sticky_frame(
660        &mut self,
661        parent_index: SpatialNodeIndex,
662        sticky_frame_info: StickyFrameInfo,
663        pipeline_id: PipelineId,
664    ) -> SpatialNodeIndex {
665        // Sticky frames are only 2d translations - they can't introduce a new static coord system
666        let static_coordinate_system_id = self.get_static_coordinate_system_id(parent_index);
667
668        let node = SpatialNode::new_sticky_frame(
669            parent_index,
670            sticky_frame_info,
671            pipeline_id,
672            static_coordinate_system_id,
673        );
674        self.add_spatial_node(node)
675    }
676
677    pub fn add_spatial_node(&mut self, mut node: SpatialNode) -> SpatialNodeIndex {
678        let index = SpatialNodeIndex::new(self.spatial_nodes.len());
679
680        // When the parent node is None this means we are adding the root.
681        if let Some(parent_index) = node.parent {
682            let parent_node = &mut self.spatial_nodes[parent_index.0 as usize];
683            parent_node.add_child(index);
684            node.update_snapping(Some(parent_node));
685        } else {
686            node.update_snapping(None);
687        }
688
689        self.spatial_nodes.push(node);
690        index
691    }
692
693    pub fn discard_frame_state_for_pipeline(&mut self, pipeline_id: PipelineId) {
694        self.pipelines_to_discard.insert(pipeline_id);
695    }
696
697    /// Check if a given spatial node is an ancestor of another spatial node.
698    pub fn is_ancestor(
699        &self,
700        maybe_parent: SpatialNodeIndex,
701        maybe_child: SpatialNodeIndex,
702    ) -> bool {
703        // Early out if same node
704        if maybe_parent == maybe_child {
705            return false;
706        }
707
708        let mut current_node = maybe_child;
709
710        while current_node != ROOT_SPATIAL_NODE_INDEX {
711            let node = &self.spatial_nodes[current_node.0 as usize];
712            current_node = node.parent.expect("bug: no parent");
713
714            if current_node == maybe_parent {
715                return true;
716            }
717        }
718
719        false
720    }
721
722    /// Find the spatial node that is the scroll root for a given spatial node.
723    /// A scroll root is the first spatial node when found travelling up the
724    /// spatial node tree that is an explicit scroll frame.
725    pub fn find_scroll_root(
726        &self,
727        spatial_node_index: SpatialNodeIndex,
728    ) -> SpatialNodeIndex {
729        let mut real_scroll_root = ROOT_SPATIAL_NODE_INDEX;
730        let mut outermost_scroll_root = ROOT_SPATIAL_NODE_INDEX;
731        let mut node_index = spatial_node_index;
732
733        while node_index != ROOT_SPATIAL_NODE_INDEX {
734            let node = &self.spatial_nodes[node_index.0 as usize];
735            match node.node_type {
736                SpatialNodeType::ReferenceFrame(ref info) => {
737                    match info.kind {
738                        ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } => {
739                            // We can handle scroll nodes that pass through a 2d scale/translation node
740                        }
741                        ReferenceFrameKind::Transform { is_2d_scale_translation: false, .. } |
742                        ReferenceFrameKind::Perspective { .. } => {
743                            // When a reference frame is encountered, forget any scroll roots
744                            // we have encountered, as they may end up with a non-axis-aligned transform.
745                            real_scroll_root = ROOT_SPATIAL_NODE_INDEX;
746                            outermost_scroll_root = ROOT_SPATIAL_NODE_INDEX;
747                        }
748                    }
749                }
750                SpatialNodeType::StickyFrame(..) => {}
751                SpatialNodeType::ScrollFrame(ref info) => {
752                    match info.frame_kind {
753                        ScrollFrameKind::PipelineRoot { is_root_pipeline } => {
754                            // Once we encounter a pipeline root, there is no need to look further
755                            if is_root_pipeline {
756                                break;
757                            }
758                        }
759                        ScrollFrameKind::Explicit => {
760                            // Store the closest scroll root we find to the root, for use
761                            // later on, even if it's not actually scrollable.
762                            outermost_scroll_root = node_index;
763
764                            // If the scroll root has no scrollable area, we don't want to
765                            // consider it. This helps pages that have a nested scroll root
766                            // within a redundant scroll root to avoid selecting the wrong
767                            // reference spatial node for a picture cache.
768                            if info.scrollable_size.width > MIN_SCROLLABLE_AMOUNT ||
769                               info.scrollable_size.height > MIN_SCROLLABLE_AMOUNT {
770                                // Since we are skipping redundant scroll roots, we may end up
771                                // selecting inner scroll roots that are very small. There is
772                                // no performance benefit to creating a slice for these roots,
773                                // as they are cheap to rasterize. The size comparison is in
774                                // local-space, but makes for a reasonable estimate. The value
775                                // is arbitrary, but is generally small enough to ignore things
776                                // like scroll roots around text input elements.
777                                if info.viewport_rect.width() > MIN_SCROLL_ROOT_SIZE &&
778                                   info.viewport_rect.height() > MIN_SCROLL_ROOT_SIZE {
779                                    // If we've found a root that is scrollable, and a reasonable
780                                    // size, select that as the current root for this node
781                                    real_scroll_root = node_index;
782                                }
783                            }
784                        }
785                    }
786                }
787            }
788            node_index = node.parent.expect("unable to find parent node");
789        }
790
791        // If we didn't find any real (scrollable) frames, then return the outermost
792        // redundant scroll frame. This is important so that we can correctly find
793        // the clips defined on the content which should be handled when drawing the
794        // picture cache tiles (by definition these clips are ancestors of the
795        // scroll root selected for the picture cache).
796        if real_scroll_root == ROOT_SPATIAL_NODE_INDEX {
797            outermost_scroll_root
798        } else {
799            real_scroll_root
800        }
801    }
802
803    fn print_node<T: PrintTreePrinter>(
804        &self,
805        index: SpatialNodeIndex,
806        pt: &mut T,
807    ) {
808        let node = &self.spatial_nodes[index.0 as usize];
809        match node.node_type {
810            SpatialNodeType::StickyFrame(ref sticky_frame_info) => {
811                pt.new_level(format!("StickyFrame"));
812                pt.add_item(format!("sticky info: {:?}", sticky_frame_info));
813            }
814            SpatialNodeType::ScrollFrame(scrolling_info) => {
815                pt.new_level(format!("ScrollFrame"));
816                pt.add_item(format!("viewport: {:?}", scrolling_info.viewport_rect));
817                pt.add_item(format!("scrollable_size: {:?}", scrolling_info.scrollable_size));
818                pt.add_item(format!("scroll offset: {:?}", scrolling_info.offset));
819                pt.add_item(format!("external_scroll_offset: {:?}", scrolling_info.external_scroll_offset));
820                pt.add_item(format!("kind: {:?}", scrolling_info.frame_kind));
821            }
822            SpatialNodeType::ReferenceFrame(ref info) => {
823                pt.new_level(format!("ReferenceFrame"));
824                pt.add_item(format!("kind: {:?}", info.kind));
825                pt.add_item(format!("transform_style: {:?}", info.transform_style));
826                pt.add_item(format!("source_transform: {:?}", info.source_transform));
827                pt.add_item(format!("origin_in_parent_reference_frame: {:?}", info.origin_in_parent_reference_frame));
828            }
829        }
830
831        pt.add_item(format!("index: {:?}", index));
832        pt.add_item(format!("content_transform: {:?}", node.content_transform));
833        pt.add_item(format!("viewport_transform: {:?}", node.viewport_transform));
834        pt.add_item(format!("snapping_transform: {:?}", node.snapping_transform));
835        pt.add_item(format!("coordinate_system_id: {:?}", node.coordinate_system_id));
836        pt.add_item(format!("static_coordinate_system_id: {:?}", node.static_coordinate_system_id));
837
838        for child_index in &node.children {
839            self.print_node(*child_index, pt);
840        }
841
842        pt.end_level();
843    }
844
845    /// Get the visible face of the transfrom from the specified node to its parent.
846    pub fn get_local_visible_face(&self, node_index: SpatialNodeIndex) -> VisibleFace {
847        let node = &self.spatial_nodes[node_index.0 as usize];
848        let mut face = VisibleFace::Front;
849        if let Some(parent_index) = node.parent {
850            self.get_relative_transform_with_face(node_index, parent_index, Some(&mut face));
851        }
852        face
853    }
854
855    #[allow(dead_code)]
856    pub fn print(&self) {
857        if !self.spatial_nodes.is_empty() {
858            let mut buf = Vec::<u8>::new();
859            {
860                let mut pt = PrintTree::new_with_sink("spatial tree", &mut buf);
861                self.print_with(&mut pt);
862            }
863            // If running in Gecko, set RUST_LOG=webrender::spatial_tree=debug
864            // to get this logging to be emitted to stderr/logcat.
865            debug!("{}", std::str::from_utf8(&buf).unwrap_or("(Tree printer emitted non-utf8)"));
866        }
867    }
868}
869
870impl PrintableTree for SpatialTree {
871    fn print_with<T: PrintTreePrinter>(&self, pt: &mut T) {
872        if !self.spatial_nodes.is_empty() {
873            self.print_node(self.root_reference_frame_index(), pt);
874        }
875    }
876}
877
878#[cfg(test)]
879fn add_reference_frame(
880    cst: &mut SpatialTree,
881    parent: Option<SpatialNodeIndex>,
882    transform: LayoutTransform,
883    origin_in_parent_reference_frame: LayoutVector2D,
884) -> SpatialNodeIndex {
885    cst.add_reference_frame(
886        parent,
887        TransformStyle::Preserve3D,
888        PropertyBinding::Value(transform),
889        ReferenceFrameKind::Transform {
890            is_2d_scale_translation: false,
891            should_snap: false,
892        },
893        origin_in_parent_reference_frame,
894        PipelineId::dummy(),
895    )
896}
897
898#[cfg(test)]
899fn test_pt(
900    px: f32,
901    py: f32,
902    cst: &SpatialTree,
903    child: SpatialNodeIndex,
904    parent: SpatialNodeIndex,
905    expected_x: f32,
906    expected_y: f32,
907) {
908    use euclid::approxeq::ApproxEq;
909    const EPSILON: f32 = 0.0001;
910
911    let p = LayoutPoint::new(px, py);
912    let m = cst.get_relative_transform(child, parent).into_transform();
913    let pt = m.transform_point2d(p).unwrap();
914    assert!(pt.x.approx_eq_eps(&expected_x, &EPSILON) &&
915            pt.y.approx_eq_eps(&expected_y, &EPSILON),
916            "p: {:?} -> {:?}\nm={:?}",
917            p, pt, m,
918            );
919}
920
921#[test]
922fn test_cst_simple_translation() {
923    // Basic translations only
924
925    let mut cst = SpatialTree::new();
926
927    let root = add_reference_frame(
928        &mut cst,
929        None,
930        LayoutTransform::identity(),
931        LayoutVector2D::zero(),
932    );
933
934    let child1 = add_reference_frame(
935        &mut cst,
936        Some(root),
937        LayoutTransform::translation(100.0, 0.0, 0.0),
938        LayoutVector2D::zero(),
939    );
940
941    let child2 = add_reference_frame(
942        &mut cst,
943        Some(child1),
944        LayoutTransform::translation(0.0, 50.0, 0.0),
945        LayoutVector2D::zero(),
946    );
947
948    let child3 = add_reference_frame(
949        &mut cst,
950        Some(child2),
951        LayoutTransform::translation(200.0, 200.0, 0.0),
952        LayoutVector2D::zero(),
953    );
954
955    cst.update_tree(&SceneProperties::new());
956
957    test_pt(100.0, 100.0, &cst, child1, root, 200.0, 100.0);
958    test_pt(100.0, 100.0, &cst, child2, root, 200.0, 150.0);
959    test_pt(100.0, 100.0, &cst, child2, child1, 100.0, 150.0);
960    test_pt(100.0, 100.0, &cst, child3, root, 400.0, 350.0);
961}
962
963#[test]
964fn test_cst_simple_scale() {
965    // Basic scale only
966
967    let mut cst = SpatialTree::new();
968
969    let root = add_reference_frame(
970        &mut cst,
971        None,
972        LayoutTransform::identity(),
973        LayoutVector2D::zero(),
974    );
975
976    let child1 = add_reference_frame(
977        &mut cst,
978        Some(root),
979        LayoutTransform::scale(4.0, 1.0, 1.0),
980        LayoutVector2D::zero(),
981    );
982
983    let child2 = add_reference_frame(
984        &mut cst,
985        Some(child1),
986        LayoutTransform::scale(1.0, 2.0, 1.0),
987        LayoutVector2D::zero(),
988    );
989
990    let child3 = add_reference_frame(
991        &mut cst,
992        Some(child2),
993        LayoutTransform::scale(2.0, 2.0, 1.0),
994        LayoutVector2D::zero(),
995    );
996
997    cst.update_tree(&SceneProperties::new());
998
999    test_pt(100.0, 100.0, &cst, child1, root, 400.0, 100.0);
1000    test_pt(100.0, 100.0, &cst, child2, root, 400.0, 200.0);
1001    test_pt(100.0, 100.0, &cst, child3, root, 800.0, 400.0);
1002    test_pt(100.0, 100.0, &cst, child2, child1, 100.0, 200.0);
1003    test_pt(100.0, 100.0, &cst, child3, child1, 200.0, 400.0);
1004}
1005
1006#[test]
1007fn test_cst_scale_translation() {
1008    // Scale + translation
1009
1010    let mut cst = SpatialTree::new();
1011
1012    let root = add_reference_frame(
1013        &mut cst,
1014        None,
1015        LayoutTransform::identity(),
1016        LayoutVector2D::zero(),
1017    );
1018
1019    let child1 = add_reference_frame(
1020        &mut cst,
1021        Some(root),
1022        LayoutTransform::translation(100.0, 50.0, 0.0),
1023        LayoutVector2D::zero(),
1024    );
1025
1026    let child2 = add_reference_frame(
1027        &mut cst,
1028        Some(child1),
1029        LayoutTransform::scale(2.0, 4.0, 1.0),
1030        LayoutVector2D::zero(),
1031    );
1032
1033    let child3 = add_reference_frame(
1034        &mut cst,
1035        Some(child2),
1036        LayoutTransform::translation(200.0, -100.0, 0.0),
1037        LayoutVector2D::zero(),
1038    );
1039
1040    let child4 = add_reference_frame(
1041        &mut cst,
1042        Some(child3),
1043        LayoutTransform::scale(3.0, 2.0, 1.0),
1044        LayoutVector2D::zero(),
1045    );
1046
1047    cst.update_tree(&SceneProperties::new());
1048
1049    test_pt(100.0, 100.0, &cst, child1, root, 200.0, 150.0);
1050    test_pt(100.0, 100.0, &cst, child2, root, 300.0, 450.0);
1051    test_pt(100.0, 100.0, &cst, child4, root, 1100.0, 450.0);
1052
1053    test_pt(0.0, 0.0, &cst, child4, child1, 400.0, -400.0);
1054    test_pt(100.0, 100.0, &cst, child4, child1, 1000.0, 400.0);
1055    test_pt(100.0, 100.0, &cst, child2, child1, 200.0, 400.0);
1056
1057    test_pt(100.0, 100.0, &cst, child3, child1, 600.0, 0.0);
1058}
1059
1060#[test]
1061fn test_cst_translation_rotate() {
1062    // Rotation + translation
1063    use euclid::Angle;
1064
1065    let mut cst = SpatialTree::new();
1066
1067    let root = add_reference_frame(
1068        &mut cst,
1069        None,
1070        LayoutTransform::identity(),
1071        LayoutVector2D::zero(),
1072    );
1073
1074    let child1 = add_reference_frame(
1075        &mut cst,
1076        Some(root),
1077        LayoutTransform::rotation(0.0, 0.0, 1.0, Angle::degrees(-90.0)),
1078        LayoutVector2D::zero(),
1079    );
1080
1081    cst.update_tree(&SceneProperties::new());
1082
1083    test_pt(100.0, 0.0, &cst, child1, root, 0.0, -100.0);
1084}
1085
1086#[test]
1087fn test_is_ancestor1() {
1088    let mut st = SpatialTree::new();
1089
1090    let root = add_reference_frame(
1091        &mut st,
1092        None,
1093        LayoutTransform::identity(),
1094        LayoutVector2D::zero(),
1095    );
1096
1097    let child1_0 = add_reference_frame(
1098        &mut st,
1099        Some(root),
1100        LayoutTransform::identity(),
1101        LayoutVector2D::zero(),
1102    );
1103
1104    let child1_1 = add_reference_frame(
1105        &mut st,
1106        Some(child1_0),
1107        LayoutTransform::identity(),
1108        LayoutVector2D::zero(),
1109    );
1110
1111    let child2 = add_reference_frame(
1112        &mut st,
1113        Some(root),
1114        LayoutTransform::identity(),
1115        LayoutVector2D::zero(),
1116    );
1117
1118    st.update_tree(&SceneProperties::new());
1119
1120    assert!(!st.is_ancestor(root, root));
1121    assert!(!st.is_ancestor(child1_0, child1_0));
1122    assert!(!st.is_ancestor(child1_1, child1_1));
1123    assert!(!st.is_ancestor(child2, child2));
1124
1125    assert!(st.is_ancestor(root, child1_0));
1126    assert!(st.is_ancestor(root, child1_1));
1127    assert!(st.is_ancestor(child1_0, child1_1));
1128
1129    assert!(!st.is_ancestor(child1_0, root));
1130    assert!(!st.is_ancestor(child1_1, root));
1131    assert!(!st.is_ancestor(child1_1, child1_0));
1132
1133    assert!(st.is_ancestor(root, child2));
1134    assert!(!st.is_ancestor(child2, root));
1135
1136    assert!(!st.is_ancestor(child1_0, child2));
1137    assert!(!st.is_ancestor(child1_1, child2));
1138    assert!(!st.is_ancestor(child2, child1_0));
1139    assert!(!st.is_ancestor(child2, child1_1));
1140}
1141
1142/// Tests that we select the correct scroll root in the simple case.
1143#[test]
1144fn test_find_scroll_root_simple() {
1145    let mut st = SpatialTree::new();
1146
1147    let root = st.add_reference_frame(
1148        None,
1149        TransformStyle::Flat,
1150        PropertyBinding::Value(LayoutTransform::identity()),
1151        ReferenceFrameKind::Transform {
1152            is_2d_scale_translation: false,
1153            should_snap: false,
1154        },
1155        LayoutVector2D::new(0.0, 0.0),
1156        PipelineId::dummy(),
1157    );
1158
1159    let scroll = st.add_scroll_frame(
1160        root,
1161        ExternalScrollId(1, PipelineId::dummy()),
1162        PipelineId::dummy(),
1163        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1164        &LayoutSize::new(800.0, 400.0),
1165        ScrollSensitivity::ScriptAndInputEvents,
1166        ScrollFrameKind::Explicit,
1167        LayoutVector2D::new(0.0, 0.0),
1168    );
1169
1170    assert_eq!(st.find_scroll_root(scroll), scroll);
1171}
1172
1173/// Tests that we select the root scroll frame rather than the subframe if both are scrollable.
1174#[test]
1175fn test_find_scroll_root_sub_scroll_frame() {
1176    let mut st = SpatialTree::new();
1177
1178    let root = st.add_reference_frame(
1179        None,
1180        TransformStyle::Flat,
1181        PropertyBinding::Value(LayoutTransform::identity()),
1182        ReferenceFrameKind::Transform {
1183            is_2d_scale_translation: false,
1184            should_snap: false,
1185        },
1186        LayoutVector2D::new(0.0, 0.0),
1187        PipelineId::dummy(),
1188    );
1189
1190    let root_scroll = st.add_scroll_frame(
1191        root,
1192        ExternalScrollId(1, PipelineId::dummy()),
1193        PipelineId::dummy(),
1194        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1195        &LayoutSize::new(800.0, 400.0),
1196        ScrollSensitivity::ScriptAndInputEvents,
1197        ScrollFrameKind::Explicit,
1198        LayoutVector2D::new(0.0, 0.0),
1199    );
1200
1201    let sub_scroll = st.add_scroll_frame(
1202        root_scroll,
1203        ExternalScrollId(1, PipelineId::dummy()),
1204        PipelineId::dummy(),
1205        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1206        &LayoutSize::new(800.0, 400.0),
1207        ScrollSensitivity::ScriptAndInputEvents,
1208        ScrollFrameKind::Explicit,
1209        LayoutVector2D::new(0.0, 0.0),
1210    );
1211
1212    assert_eq!(st.find_scroll_root(sub_scroll), root_scroll);
1213}
1214
1215/// Tests that we select the sub scroll frame when the root scroll frame is not scrollable.
1216#[test]
1217fn test_find_scroll_root_not_scrollable() {
1218    let mut st = SpatialTree::new();
1219
1220    let root = st.add_reference_frame(
1221        None,
1222        TransformStyle::Flat,
1223        PropertyBinding::Value(LayoutTransform::identity()),
1224        ReferenceFrameKind::Transform {
1225            is_2d_scale_translation: false,
1226            should_snap: false,
1227        },
1228        LayoutVector2D::new(0.0, 0.0),
1229        PipelineId::dummy(),
1230    );
1231
1232    let root_scroll = st.add_scroll_frame(
1233        root,
1234        ExternalScrollId(1, PipelineId::dummy()),
1235        PipelineId::dummy(),
1236        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1237        &LayoutSize::new(400.0, 400.0),
1238        ScrollSensitivity::ScriptAndInputEvents,
1239        ScrollFrameKind::Explicit,
1240        LayoutVector2D::new(0.0, 0.0),
1241    );
1242
1243    let sub_scroll = st.add_scroll_frame(
1244        root_scroll,
1245        ExternalScrollId(1, PipelineId::dummy()),
1246        PipelineId::dummy(),
1247        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1248        &LayoutSize::new(800.0, 400.0),
1249        ScrollSensitivity::ScriptAndInputEvents,
1250        ScrollFrameKind::Explicit,
1251        LayoutVector2D::new(0.0, 0.0),
1252    );
1253
1254    assert_eq!(st.find_scroll_root(sub_scroll), sub_scroll);
1255}
1256
1257/// Tests that we select the sub scroll frame when the root scroll frame is too small.
1258#[test]
1259fn test_find_scroll_root_too_small() {
1260    let mut st = SpatialTree::new();
1261
1262    let root = st.add_reference_frame(
1263        None,
1264        TransformStyle::Flat,
1265        PropertyBinding::Value(LayoutTransform::identity()),
1266        ReferenceFrameKind::Transform {
1267            is_2d_scale_translation: false,
1268            should_snap: false,
1269        },
1270        LayoutVector2D::new(0.0, 0.0),
1271        PipelineId::dummy(),
1272    );
1273
1274    let root_scroll = st.add_scroll_frame(
1275        root,
1276        ExternalScrollId(1, PipelineId::dummy()),
1277        PipelineId::dummy(),
1278        &LayoutRect::from_size(LayoutSize::new(MIN_SCROLL_ROOT_SIZE, MIN_SCROLL_ROOT_SIZE)),
1279        &LayoutSize::new(1000.0, 1000.0),
1280        ScrollSensitivity::ScriptAndInputEvents,
1281        ScrollFrameKind::Explicit,
1282        LayoutVector2D::new(0.0, 0.0),
1283    );
1284
1285    let sub_scroll = st.add_scroll_frame(
1286        root_scroll,
1287        ExternalScrollId(1, PipelineId::dummy()),
1288        PipelineId::dummy(),
1289        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1290        &LayoutSize::new(800.0, 400.0),
1291        ScrollSensitivity::ScriptAndInputEvents,
1292        ScrollFrameKind::Explicit,
1293        LayoutVector2D::new(0.0, 0.0),
1294    );
1295
1296    assert_eq!(st.find_scroll_root(sub_scroll), sub_scroll);
1297}
1298
1299/// Tests that we select the root scroll node, even if it is not scrollable,
1300/// when encountering a non-axis-aligned transform.
1301#[test]
1302fn test_find_scroll_root_perspective() {
1303    let mut st = SpatialTree::new();
1304
1305    let root = st.add_reference_frame(
1306        None,
1307        TransformStyle::Flat,
1308        PropertyBinding::Value(LayoutTransform::identity()),
1309        ReferenceFrameKind::Transform {
1310            is_2d_scale_translation: false,
1311            should_snap: false,
1312        },
1313        LayoutVector2D::new(0.0, 0.0),
1314        PipelineId::dummy(),
1315    );
1316
1317    let root_scroll = st.add_scroll_frame(
1318        root,
1319        ExternalScrollId(1, PipelineId::dummy()),
1320        PipelineId::dummy(),
1321        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1322        &LayoutSize::new(400.0, 400.0),
1323        ScrollSensitivity::ScriptAndInputEvents,
1324        ScrollFrameKind::Explicit,
1325        LayoutVector2D::new(0.0, 0.0),
1326    );
1327
1328    let perspective = st.add_reference_frame(
1329        Some(root_scroll),
1330        TransformStyle::Flat,
1331        PropertyBinding::Value(LayoutTransform::identity()),
1332        ReferenceFrameKind::Perspective {
1333            scrolling_relative_to: None,
1334        },
1335        LayoutVector2D::new(0.0, 0.0),
1336        PipelineId::dummy(),
1337    );
1338
1339    let sub_scroll = st.add_scroll_frame(
1340        perspective,
1341        ExternalScrollId(1, PipelineId::dummy()),
1342        PipelineId::dummy(),
1343        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1344        &LayoutSize::new(800.0, 400.0),
1345        ScrollSensitivity::ScriptAndInputEvents,
1346        ScrollFrameKind::Explicit,
1347        LayoutVector2D::new(0.0, 0.0),
1348    );
1349
1350    assert_eq!(st.find_scroll_root(sub_scroll), root_scroll);
1351}
1352
1353/// Tests that encountering a 2D scale or translation transform does not prevent
1354/// us from selecting the sub scroll frame if the root scroll frame is unscrollable.
1355#[test]
1356fn test_find_scroll_root_2d_scale() {
1357    let mut st = SpatialTree::new();
1358
1359    let root = st.add_reference_frame(
1360        None,
1361        TransformStyle::Flat,
1362        PropertyBinding::Value(LayoutTransform::identity()),
1363        ReferenceFrameKind::Transform {
1364            is_2d_scale_translation: false,
1365            should_snap: false,
1366        },
1367        LayoutVector2D::new(0.0, 0.0),
1368        PipelineId::dummy(),
1369    );
1370
1371    let root_scroll = st.add_scroll_frame(
1372        root,
1373        ExternalScrollId(1, PipelineId::dummy()),
1374        PipelineId::dummy(),
1375        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1376        &LayoutSize::new(400.0, 400.0),
1377        ScrollSensitivity::ScriptAndInputEvents,
1378        ScrollFrameKind::Explicit,
1379        LayoutVector2D::new(0.0, 0.0),
1380    );
1381
1382    let scale = st.add_reference_frame(
1383        Some(root_scroll),
1384        TransformStyle::Flat,
1385        PropertyBinding::Value(LayoutTransform::identity()),
1386        ReferenceFrameKind::Transform {
1387            is_2d_scale_translation: true,
1388            should_snap: false,
1389        },
1390        LayoutVector2D::new(0.0, 0.0),
1391        PipelineId::dummy(),
1392    );
1393
1394    let sub_scroll = st.add_scroll_frame(
1395        scale,
1396        ExternalScrollId(1, PipelineId::dummy()),
1397        PipelineId::dummy(),
1398        &LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
1399        &LayoutSize::new(800.0, 400.0),
1400        ScrollSensitivity::ScriptAndInputEvents,
1401        ScrollFrameKind::Explicit,
1402        LayoutVector2D::new(0.0, 0.0),
1403    );
1404
1405    assert_eq!(st.find_scroll_root(sub_scroll), sub_scroll);
1406}