webrender/
spatial_node.rs

1
2/* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6use api::{ExternalScrollId, PipelineId, PropertyBinding, PropertyBindingId, ReferenceFrameKind};
7use api::{APZScrollGeneration, HasScrollLinkedEffect, SampledScrollOffset};
8use api::{TransformStyle, StickyOffsetBounds, SpatialTreeItemKey};
9use api::units::*;
10use crate::internal_types::PipelineInstanceId;
11use crate::spatial_tree::{CoordinateSystem, SpatialNodeIndex, TransformUpdateState};
12use crate::spatial_tree::CoordinateSystemId;
13use euclid::{Vector2D, SideOffsets2D};
14use crate::scene::SceneProperties;
15use crate::util::{LayoutFastTransform, MatrixHelpers, ScaleOffset, TransformedRectKind, PointHelpers};
16
17/// The kind of a spatial node uid. These are required because we currently create external
18/// nodes during DL building, but the internal nodes aren't created until scene building.
19/// TODO(gw): The internal scroll and reference frames are not used in any important way
20//            by Gecko - they were primarily useful for Servo. So we should plan to remove
21//            them completely.
22#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
23#[cfg_attr(feature = "capture", derive(Serialize))]
24#[cfg_attr(feature = "replay", derive(Deserialize))]
25pub enum SpatialNodeUidKind {
26    /// The root node of the entire spatial tree
27    Root,
28    /// Internal scroll frame created during scene building for each iframe
29    InternalScrollFrame,
30    /// Internal reference frame created during scene building for each iframe
31    InternalReferenceFrame,
32    /// A normal spatial node uid, defined by a caller provided unique key
33    External {
34        key: SpatialTreeItemKey,
35    },
36}
37
38/// A unique identifier for a spatial node, that is stable across display lists
39#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
40#[cfg_attr(feature = "capture", derive(Serialize))]
41#[cfg_attr(feature = "replay", derive(Deserialize))]
42pub struct SpatialNodeUid {
43    /// The unique key for a given pipeline for this uid
44    pub kind: SpatialNodeUidKind,
45    /// Pipeline id to namespace key kinds
46    pub pipeline_id: PipelineId,
47    /// Instance of this pipeline id
48    pub instance_id: PipelineInstanceId,
49}
50
51impl SpatialNodeUid {
52    pub fn root() -> Self {
53        SpatialNodeUid {
54            kind: SpatialNodeUidKind::Root,
55            pipeline_id: PipelineId::dummy(),
56            instance_id: PipelineInstanceId::new(0),
57        }
58    }
59
60    pub fn root_scroll_frame(
61        pipeline_id: PipelineId,
62        instance_id: PipelineInstanceId,
63    ) -> Self {
64        SpatialNodeUid {
65            kind: SpatialNodeUidKind::InternalScrollFrame,
66            pipeline_id,
67            instance_id,
68        }
69    }
70
71    pub fn root_reference_frame(
72        pipeline_id: PipelineId,
73        instance_id: PipelineInstanceId,
74    ) -> Self {
75        SpatialNodeUid {
76            kind: SpatialNodeUidKind::InternalReferenceFrame,
77            pipeline_id,
78            instance_id,
79        }
80    }
81
82    pub fn external(
83        key: SpatialTreeItemKey,
84        pipeline_id: PipelineId,
85        instance_id: PipelineInstanceId,
86    ) -> Self {
87        SpatialNodeUid {
88            kind: SpatialNodeUidKind::External {
89                key,
90            },
91            pipeline_id,
92            instance_id,
93        }
94    }
95}
96
97/// Defines the content of a spatial node. If the values in the descriptor don't
98/// change, that means the rest of the fields in a spatial node will end up with
99/// the same result
100#[derive(Clone, PartialEq)]
101#[cfg_attr(feature = "capture", derive(Serialize))]
102#[cfg_attr(feature = "replay", derive(Deserialize))]
103pub struct SpatialNodeDescriptor {
104    /// The type of this node and any data associated with that node type.
105    pub node_type: SpatialNodeType,
106
107    /// Pipeline that this layer belongs to
108    pub pipeline_id: PipelineId,
109}
110
111#[derive(Clone, PartialEq)]
112#[cfg_attr(feature = "capture", derive(Serialize))]
113#[cfg_attr(feature = "replay", derive(Deserialize))]
114pub enum SpatialNodeType {
115    /// A special kind of node that adjusts its position based on the position
116    /// of its parent node and a given set of sticky positioning offset bounds.
117    /// Sticky positioned is described in the CSS Positioned Layout Module Level 3 here:
118    /// https://www.w3.org/TR/css-position-3/#sticky-pos
119    StickyFrame(StickyFrameInfo),
120
121    /// Transforms it's content, but doesn't clip it. Can also be adjusted
122    /// by scroll events or setting scroll offsets.
123    ScrollFrame(ScrollFrameInfo),
124
125    /// A reference frame establishes a new coordinate space in the tree.
126    ReferenceFrame(ReferenceFrameInfo),
127}
128
129/// Information about a spatial node that can be queried during either scene of
130/// frame building.
131pub struct SpatialNodeInfo<'a> {
132    /// The type of this node and any data associated with that node type.
133    pub node_type: &'a SpatialNodeType,
134
135    /// Parent spatial node. If this is None, we are the root node.
136    pub parent: Option<SpatialNodeIndex>,
137
138    /// Snapping scale/offset relative to the coordinate system. If None, then
139    /// we should not snap entities bound to this spatial node.
140    pub snapping_transform: Option<ScaleOffset>,
141}
142
143/// Scene building specific representation of a spatial node, which is a much
144/// lighter subset of a full spatial node constructed and used for frame building
145#[cfg_attr(feature = "capture", derive(Serialize))]
146#[cfg_attr(feature = "replay", derive(Deserialize))]
147#[derive(PartialEq)]
148pub struct SceneSpatialNode {
149    /// Snapping scale/offset relative to the coordinate system. If None, then
150    /// we should not snap entities bound to this spatial node.
151    pub snapping_transform: Option<ScaleOffset>,
152
153    /// Parent spatial node. If this is None, we are the root node.
154    pub parent: Option<SpatialNodeIndex>,
155
156    /// Descriptor describing how this spatial node behaves
157    pub descriptor: SpatialNodeDescriptor,
158
159    /// If true, this spatial node is known to exist in the root coordinate
160    /// system in all cases (it has no animated or complex transforms)
161    pub is_root_coord_system: bool,
162}
163
164impl SceneSpatialNode {
165    pub fn new_reference_frame(
166        parent_index: Option<SpatialNodeIndex>,
167        transform_style: TransformStyle,
168        source_transform: PropertyBinding<LayoutTransform>,
169        kind: ReferenceFrameKind,
170        origin_in_parent_reference_frame: LayoutVector2D,
171        pipeline_id: PipelineId,
172        is_root_coord_system: bool,
173        is_pipeline_root: bool,
174    ) -> Self {
175        let info = ReferenceFrameInfo {
176            transform_style,
177            source_transform,
178            kind,
179            origin_in_parent_reference_frame,
180            is_pipeline_root,
181        };
182        Self::new(
183            pipeline_id,
184            parent_index,
185            SpatialNodeType::ReferenceFrame(info),
186            is_root_coord_system,
187        )
188    }
189
190    pub fn new_scroll_frame(
191        pipeline_id: PipelineId,
192        parent_index: SpatialNodeIndex,
193        external_id: ExternalScrollId,
194        frame_rect: &LayoutRect,
195        content_size: &LayoutSize,
196        frame_kind: ScrollFrameKind,
197        external_scroll_offset: LayoutVector2D,
198        offset_generation: APZScrollGeneration,
199        has_scroll_linked_effect: HasScrollLinkedEffect,
200        is_root_coord_system: bool,
201    ) -> Self {
202        let node_type = SpatialNodeType::ScrollFrame(ScrollFrameInfo::new(
203                *frame_rect,
204                LayoutSize::new(
205                    (content_size.width - frame_rect.width()).max(0.0),
206                    (content_size.height - frame_rect.height()).max(0.0)
207                ),
208                external_id,
209                frame_kind,
210                external_scroll_offset,
211                offset_generation,
212                has_scroll_linked_effect,
213            )
214        );
215
216        Self::new(
217            pipeline_id,
218            Some(parent_index),
219            node_type,
220            is_root_coord_system,
221        )
222    }
223
224    pub fn new_sticky_frame(
225        parent_index: SpatialNodeIndex,
226        sticky_frame_info: StickyFrameInfo,
227        pipeline_id: PipelineId,
228        is_root_coord_system: bool,
229    ) -> Self {
230        Self::new(
231            pipeline_id,
232            Some(parent_index),
233            SpatialNodeType::StickyFrame(sticky_frame_info),
234            is_root_coord_system,
235        )
236    }
237
238    fn new(
239        pipeline_id: PipelineId,
240        parent_index: Option<SpatialNodeIndex>,
241        node_type: SpatialNodeType,
242        is_root_coord_system: bool,
243    ) -> Self {
244        SceneSpatialNode {
245            parent: parent_index,
246            descriptor: SpatialNodeDescriptor {
247                pipeline_id,
248                node_type,
249            },
250            snapping_transform: None,
251            is_root_coord_system,
252        }
253    }
254}
255
256/// Contains information common among all types of SpatialTree nodes.
257#[cfg_attr(feature = "capture", derive(Serialize))]
258#[cfg_attr(feature = "replay", derive(Deserialize))]
259pub struct SpatialNode {
260    /// The scale/offset of the viewport for this spatial node, relative to the
261    /// coordinate system. Includes any accumulated scrolling offsets from nodes
262    /// between our reference frame and this node.
263    pub viewport_transform: ScaleOffset,
264
265    /// Content scale/offset relative to the coordinate system.
266    pub content_transform: ScaleOffset,
267
268    /// Snapping scale/offset relative to the coordinate system. If None, then
269    /// we should not snap entities bound to this spatial node.
270    pub snapping_transform: Option<ScaleOffset>,
271
272    /// The axis-aligned coordinate system id of this node.
273    pub coordinate_system_id: CoordinateSystemId,
274
275    /// The current transform kind of this node.
276    pub transform_kind: TransformedRectKind,
277
278    /// Pipeline that this layer belongs to
279    pub pipeline_id: PipelineId,
280
281    /// Parent layer. If this is None, we are the root node.
282    pub parent: Option<SpatialNodeIndex>,
283
284    /// Child layers
285    pub children: Vec<SpatialNodeIndex>,
286
287    /// The type of this node and any data associated with that node type.
288    pub node_type: SpatialNodeType,
289
290    /// True if this node is transformed by an invertible transform.  If not, display items
291    /// transformed by this node will not be displayed and display items not transformed by this
292    /// node will not be clipped by clips that are transformed by this node.
293    pub invertible: bool,
294
295    /// Whether this specific node is currently being async zoomed.
296    /// Should be set when a SetIsTransformAsyncZooming FrameMsg is received.
297    pub is_async_zooming: bool,
298
299    /// Whether this node or any of its ancestors is being pinch zoomed.
300    /// This is calculated in update(). This will be used to decide whether
301    /// to override corresponding picture's raster space as an optimisation.
302    pub is_ancestor_or_self_zooming: bool,
303}
304
305/// Snap an offset to be incorporated into a transform, where the local space
306/// may be considered the world space. We assume raster scale is 1.0, which
307/// may not always be correct if there are intermediate surfaces used, however
308/// those are either cases where snapping is not important (e.g. has perspective
309/// or is not axis aligned), or an edge case (e.g. SVG filters) which we can accept
310/// imperfection for now.
311fn snap_offset<OffsetUnits, ScaleUnits>(
312    offset: Vector2D<f32, OffsetUnits>,
313    scale: Vector2D<f32, ScaleUnits>,
314) -> Vector2D<f32, OffsetUnits> {
315    let world_offset = WorldPoint::new(offset.x * scale.x, offset.y * scale.y);
316    let snapped_world_offset = world_offset.snap();
317    Vector2D::new(
318        if scale.x != 0.0 { snapped_world_offset.x / scale.x } else { offset.x },
319        if scale.y != 0.0 { snapped_world_offset.y / scale.y } else { offset.y },
320    )
321}
322
323impl SpatialNode {
324    pub fn add_child(&mut self, child: SpatialNodeIndex) {
325        self.children.push(child);
326    }
327
328    pub fn set_scroll_offsets(&mut self, mut offsets: Vec<SampledScrollOffset>) -> bool {
329        debug_assert!(offsets.len() > 0);
330
331        let scrolling = match self.node_type {
332            SpatialNodeType::ScrollFrame(ref mut scrolling) => scrolling,
333            _ => {
334                warn!("Tried to scroll a non-scroll node.");
335                return false;
336            }
337        };
338
339        for element in offsets.iter_mut() {
340            element.offset = -element.offset - scrolling.external_scroll_offset;
341        }
342
343        if scrolling.offsets == offsets {
344            return false;
345        }
346
347        scrolling.offsets = offsets;
348        true
349    }
350
351    pub fn mark_uninvertible(
352        &mut self,
353        state: &TransformUpdateState,
354    ) {
355        self.invertible = false;
356        self.viewport_transform = ScaleOffset::identity();
357        self.content_transform = ScaleOffset::identity();
358        self.coordinate_system_id = state.current_coordinate_system_id;
359    }
360
361    pub fn update(
362        &mut self,
363        state_stack: &[TransformUpdateState],
364        coord_systems: &mut Vec<CoordinateSystem>,
365        scene_properties: &SceneProperties,
366    ) {
367        let state = state_stack.last().unwrap();
368
369        self.is_ancestor_or_self_zooming = self.is_async_zooming | state.is_ancestor_or_self_zooming;
370
371        // If any of our parents was not rendered, we are not rendered either and can just
372        // quit here.
373        if !state.invertible {
374            self.mark_uninvertible(state);
375            return;
376        }
377
378        self.update_transform(
379            state_stack,
380            coord_systems,
381            scene_properties,
382        );
383
384        if !self.invertible {
385            self.mark_uninvertible(state);
386        }
387    }
388
389    pub fn update_transform(
390        &mut self,
391        state_stack: &[TransformUpdateState],
392        coord_systems: &mut Vec<CoordinateSystem>,
393        scene_properties: &SceneProperties,
394    ) {
395        let state = state_stack.last().unwrap();
396
397        // Start by assuming we're invertible
398        self.invertible = true;
399
400        match self.node_type {
401            SpatialNodeType::ReferenceFrame(ref mut info) => {
402                let mut cs_scale_offset = ScaleOffset::identity();
403                let mut coordinate_system_id = state.current_coordinate_system_id;
404
405                // Resolve the transform against any property bindings.
406                let source_transform = {
407                    let source_transform = scene_properties.resolve_layout_transform(&info.source_transform);
408                    if let ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } = info.kind {
409                        assert!(source_transform.is_2d_scale_translation(), "Reference frame was marked as only having 2d scale or translation");
410                    }
411
412                    LayoutFastTransform::from(source_transform)
413                };
414
415                // Do a change-basis operation on the perspective matrix using
416                // the scroll offset.
417                let source_transform = match info.kind {
418                    ReferenceFrameKind::Perspective { scrolling_relative_to: Some(external_id) } => {
419                        let mut scroll_offset = LayoutVector2D::zero();
420
421                        for parent_state in state_stack.iter().rev() {
422                            if let Some(parent_external_id) = parent_state.external_id {
423                                if parent_external_id == external_id {
424                                    break;
425                                }
426                            }
427
428                            scroll_offset += parent_state.scroll_offset;
429                        }
430
431                        // Do a change-basis operation on the
432                        // perspective matrix using the scroll offset.
433                        source_transform
434                            .pre_translate(scroll_offset)
435                            .then_translate(-scroll_offset)
436                    }
437                    ReferenceFrameKind::Perspective { scrolling_relative_to: None } |
438                    ReferenceFrameKind::Transform { .. } => source_transform,
439                };
440
441                let resolved_transform =
442                    LayoutFastTransform::with_vector(info.origin_in_parent_reference_frame)
443                        .pre_transform(&source_transform);
444
445                // The transformation for this viewport in world coordinates is the transformation for
446                // our parent reference frame, plus any accumulated scrolling offsets from nodes
447                // between our reference frame and this node. Finally, we also include
448                // whatever local transformation this reference frame provides.
449                let relative_transform = resolved_transform
450                    .then_translate(snap_offset(state.parent_accumulated_scroll_offset, state.coordinate_system_relative_scale_offset.scale))
451                    .to_transform()
452                    .with_destination::<LayoutPixel>();
453
454                let mut reset_cs_id = match info.transform_style {
455                    TransformStyle::Preserve3D => !state.preserves_3d,
456                    TransformStyle::Flat => state.preserves_3d,
457                };
458
459                // We reset the coordinate system upon either crossing the preserve-3d context boundary,
460                // or simply a 3D transformation.
461                if !reset_cs_id {
462                    // Try to update our compatible coordinate system transform. If we cannot, start a new
463                    // incompatible coordinate system.
464                    match ScaleOffset::from_transform(&relative_transform) {
465                        Some(ref scale_offset) => {
466                            // We generally do not want to snap animated transforms as it causes jitter.
467                            // However, we do want to snap the visual viewport offset when scrolling.
468                            // This may still cause jitter when zooming, unfortunately.
469                            let mut maybe_snapped = scale_offset.clone();
470                            if let ReferenceFrameKind::Transform { should_snap: true, .. } = info.kind {
471                                maybe_snapped.offset = snap_offset(
472                                    scale_offset.offset,
473                                    state.coordinate_system_relative_scale_offset.scale,
474                                );
475                            }
476                            cs_scale_offset = maybe_snapped.then(&state.coordinate_system_relative_scale_offset);
477                        }
478                        None => reset_cs_id = true,
479                    }
480                }
481                if reset_cs_id {
482                    // If we break 2D axis alignment or have a perspective component, we need to start a
483                    // new incompatible coordinate system with which we cannot share clips without masking.
484                    let transform = relative_transform.then(
485                        &state.coordinate_system_relative_scale_offset.to_transform()
486                    );
487
488                    // Push that new coordinate system and record the new id.
489                    let coord_system = {
490                        let parent_system = &coord_systems[state.current_coordinate_system_id.0 as usize];
491                        let mut cur_transform = transform;
492                        if parent_system.should_flatten {
493                            cur_transform.flatten_z_output();
494                        }
495                        let world_transform = cur_transform.then(&parent_system.world_transform);
496                        let determinant = world_transform.determinant();
497                        self.invertible = determinant != 0.0 && !determinant.is_nan();
498
499                        CoordinateSystem {
500                            transform,
501                            world_transform,
502                            should_flatten: match (info.transform_style, info.kind) {
503                                (TransformStyle::Flat, ReferenceFrameKind::Transform { .. }) => true,
504                                (_, _) => false,
505                            },
506                            parent: Some(state.current_coordinate_system_id),
507                        }
508                    };
509                    coordinate_system_id = CoordinateSystemId(coord_systems.len() as u32);
510                    coord_systems.push(coord_system);
511                }
512
513                // Ensure that the current coordinate system ID is propagated to child
514                // nodes, even if we encounter a node that is not invertible. This ensures
515                // that the invariant in get_relative_transform is not violated.
516                self.coordinate_system_id = coordinate_system_id;
517                self.viewport_transform = cs_scale_offset;
518                self.content_transform = cs_scale_offset;
519            }
520            SpatialNodeType::StickyFrame(ref mut info) => {
521                let animated_offset = if let Some(transform_binding) = info.transform {
522                  let transform = scene_properties.resolve_layout_transform(&transform_binding);
523                  match ScaleOffset::from_transform(&transform) {
524                    Some(ref scale_offset) => {
525                      debug_assert!(scale_offset.scale == Vector2D::new(1.0, 1.0),
526                                    "Can only animate a translation on sticky elements");
527                      LayoutVector2D::from_untyped(scale_offset.offset)
528                    }
529                    None => {
530                      debug_assert!(false, "Can only animate a translation on sticky elements");
531                      LayoutVector2D::zero()
532                    }
533                  }
534                } else {
535                  LayoutVector2D::zero()
536                };
537
538                let sticky_offset = Self::calculate_sticky_offset(
539                    &state.nearest_scrolling_ancestor_offset,
540                    &state.nearest_scrolling_ancestor_viewport,
541                    info,
542                );
543
544                // The transformation for the bounds of our viewport is the parent reference frame
545                // transform, plus any accumulated scroll offset from our parents, plus any offset
546                // provided by our own sticky positioning.
547                let accumulated_offset = state.parent_accumulated_scroll_offset + sticky_offset + animated_offset;
548                self.viewport_transform = state.coordinate_system_relative_scale_offset
549                    .pre_offset(snap_offset(accumulated_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());
550                self.content_transform = self.viewport_transform;
551
552                info.current_offset = sticky_offset + animated_offset;
553
554                self.coordinate_system_id = state.current_coordinate_system_id;
555            }
556            SpatialNodeType::ScrollFrame(_) => {
557                // The transformation for the bounds of our viewport is the parent reference frame
558                // transform, plus any accumulated scroll offset from our parents.
559                let accumulated_offset = state.parent_accumulated_scroll_offset;
560                self.viewport_transform = state.coordinate_system_relative_scale_offset
561                    .pre_offset(snap_offset(accumulated_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());
562
563                // The transformation for any content inside of us is the viewport transformation, plus
564                // whatever scrolling offset we supply as well.
565                let added_offset = accumulated_offset + self.scroll_offset();
566                self.content_transform = state.coordinate_system_relative_scale_offset
567                    .pre_offset(snap_offset(added_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());
568
569                self.coordinate_system_id = state.current_coordinate_system_id;
570          }
571        }
572
573        //TODO: remove the field entirely?
574        self.transform_kind = if self.coordinate_system_id.0 == 0 {
575            TransformedRectKind::AxisAligned
576        } else {
577            TransformedRectKind::Complex
578        };
579    }
580
581    fn calculate_sticky_offset(
582        viewport_scroll_offset: &LayoutVector2D,
583        viewport_rect: &LayoutRect,
584        info: &StickyFrameInfo
585    ) -> LayoutVector2D {
586        if info.margins.top.is_none() && info.margins.bottom.is_none() &&
587            info.margins.left.is_none() && info.margins.right.is_none() {
588            return LayoutVector2D::zero();
589        }
590
591        // The viewport and margins of the item establishes the maximum amount that it can
592        // be offset in order to keep it on screen. Since we care about the relationship
593        // between the scrolled content and unscrolled viewport we adjust the viewport's
594        // position by the scroll offset in order to work with their relative positions on the
595        // page.
596        let mut sticky_rect = info.frame_rect.translate(*viewport_scroll_offset);
597
598        let mut sticky_offset = LayoutVector2D::zero();
599        if let Some(margin) = info.margins.top {
600            let top_viewport_edge = viewport_rect.min.y + margin;
601            if sticky_rect.min.y < top_viewport_edge {
602                // If the sticky rect is positioned above the top edge of the viewport (plus margin)
603                // we move it down so that it is fully inside the viewport.
604                sticky_offset.y = top_viewport_edge - sticky_rect.min.y;
605            } else if info.previously_applied_offset.y > 0.0 &&
606                sticky_rect.min.y > top_viewport_edge {
607                // However, if the sticky rect is positioned *below* the top edge of the viewport
608                // and there is already some offset applied to the sticky rect's position, then
609                // we need to move it up so that it remains at the correct position. This
610                // makes sticky_offset.y negative and effectively reduces the amount of the
611                // offset that was already applied. We limit the reduction so that it can, at most,
612                // cancel out the already-applied offset, but should never end up adjusting the
613                // position the other way.
614                sticky_offset.y = top_viewport_edge - sticky_rect.min.y;
615                sticky_offset.y = sticky_offset.y.max(-info.previously_applied_offset.y);
616            }
617        }
618
619        // If we don't have a sticky-top offset (sticky_offset.y + info.previously_applied_offset.y
620        // == 0), or if we have a previously-applied bottom offset (previously_applied_offset.y < 0)
621        // then we check for handling the bottom margin case. Note that the "don't have a sticky-top
622        // offset" case includes the case where we *had* a sticky-top offset but we reduced it to
623        // zero in the above block.
624        if sticky_offset.y + info.previously_applied_offset.y <= 0.0 {
625            if let Some(margin) = info.margins.bottom {
626                // If sticky_offset.y is nonzero that means we must have set it
627                // in the sticky-top handling code above, so this item must have
628                // both top and bottom sticky margins. We adjust the item's rect
629                // by the top-sticky offset, and then combine any offset from
630                // the bottom-sticky calculation into sticky_offset below.
631                sticky_rect.min.y += sticky_offset.y;
632                sticky_rect.max.y += sticky_offset.y;
633
634                // Same as the above case, but inverted for bottom-sticky items. Here
635                // we adjust items upwards, resulting in a negative sticky_offset.y,
636                // or reduce the already-present upward adjustment, resulting in a positive
637                // sticky_offset.y.
638                let bottom_viewport_edge = viewport_rect.max.y - margin;
639                if sticky_rect.max.y > bottom_viewport_edge {
640                    sticky_offset.y += bottom_viewport_edge - sticky_rect.max.y;
641                } else if info.previously_applied_offset.y < 0.0 &&
642                    sticky_rect.max.y < bottom_viewport_edge {
643                    sticky_offset.y += bottom_viewport_edge - sticky_rect.max.y;
644                    sticky_offset.y = sticky_offset.y.min(-info.previously_applied_offset.y);
645                }
646            }
647        }
648
649        // Same as above, but for the x-axis.
650        if let Some(margin) = info.margins.left {
651            let left_viewport_edge = viewport_rect.min.x + margin;
652            if sticky_rect.min.x < left_viewport_edge {
653                sticky_offset.x = left_viewport_edge - sticky_rect.min.x;
654            } else if info.previously_applied_offset.x > 0.0 &&
655                sticky_rect.min.x > left_viewport_edge {
656                sticky_offset.x = left_viewport_edge - sticky_rect.min.x;
657                sticky_offset.x = sticky_offset.x.max(-info.previously_applied_offset.x);
658            }
659        }
660
661        if sticky_offset.x + info.previously_applied_offset.x <= 0.0 {
662            if let Some(margin) = info.margins.right {
663                sticky_rect.min.x += sticky_offset.x;
664                sticky_rect.max.x += sticky_offset.x;
665                let right_viewport_edge = viewport_rect.max.x - margin;
666                if sticky_rect.max.x > right_viewport_edge {
667                    sticky_offset.x += right_viewport_edge - sticky_rect.max.x;
668                } else if info.previously_applied_offset.x < 0.0 &&
669                    sticky_rect.max.x < right_viewport_edge {
670                    sticky_offset.x += right_viewport_edge - sticky_rect.max.x;
671                    sticky_offset.x = sticky_offset.x.min(-info.previously_applied_offset.x);
672                }
673            }
674        }
675
676        // The total "sticky offset" (which is the sum that was already applied by
677        // the calling code, stored in info.previously_applied_offset, and the extra amount we
678        // computed as a result of scrolling, stored in sticky_offset) needs to be
679        // clamped to the provided bounds.
680        let clamp_adjusted = |value: f32, adjust: f32, bounds: &StickyOffsetBounds| {
681            (value + adjust).max(bounds.min).min(bounds.max) - adjust
682        };
683        sticky_offset.y = clamp_adjusted(sticky_offset.y,
684                                         info.previously_applied_offset.y,
685                                         &info.vertical_offset_bounds);
686        sticky_offset.x = clamp_adjusted(sticky_offset.x,
687                                         info.previously_applied_offset.x,
688                                         &info.horizontal_offset_bounds);
689
690        sticky_offset
691    }
692
693    pub fn prepare_state_for_children(&self, state: &mut TransformUpdateState) {
694        state.current_coordinate_system_id = self.coordinate_system_id;
695        state.is_ancestor_or_self_zooming = self.is_ancestor_or_self_zooming;
696        state.invertible &= self.invertible;
697
698        // The transformation we are passing is the transformation of the parent
699        // reference frame and the offset is the accumulated offset of all the nodes
700        // between us and the parent reference frame. If we are a reference frame,
701        // we need to reset both these values.
702        match self.node_type {
703            SpatialNodeType::StickyFrame(ref info) => {
704                // We don't translate the combined rect by the sticky offset, because sticky
705                // offsets actually adjust the node position itself, whereas scroll offsets
706                // only apply to contents inside the node.
707                state.parent_accumulated_scroll_offset += info.current_offset;
708                // We want nested sticky items to take into account the shift
709                // we applied as well.
710                state.nearest_scrolling_ancestor_offset += info.current_offset;
711                state.preserves_3d = false;
712                state.external_id = None;
713                state.scroll_offset = info.current_offset;
714            }
715            SpatialNodeType::ScrollFrame(ref scrolling) => {
716                state.parent_accumulated_scroll_offset += scrolling.offset();
717                state.nearest_scrolling_ancestor_offset = scrolling.offset();
718                state.nearest_scrolling_ancestor_viewport = scrolling.viewport_rect;
719                state.preserves_3d = false;
720                state.external_id = Some(scrolling.external_id);
721                state.scroll_offset = scrolling.offset() + scrolling.external_scroll_offset;
722            }
723            SpatialNodeType::ReferenceFrame(ref info) => {
724                state.external_id = None;
725                state.scroll_offset = LayoutVector2D::zero();
726                state.preserves_3d = info.transform_style == TransformStyle::Preserve3D;
727                state.parent_accumulated_scroll_offset = LayoutVector2D::zero();
728                state.coordinate_system_relative_scale_offset = self.content_transform;
729                let translation = -info.origin_in_parent_reference_frame;
730                state.nearest_scrolling_ancestor_viewport =
731                    state.nearest_scrolling_ancestor_viewport
732                       .translate(translation);
733            }
734        }
735    }
736
737    pub fn scroll_offset(&self) -> LayoutVector2D {
738        match self.node_type {
739            SpatialNodeType::ScrollFrame(ref scrolling) => scrolling.offset(),
740            _ => LayoutVector2D::zero(),
741        }
742    }
743
744    pub fn matches_external_id(&self, external_id: ExternalScrollId) -> bool {
745        match self.node_type {
746            SpatialNodeType::ScrollFrame(ref info) if info.external_id == external_id => true,
747            _ => false,
748        }
749    }
750
751    /// Returns true for ReferenceFrames whose source_transform is
752    /// bound to the property binding id.
753    pub fn is_transform_bound_to_property(&self, id: PropertyBindingId) -> bool {
754        if let SpatialNodeType::ReferenceFrame(ref info) = self.node_type {
755            if let PropertyBinding::Binding(key, _) = info.source_transform {
756                id == key.id
757            } else {
758                false
759            }
760        } else {
761            false
762        }
763    }
764}
765
766/// Defines whether we have an implicit scroll frame for a pipeline root,
767/// or an explicitly defined scroll frame from the display list.
768#[derive(Copy, Clone, Debug, PartialEq)]
769#[cfg_attr(feature = "capture", derive(Serialize))]
770#[cfg_attr(feature = "replay", derive(Deserialize))]
771pub enum ScrollFrameKind {
772    PipelineRoot {
773        is_root_pipeline: bool,
774    },
775    Explicit,
776}
777
778#[derive(Clone, Debug, PartialEq)]
779#[cfg_attr(feature = "capture", derive(Serialize))]
780#[cfg_attr(feature = "replay", derive(Deserialize))]
781pub struct ScrollFrameInfo {
782    /// The rectangle of the viewport of this scroll frame. This is important for
783    /// positioning of items inside child StickyFrames.
784    pub viewport_rect: LayoutRect,
785
786    /// Amount that this ScrollFrame can scroll in both directions.
787    pub scrollable_size: LayoutSize,
788
789    /// An external id to identify this scroll frame to API clients. This
790    /// allows setting scroll positions via the API without relying on ClipsIds
791    /// which may change between frames.
792    pub external_id: ExternalScrollId,
793
794    /// Stores whether this is a scroll frame added implicitly by WR when adding
795    /// a pipeline (either the root or an iframe). We need to exclude these
796    /// when searching for scroll roots we care about for picture caching.
797    /// TODO(gw): I think we can actually completely remove the implicit
798    ///           scroll frame being added by WR, and rely on the embedder
799    ///           to define scroll frames. However, that involves API changes
800    ///           so we will use this as a temporary hack!
801    pub frame_kind: ScrollFrameKind,
802
803    /// Amount that visual components attached to this scroll node have been
804    /// pre-scrolled in their local coordinates.
805    pub external_scroll_offset: LayoutVector2D,
806
807    /// A set of a pair of negated scroll offset and scroll generation of this
808    /// scroll node. The negated scroll offset is including the pre-scrolled
809    /// amount. If, for example, a scroll node was pre-scrolled to y=10 (10
810    /// pixels down from the initial unscrolled position), then
811    /// `external_scroll_offset` would be (0,10), and this `offset` field would
812    /// be (0,-10). If WebRender is then asked to change the scroll position by
813    /// an additional 10 pixels (without changing the pre-scroll amount in the
814    /// display list), `external_scroll_offset` would remain at (0,10) and
815    /// `offset` would change to (0,-20).
816    pub offsets: Vec<SampledScrollOffset>,
817
818    /// The generation of the external_scroll_offset.
819    /// This is used to pick up the most appropriate scroll offset sampled
820    /// off the main thread.
821    pub offset_generation: APZScrollGeneration,
822
823    /// Whether the document containing this scroll frame has any scroll-linked
824    /// effect or not.
825    pub has_scroll_linked_effect: HasScrollLinkedEffect,
826}
827
828/// Manages scrolling offset.
829impl ScrollFrameInfo {
830    pub fn new(
831        viewport_rect: LayoutRect,
832        scrollable_size: LayoutSize,
833        external_id: ExternalScrollId,
834        frame_kind: ScrollFrameKind,
835        external_scroll_offset: LayoutVector2D,
836        offset_generation: APZScrollGeneration,
837        has_scroll_linked_effect: HasScrollLinkedEffect,
838    ) -> ScrollFrameInfo {
839        ScrollFrameInfo {
840            viewport_rect,
841            scrollable_size,
842            external_id,
843            frame_kind,
844            external_scroll_offset,
845            offsets: vec![SampledScrollOffset{
846                // If this scroll frame is a newly created one, using
847                // `external_scroll_offset` and `offset_generation` is correct.
848                // If this scroll frame is a result of updating an existing
849                // scroll frame and if there have already been sampled async
850                // scroll offsets by APZ, then these offsets will be replaced in
851                // SpatialTree::set_scroll_offsets via a
852                // RenderBackend::update_document call.
853                offset: -external_scroll_offset,
854                generation: offset_generation.clone(),
855            }],
856            offset_generation,
857            has_scroll_linked_effect,
858        }
859    }
860
861    pub fn offset(&self) -> LayoutVector2D {
862        debug_assert!(self.offsets.len() > 0, "There should be at least one sampled offset!");
863
864        if self.has_scroll_linked_effect == HasScrollLinkedEffect::No {
865            // If there's no scroll-linked effect, use the one-frame delay offset.
866            return self.offsets.first().map_or(LayoutVector2D::zero(), |sampled| sampled.offset);
867        }
868
869        match self.offsets.iter().find(|sampled| sampled.generation == self.offset_generation) {
870            // If we found an offset having the same generation, use it.
871            Some(sampled) => sampled.offset,
872            // If we don't have any offset having the same generation, i.e.
873            // the generation of this scroll frame is behind sampled offsets,
874            // use the first queued sampled offset.
875            _ => self.offsets.first().map_or(LayoutVector2D::zero(), |sampled| sampled.offset),
876        }
877    }
878}
879
880/// Contains information about reference frames.
881#[derive(Copy, Clone, Debug, PartialEq)]
882#[cfg_attr(feature = "capture", derive(Serialize))]
883#[cfg_attr(feature = "replay", derive(Deserialize))]
884pub struct ReferenceFrameInfo {
885    /// The source transform and perspective matrices provided by the stacking context
886    /// that forms this reference frame. We maintain the property binding information
887    /// here so that we can resolve the animated transform and update the tree each
888    /// frame.
889    pub source_transform: PropertyBinding<LayoutTransform>,
890    pub transform_style: TransformStyle,
891    pub kind: ReferenceFrameKind,
892
893    /// The original, not including the transform and relative to the parent reference frame,
894    /// origin of this reference frame. This is already rolled into the `transform' property, but
895    /// we also store it here to properly transform the viewport for sticky positioning.
896    pub origin_in_parent_reference_frame: LayoutVector2D,
897
898    /// True if this is the root reference frame for a given pipeline. This is only used
899    /// by the hit-test code, perhaps we can change the interface to not require this.
900    pub is_pipeline_root: bool,
901}
902
903#[derive(Clone, Debug, PartialEq)]
904#[cfg_attr(feature = "capture", derive(Serialize))]
905#[cfg_attr(feature = "replay", derive(Deserialize))]
906pub struct StickyFrameInfo {
907  pub margins: SideOffsets2D<Option<f32>, LayoutPixel>,
908  pub frame_rect: LayoutRect,
909    pub vertical_offset_bounds: StickyOffsetBounds,
910    pub horizontal_offset_bounds: StickyOffsetBounds,
911    pub previously_applied_offset: LayoutVector2D,
912    pub current_offset: LayoutVector2D,
913    pub transform: Option<PropertyBinding<LayoutTransform>>,
914}
915
916impl StickyFrameInfo {
917    pub fn new(
918        frame_rect: LayoutRect,
919        margins: SideOffsets2D<Option<f32>, LayoutPixel>,
920        vertical_offset_bounds: StickyOffsetBounds,
921        horizontal_offset_bounds: StickyOffsetBounds,
922        previously_applied_offset: LayoutVector2D,
923        transform: Option<PropertyBinding<LayoutTransform>>,
924    ) -> StickyFrameInfo {
925        StickyFrameInfo {
926            frame_rect,
927            margins,
928            vertical_offset_bounds,
929            horizontal_offset_bounds,
930            previously_applied_offset,
931            current_offset: LayoutVector2D::zero(),
932            transform,
933        }
934    }
935}
936
937#[test]
938fn test_cst_perspective_relative_scroll() {
939    // Verify that when computing the offset from a perspective transform
940    // to a relative scroll node that any external scroll offset is
941    // ignored. This is because external scroll offsets are not
942    // propagated across reference frame boundaries.
943
944    // It's not currently possible to verify this with a wrench reftest,
945    // since wrench doesn't understand external scroll ids. When wrench
946    // supports this, we could also verify with a reftest.
947
948    use crate::spatial_tree::{SceneSpatialTree, SpatialTree};
949    use euclid::Angle;
950
951    let mut cst = SceneSpatialTree::new();
952    let pipeline_id = PipelineId::dummy();
953    let ext_scroll_id = ExternalScrollId(1, pipeline_id);
954    let transform = LayoutTransform::rotation(0.0, 0.0, 1.0, Angle::degrees(45.0));
955    let pid = PipelineInstanceId::new(0);
956
957    let root = cst.add_reference_frame(
958        cst.root_reference_frame_index(),
959        TransformStyle::Flat,
960        PropertyBinding::Value(LayoutTransform::identity()),
961        ReferenceFrameKind::Transform {
962            is_2d_scale_translation: false,
963            should_snap: false,
964            paired_with_perspective: false,
965        },
966        LayoutVector2D::zero(),
967        pipeline_id,
968        SpatialNodeUid::external(SpatialTreeItemKey::new(0, 0), PipelineId::dummy(), pid),
969    );
970
971    let scroll_frame_1 = cst.add_scroll_frame(
972        root,
973        ext_scroll_id,
974        pipeline_id,
975        &LayoutRect::from_size(LayoutSize::new(100.0, 100.0)),
976        &LayoutSize::new(100.0, 500.0),
977        ScrollFrameKind::Explicit,
978        LayoutVector2D::zero(),
979        APZScrollGeneration::default(),
980        HasScrollLinkedEffect::No,
981        SpatialNodeUid::external(SpatialTreeItemKey::new(0, 1), PipelineId::dummy(), pid),
982    );
983
984    let scroll_frame_2 = cst.add_scroll_frame(
985        scroll_frame_1,
986        ExternalScrollId(2, pipeline_id),
987        pipeline_id,
988        &LayoutRect::from_size(LayoutSize::new(100.0, 100.0)),
989        &LayoutSize::new(100.0, 500.0),
990        ScrollFrameKind::Explicit,
991        LayoutVector2D::new(0.0, 50.0),
992        APZScrollGeneration::default(),
993        HasScrollLinkedEffect::No,
994        SpatialNodeUid::external(SpatialTreeItemKey::new(0, 3), PipelineId::dummy(), pid),
995    );
996
997    let ref_frame = cst.add_reference_frame(
998        scroll_frame_2,
999        TransformStyle::Preserve3D,
1000        PropertyBinding::Value(transform),
1001        ReferenceFrameKind::Perspective {
1002            scrolling_relative_to: Some(ext_scroll_id),
1003        },
1004        LayoutVector2D::zero(),
1005        pipeline_id,
1006        SpatialNodeUid::external(SpatialTreeItemKey::new(0, 4), PipelineId::dummy(), pid),
1007    );
1008
1009    let mut st = SpatialTree::new();
1010    st.apply_updates(cst.end_frame_and_get_pending_updates());
1011    st.update_tree(&SceneProperties::new());
1012
1013    let world_transform = st.get_world_transform(ref_frame).into_transform().cast_unit();
1014    let ref_transform = transform.then_translate(LayoutVector3D::new(0.0, -50.0, 0.0));
1015    assert!(world_transform.approx_eq(&ref_transform));
1016}
1017