tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

spatial_node.rs (47338B)


      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 
      5 use api::{ExternalScrollId, PipelineId, PropertyBinding, PropertyBindingId, ReferenceFrameKind};
      6 use api::{APZScrollGeneration, HasScrollLinkedEffect, SampledScrollOffset};
      7 use api::{TransformStyle, StickyOffsetBounds, SpatialTreeItemKey};
      8 use api::units::*;
      9 use crate::internal_types::PipelineInstanceId;
     10 use crate::spatial_tree::{CoordinateSystem, SpatialNodeIndex, TransformUpdateState};
     11 use crate::spatial_tree::CoordinateSystemId;
     12 use euclid::{Vector2D, SideOffsets2D};
     13 use crate::scene::SceneProperties;
     14 use crate::util::{LayoutFastTransform, MatrixHelpers, ScaleOffset, TransformedRectKind};
     15 use crate::util::{PointHelpers, VectorHelpers};
     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))]
     25 pub 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))]
     42 pub 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 
     51 impl 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))]
    103 pub 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))]
    114 pub 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.
    131 pub 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)]
    148 pub 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 
    164 impl 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))]
    259 pub 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    /// An internal unique identifier for use during frame building (as opposed
    305    /// to SpatialNodeUid which is used before interning).
    306    pub uid: u64,
    307 }
    308 
    309 /// Snap an offset to be incorporated into a transform, where the local space
    310 /// may be considered the world space. We assume raster scale is 1.0, which
    311 /// may not always be correct if there are intermediate surfaces used, however
    312 /// those are either cases where snapping is not important (e.g. has perspective
    313 /// or is not axis aligned), or an edge case (e.g. SVG filters) which we can accept
    314 /// imperfection for now.
    315 fn snap_offset<OffsetUnits, ScaleUnits>(
    316    offset: Vector2D<f32, OffsetUnits>,
    317    scale: Vector2D<f32, ScaleUnits>,
    318 ) -> Vector2D<f32, OffsetUnits> {
    319    let world_offset = WorldPoint::new(offset.x * scale.x, offset.y * scale.y);
    320    let snapped_world_offset = world_offset.snap();
    321    Vector2D::new(
    322        if scale.x != 0.0 { snapped_world_offset.x / scale.x } else { offset.x },
    323        if scale.y != 0.0 { snapped_world_offset.y / scale.y } else { offset.y },
    324    )
    325 }
    326 
    327 impl SpatialNode {
    328    pub fn add_child(&mut self, child: SpatialNodeIndex) {
    329        self.children.push(child);
    330    }
    331 
    332    pub fn set_scroll_offsets(&mut self, mut offsets: Vec<SampledScrollOffset>) -> bool {
    333        debug_assert!(offsets.len() > 0);
    334 
    335        let scrolling = match self.node_type {
    336            SpatialNodeType::ScrollFrame(ref mut scrolling) => scrolling,
    337            _ => {
    338                warn!("Tried to scroll a non-scroll node.");
    339                return false;
    340            }
    341        };
    342 
    343        for element in offsets.iter_mut() {
    344            element.offset = -element.offset - scrolling.external_scroll_offset;
    345 
    346            // Once the final scroll offset (APZ + content external offset) is
    347            // calculated, we need to snap it to a device pixel. We already snap
    348            // the final transforms in `update_transform`. However, we need to
    349            // ensure the offsets are also snapped so that if the offset is used
    350            // in a nested sticky frame, it is pre-snapped.
    351            element.offset = element.offset.snap();
    352        }
    353 
    354        if scrolling.offsets == offsets {
    355            return false;
    356        }
    357 
    358        scrolling.offsets = offsets;
    359        true
    360    }
    361 
    362    pub fn mark_uninvertible(
    363        &mut self,
    364        state: &TransformUpdateState,
    365    ) {
    366        self.invertible = false;
    367        self.viewport_transform = ScaleOffset::identity();
    368        self.content_transform = ScaleOffset::identity();
    369        self.coordinate_system_id = state.current_coordinate_system_id;
    370    }
    371 
    372    pub fn update(
    373        &mut self,
    374        state_stack: &[TransformUpdateState],
    375        coord_systems: &mut Vec<CoordinateSystem>,
    376        scene_properties: &SceneProperties,
    377    ) {
    378        let state = state_stack.last().unwrap();
    379 
    380        self.is_ancestor_or_self_zooming = self.is_async_zooming | state.is_ancestor_or_self_zooming;
    381 
    382        // If any of our parents was not rendered, we are not rendered either and can just
    383        // quit here.
    384        if !state.invertible {
    385            self.mark_uninvertible(state);
    386            return;
    387        }
    388 
    389        self.update_transform(
    390            state_stack,
    391            coord_systems,
    392            scene_properties,
    393        );
    394 
    395        if !self.invertible {
    396            self.mark_uninvertible(state);
    397        }
    398    }
    399 
    400    pub fn update_transform(
    401        &mut self,
    402        state_stack: &[TransformUpdateState],
    403        coord_systems: &mut Vec<CoordinateSystem>,
    404        scene_properties: &SceneProperties,
    405    ) {
    406        let state = state_stack.last().unwrap();
    407 
    408        // Start by assuming we're invertible
    409        self.invertible = true;
    410 
    411        match self.node_type {
    412            SpatialNodeType::ReferenceFrame(ref mut info) => {
    413                let mut cs_scale_offset = ScaleOffset::identity();
    414                let mut coordinate_system_id = state.current_coordinate_system_id;
    415 
    416                // Resolve the transform against any property bindings.
    417                let source_transform = {
    418                    let source_transform = scene_properties.resolve_layout_transform(&info.source_transform);
    419                    if let ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } = info.kind {
    420                        assert!(source_transform.is_2d_scale_translation(), "Reference frame was marked as only having 2d scale or translation");
    421                    }
    422 
    423                    LayoutFastTransform::from(source_transform)
    424                };
    425 
    426                // Do a change-basis operation on the perspective matrix using
    427                // the scroll offset.
    428                let source_transform = match info.kind {
    429                    ReferenceFrameKind::Perspective { scrolling_relative_to: Some(external_id) } => {
    430                        let mut scroll_offset = LayoutVector2D::zero();
    431 
    432                        for parent_state in state_stack.iter().rev() {
    433                            if let Some(parent_external_id) = parent_state.external_id {
    434                                if parent_external_id == external_id {
    435                                    break;
    436                                }
    437                            }
    438 
    439                            scroll_offset += parent_state.scroll_offset;
    440                        }
    441 
    442                        // Do a change-basis operation on the
    443                        // perspective matrix using the scroll offset.
    444                        source_transform
    445                            .pre_translate(scroll_offset)
    446                            .then_translate(-scroll_offset)
    447                    }
    448                    ReferenceFrameKind::Perspective { scrolling_relative_to: None } |
    449                    ReferenceFrameKind::Transform { .. } => source_transform,
    450                };
    451 
    452                // Previously, the origin of a stacking context transform was snapped
    453                // in Gecko. However, this causes jittering issues during scrolling in
    454                // some cases when fractional scrolling is enabled. The origin used in
    455                // Gecko doesn't have the external scroll offset from the content process
    456                // removed, so if that content-side scroll amount is fractional, it can
    457                // cause inconsistent snapping during scene building. Instead, we need
    458                // to apply the device-pixel snap _after_ the external scroll offset
    459                // has been removed. To further complicate matters, we _don't_ want to
    460                // snap this if this spatial node has a snapping transform, as we rely
    461                // on the fractional intermediate nodes in order to arrive at a correct
    462                // final snapping result. If we don't have a snapping offset, we've
    463                // reached a spatial node where snapping will no longer apply (e.g. a
    464                // complex transform) and then we need to snap the device pixel position
    465                // of that transform.
    466                let parent_origin = match self.snapping_transform {
    467                    Some(..) => {
    468                        info.origin_in_parent_reference_frame
    469                    }
    470                    None => {
    471                        snap_offset(
    472                            info.origin_in_parent_reference_frame,
    473                            state.coordinate_system_relative_scale_offset.scale,
    474                        )
    475                    }
    476                };
    477 
    478                let resolved_transform =
    479                    LayoutFastTransform::with_vector(parent_origin)
    480                        .pre_transform(&source_transform);
    481 
    482                // The transformation for this viewport in world coordinates is the transformation for
    483                // our parent reference frame, plus any accumulated scrolling offsets from nodes
    484                // between our reference frame and this node. Finally, we also include
    485                // whatever local transformation this reference frame provides.
    486                let relative_transform = resolved_transform
    487                    .then_translate(snap_offset(state.parent_accumulated_scroll_offset, state.coordinate_system_relative_scale_offset.scale))
    488                    .to_transform()
    489                    .with_destination::<LayoutPixel>();
    490 
    491                let mut reset_cs_id = match info.transform_style {
    492                    TransformStyle::Preserve3D => !state.preserves_3d,
    493                    TransformStyle::Flat => state.preserves_3d,
    494                };
    495 
    496                // We reset the coordinate system upon either crossing the preserve-3d context boundary,
    497                // or simply a 3D transformation.
    498                if !reset_cs_id {
    499                    // Try to update our compatible coordinate system transform. If we cannot, start a new
    500                    // incompatible coordinate system.
    501                    match ScaleOffset::from_transform(&relative_transform) {
    502                        Some(ref scale_offset) => {
    503                            // We generally do not want to snap animated transforms as it causes jitter.
    504                            // However, we do want to snap the visual viewport offset when scrolling.
    505                            // This may still cause jitter when zooming, unfortunately.
    506                            let mut maybe_snapped = scale_offset.clone();
    507                            if let ReferenceFrameKind::Transform { should_snap: true, .. } = info.kind {
    508                                maybe_snapped.offset = snap_offset(
    509                                    scale_offset.offset,
    510                                    state.coordinate_system_relative_scale_offset.scale,
    511                                );
    512                            }
    513                            cs_scale_offset = maybe_snapped.then(&state.coordinate_system_relative_scale_offset);
    514                        }
    515                        None => reset_cs_id = true,
    516                    }
    517                }
    518                if reset_cs_id {
    519                    // If we break 2D axis alignment or have a perspective component, we need to start a
    520                    // new incompatible coordinate system with which we cannot share clips without masking.
    521                    let transform = relative_transform.then(
    522                        &state.coordinate_system_relative_scale_offset.to_transform()
    523                    );
    524 
    525                    // Push that new coordinate system and record the new id.
    526                    let coord_system = {
    527                        let parent_system = &coord_systems[state.current_coordinate_system_id.0 as usize];
    528                        let mut cur_transform = transform;
    529                        if parent_system.should_flatten {
    530                            cur_transform.flatten_z_output();
    531                        }
    532                        let world_transform = cur_transform.then(&parent_system.world_transform);
    533                        let determinant = world_transform.determinant();
    534                        self.invertible = determinant != 0.0 && !determinant.is_nan();
    535 
    536                        CoordinateSystem {
    537                            transform,
    538                            world_transform,
    539                            should_flatten: match (info.transform_style, info.kind) {
    540                                (TransformStyle::Flat, ReferenceFrameKind::Transform { .. }) => true,
    541                                (_, _) => false,
    542                            },
    543                            parent: Some(state.current_coordinate_system_id),
    544                        }
    545                    };
    546                    coordinate_system_id = CoordinateSystemId(coord_systems.len() as u32);
    547                    coord_systems.push(coord_system);
    548                }
    549 
    550                // Ensure that the current coordinate system ID is propagated to child
    551                // nodes, even if we encounter a node that is not invertible. This ensures
    552                // that the invariant in get_relative_transform is not violated.
    553                self.coordinate_system_id = coordinate_system_id;
    554                self.viewport_transform = cs_scale_offset;
    555                self.content_transform = cs_scale_offset;
    556            }
    557            SpatialNodeType::StickyFrame(ref mut info) => {
    558                let animated_offset = if let Some(transform_binding) = info.transform {
    559                  let transform = scene_properties.resolve_layout_transform(&transform_binding);
    560                  match ScaleOffset::from_transform(&transform) {
    561                    Some(ref scale_offset) => {
    562                      debug_assert!(scale_offset.scale == Vector2D::new(1.0, 1.0),
    563                                    "Can only animate a translation on sticky elements");
    564                      LayoutVector2D::from_untyped(scale_offset.offset)
    565                    }
    566                    None => {
    567                      debug_assert!(false, "Can only animate a translation on sticky elements");
    568                      LayoutVector2D::zero()
    569                    }
    570                  }
    571                } else {
    572                  LayoutVector2D::zero()
    573                };
    574 
    575                let sticky_offset = Self::calculate_sticky_offset(
    576                    &state.nearest_scrolling_ancestor_offset,
    577                    &state.nearest_scrolling_ancestor_viewport,
    578                    info,
    579                );
    580 
    581                // The transformation for the bounds of our viewport is the parent reference frame
    582                // transform, plus any accumulated scroll offset from our parents, plus any offset
    583                // provided by our own sticky positioning.
    584                let accumulated_offset = state.parent_accumulated_scroll_offset + sticky_offset + animated_offset;
    585                self.viewport_transform = state.coordinate_system_relative_scale_offset
    586                    .pre_offset(snap_offset(accumulated_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());
    587                self.content_transform = self.viewport_transform;
    588 
    589                info.current_offset = sticky_offset + animated_offset;
    590 
    591                self.coordinate_system_id = state.current_coordinate_system_id;
    592            }
    593            SpatialNodeType::ScrollFrame(_) => {
    594                // The transformation for the bounds of our viewport is the parent reference frame
    595                // transform, plus any accumulated scroll offset from our parents.
    596                let accumulated_offset = state.parent_accumulated_scroll_offset;
    597                self.viewport_transform = state.coordinate_system_relative_scale_offset
    598                    .pre_offset(snap_offset(accumulated_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());
    599 
    600                // The transformation for any content inside of us is the viewport transformation, plus
    601                // whatever scrolling offset we supply as well.
    602                let added_offset = accumulated_offset + self.scroll_offset();
    603                self.content_transform = state.coordinate_system_relative_scale_offset
    604                    .pre_offset(snap_offset(added_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());
    605 
    606                self.coordinate_system_id = state.current_coordinate_system_id;
    607          }
    608        }
    609 
    610        //TODO: remove the field entirely?
    611        self.transform_kind = if self.coordinate_system_id.0 == 0 {
    612            TransformedRectKind::AxisAligned
    613        } else {
    614            TransformedRectKind::Complex
    615        };
    616    }
    617 
    618    fn calculate_sticky_offset(
    619        viewport_scroll_offset: &LayoutVector2D,
    620        viewport_rect: &LayoutRect,
    621        info: &StickyFrameInfo
    622    ) -> LayoutVector2D {
    623        if info.margins.top.is_none() && info.margins.bottom.is_none() &&
    624            info.margins.left.is_none() && info.margins.right.is_none() {
    625            return LayoutVector2D::zero();
    626        }
    627 
    628        // The viewport and margins of the item establishes the maximum amount that it can
    629        // be offset in order to keep it on screen. Since we care about the relationship
    630        // between the scrolled content and unscrolled viewport we adjust the viewport's
    631        // position by the scroll offset in order to work with their relative positions on the
    632        // page.
    633        let mut sticky_rect = info.frame_rect.translate(*viewport_scroll_offset);
    634 
    635        let mut sticky_offset = LayoutVector2D::zero();
    636        if let Some(margin) = info.margins.top {
    637            let top_viewport_edge = viewport_rect.min.y + margin;
    638            if sticky_rect.min.y < top_viewport_edge {
    639                // If the sticky rect is positioned above the top edge of the viewport (plus margin)
    640                // we move it down so that it is fully inside the viewport.
    641                sticky_offset.y = top_viewport_edge - sticky_rect.min.y;
    642            } else if info.previously_applied_offset.y > 0.0 &&
    643                sticky_rect.min.y > top_viewport_edge {
    644                // However, if the sticky rect is positioned *below* the top edge of the viewport
    645                // and there is already some offset applied to the sticky rect's position, then
    646                // we need to move it up so that it remains at the correct position. This
    647                // makes sticky_offset.y negative and effectively reduces the amount of the
    648                // offset that was already applied. We limit the reduction so that it can, at most,
    649                // cancel out the already-applied offset, but should never end up adjusting the
    650                // position the other way.
    651                sticky_offset.y = top_viewport_edge - sticky_rect.min.y;
    652                sticky_offset.y = sticky_offset.y.max(-info.previously_applied_offset.y);
    653            }
    654        }
    655 
    656        // If we don't have a sticky-top offset (sticky_offset.y + info.previously_applied_offset.y
    657        // == 0), or if we have a previously-applied bottom offset (previously_applied_offset.y < 0)
    658        // then we check for handling the bottom margin case. Note that the "don't have a sticky-top
    659        // offset" case includes the case where we *had* a sticky-top offset but we reduced it to
    660        // zero in the above block.
    661        if sticky_offset.y + info.previously_applied_offset.y <= 0.0 {
    662            if let Some(margin) = info.margins.bottom {
    663                // If sticky_offset.y is nonzero that means we must have set it
    664                // in the sticky-top handling code above, so this item must have
    665                // both top and bottom sticky margins. We adjust the item's rect
    666                // by the top-sticky offset, and then combine any offset from
    667                // the bottom-sticky calculation into sticky_offset below.
    668                sticky_rect.min.y += sticky_offset.y;
    669                sticky_rect.max.y += sticky_offset.y;
    670 
    671                // Same as the above case, but inverted for bottom-sticky items. Here
    672                // we adjust items upwards, resulting in a negative sticky_offset.y,
    673                // or reduce the already-present upward adjustment, resulting in a positive
    674                // sticky_offset.y.
    675                let bottom_viewport_edge = viewport_rect.max.y - margin;
    676                if sticky_rect.max.y > bottom_viewport_edge {
    677                    sticky_offset.y += bottom_viewport_edge - sticky_rect.max.y;
    678                } else if info.previously_applied_offset.y < 0.0 &&
    679                    sticky_rect.max.y < bottom_viewport_edge {
    680                    sticky_offset.y += bottom_viewport_edge - sticky_rect.max.y;
    681                    sticky_offset.y = sticky_offset.y.min(-info.previously_applied_offset.y);
    682                }
    683            }
    684        }
    685 
    686        // Same as above, but for the x-axis.
    687        if let Some(margin) = info.margins.left {
    688            let left_viewport_edge = viewport_rect.min.x + margin;
    689            if sticky_rect.min.x < left_viewport_edge {
    690                sticky_offset.x = left_viewport_edge - sticky_rect.min.x;
    691            } else if info.previously_applied_offset.x > 0.0 &&
    692                sticky_rect.min.x > left_viewport_edge {
    693                sticky_offset.x = left_viewport_edge - sticky_rect.min.x;
    694                sticky_offset.x = sticky_offset.x.max(-info.previously_applied_offset.x);
    695            }
    696        }
    697 
    698        if sticky_offset.x + info.previously_applied_offset.x <= 0.0 {
    699            if let Some(margin) = info.margins.right {
    700                sticky_rect.min.x += sticky_offset.x;
    701                sticky_rect.max.x += sticky_offset.x;
    702                let right_viewport_edge = viewport_rect.max.x - margin;
    703                if sticky_rect.max.x > right_viewport_edge {
    704                    sticky_offset.x += right_viewport_edge - sticky_rect.max.x;
    705                } else if info.previously_applied_offset.x < 0.0 &&
    706                    sticky_rect.max.x < right_viewport_edge {
    707                    sticky_offset.x += right_viewport_edge - sticky_rect.max.x;
    708                    sticky_offset.x = sticky_offset.x.min(-info.previously_applied_offset.x);
    709                }
    710            }
    711        }
    712 
    713        // The total "sticky offset" (which is the sum that was already applied by
    714        // the calling code, stored in info.previously_applied_offset, and the extra amount we
    715        // computed as a result of scrolling, stored in sticky_offset) needs to be
    716        // clamped to the provided bounds.
    717        let clamp_adjusted = |value: f32, adjust: f32, bounds: &StickyOffsetBounds| {
    718            (value + adjust).max(bounds.min).min(bounds.max) - adjust
    719        };
    720        sticky_offset.y = clamp_adjusted(sticky_offset.y,
    721                                         info.previously_applied_offset.y,
    722                                         &info.vertical_offset_bounds);
    723        sticky_offset.x = clamp_adjusted(sticky_offset.x,
    724                                         info.previously_applied_offset.x,
    725                                         &info.horizontal_offset_bounds);
    726 
    727        // Reapply the content-process side sticky offset, which was removed
    728        // from the primitive bounds attached to this node, so that interning
    729        // sees stable values.
    730        sticky_offset + info.previously_applied_offset
    731    }
    732 
    733    pub fn prepare_state_for_children(&self, state: &mut TransformUpdateState) {
    734        state.current_coordinate_system_id = self.coordinate_system_id;
    735        state.is_ancestor_or_self_zooming = self.is_ancestor_or_self_zooming;
    736        state.invertible &= self.invertible;
    737 
    738        // The transformation we are passing is the transformation of the parent
    739        // reference frame and the offset is the accumulated offset of all the nodes
    740        // between us and the parent reference frame. If we are a reference frame,
    741        // we need to reset both these values.
    742        match self.node_type {
    743            SpatialNodeType::StickyFrame(ref info) => {
    744                // We don't translate the combined rect by the sticky offset, because sticky
    745                // offsets actually adjust the node position itself, whereas scroll offsets
    746                // only apply to contents inside the node.
    747                state.parent_accumulated_scroll_offset += info.current_offset;
    748                // We want nested sticky items to take into account the shift
    749                // we applied as well.
    750                state.nearest_scrolling_ancestor_offset += info.current_offset;
    751                state.preserves_3d = false;
    752                state.external_id = None;
    753                state.scroll_offset = info.current_offset;
    754            }
    755            SpatialNodeType::ScrollFrame(ref scrolling) => {
    756                state.parent_accumulated_scroll_offset += scrolling.offset();
    757                state.nearest_scrolling_ancestor_offset = scrolling.offset();
    758                state.nearest_scrolling_ancestor_viewport = scrolling.viewport_rect;
    759                state.preserves_3d = false;
    760                state.external_id = Some(scrolling.external_id);
    761                state.scroll_offset = scrolling.offset() + scrolling.external_scroll_offset;
    762            }
    763            SpatialNodeType::ReferenceFrame(ref info) => {
    764                state.external_id = None;
    765                state.scroll_offset = LayoutVector2D::zero();
    766                state.preserves_3d = info.transform_style == TransformStyle::Preserve3D;
    767                state.parent_accumulated_scroll_offset = LayoutVector2D::zero();
    768                state.coordinate_system_relative_scale_offset = self.content_transform;
    769                let translation = -info.origin_in_parent_reference_frame;
    770                state.nearest_scrolling_ancestor_viewport =
    771                    state.nearest_scrolling_ancestor_viewport
    772                       .translate(translation);
    773            }
    774        }
    775    }
    776 
    777    pub fn scroll_offset(&self) -> LayoutVector2D {
    778        match self.node_type {
    779            SpatialNodeType::ScrollFrame(ref scrolling) => scrolling.offset(),
    780            _ => LayoutVector2D::zero(),
    781        }
    782    }
    783 
    784    pub fn matches_external_id(&self, external_id: ExternalScrollId) -> bool {
    785        match self.node_type {
    786            SpatialNodeType::ScrollFrame(ref info) if info.external_id == external_id => true,
    787            _ => false,
    788        }
    789    }
    790 
    791    /// Returns true for ReferenceFrames whose source_transform is
    792    /// bound to the property binding id.
    793    pub fn is_transform_bound_to_property(&self, id: PropertyBindingId) -> bool {
    794        if let SpatialNodeType::ReferenceFrame(ref info) = self.node_type {
    795            if let PropertyBinding::Binding(key, _) = info.source_transform {
    796                id == key.id
    797            } else {
    798                false
    799            }
    800        } else {
    801            false
    802        }
    803    }
    804 }
    805 
    806 /// Defines whether we have an implicit scroll frame for a pipeline root,
    807 /// or an explicitly defined scroll frame from the display list.
    808 #[derive(Copy, Clone, Debug, PartialEq)]
    809 #[cfg_attr(feature = "capture", derive(Serialize))]
    810 #[cfg_attr(feature = "replay", derive(Deserialize))]
    811 pub enum ScrollFrameKind {
    812    PipelineRoot {
    813        is_root_pipeline: bool,
    814    },
    815    Explicit,
    816 }
    817 
    818 #[derive(Clone, Debug, PartialEq)]
    819 #[cfg_attr(feature = "capture", derive(Serialize))]
    820 #[cfg_attr(feature = "replay", derive(Deserialize))]
    821 pub struct ScrollFrameInfo {
    822    /// The rectangle of the viewport of this scroll frame. This is important for
    823    /// positioning of items inside child StickyFrames.
    824    pub viewport_rect: LayoutRect,
    825 
    826    /// Amount that this ScrollFrame can scroll in both directions.
    827    pub scrollable_size: LayoutSize,
    828 
    829    /// An external id to identify this scroll frame to API clients. This
    830    /// allows setting scroll positions via the API without relying on ClipsIds
    831    /// which may change between frames.
    832    pub external_id: ExternalScrollId,
    833 
    834    /// Stores whether this is a scroll frame added implicitly by WR when adding
    835    /// a pipeline (either the root or an iframe). We need to exclude these
    836    /// when searching for scroll roots we care about for picture caching.
    837    /// TODO(gw): I think we can actually completely remove the implicit
    838    ///           scroll frame being added by WR, and rely on the embedder
    839    ///           to define scroll frames. However, that involves API changes
    840    ///           so we will use this as a temporary hack!
    841    pub frame_kind: ScrollFrameKind,
    842 
    843    /// Amount that visual components attached to this scroll node have been
    844    /// pre-scrolled in their local coordinates.
    845    pub external_scroll_offset: LayoutVector2D,
    846 
    847    /// A set of a pair of negated scroll offset and scroll generation of this
    848    /// scroll node. The negated scroll offset is including the pre-scrolled
    849    /// amount. If, for example, a scroll node was pre-scrolled to y=10 (10
    850    /// pixels down from the initial unscrolled position), then
    851    /// `external_scroll_offset` would be (0,10), and this `offset` field would
    852    /// be (0,-10). If WebRender is then asked to change the scroll position by
    853    /// an additional 10 pixels (without changing the pre-scroll amount in the
    854    /// display list), `external_scroll_offset` would remain at (0,10) and
    855    /// `offset` would change to (0,-20).
    856    pub offsets: Vec<SampledScrollOffset>,
    857 
    858    /// The generation of the external_scroll_offset.
    859    /// This is used to pick up the most appropriate scroll offset sampled
    860    /// off the main thread.
    861    pub offset_generation: APZScrollGeneration,
    862 
    863    /// Whether the document containing this scroll frame has any scroll-linked
    864    /// effect or not.
    865    pub has_scroll_linked_effect: HasScrollLinkedEffect,
    866 }
    867 
    868 /// Manages scrolling offset.
    869 impl ScrollFrameInfo {
    870    pub fn new(
    871        viewport_rect: LayoutRect,
    872        scrollable_size: LayoutSize,
    873        external_id: ExternalScrollId,
    874        frame_kind: ScrollFrameKind,
    875        external_scroll_offset: LayoutVector2D,
    876        offset_generation: APZScrollGeneration,
    877        has_scroll_linked_effect: HasScrollLinkedEffect,
    878    ) -> ScrollFrameInfo {
    879        ScrollFrameInfo {
    880            viewport_rect,
    881            scrollable_size,
    882            external_id,
    883            frame_kind,
    884            external_scroll_offset,
    885            offsets: vec![SampledScrollOffset{
    886                // If this scroll frame is a newly created one, using
    887                // `external_scroll_offset` and `offset_generation` is correct.
    888                // If this scroll frame is a result of updating an existing
    889                // scroll frame and if there have already been sampled async
    890                // scroll offsets by APZ, then these offsets will be replaced in
    891                // SpatialTree::set_scroll_offsets via a
    892                // RenderBackend::update_document call.
    893                offset: -external_scroll_offset,
    894                generation: offset_generation.clone(),
    895            }],
    896            offset_generation,
    897            has_scroll_linked_effect,
    898        }
    899    }
    900 
    901    pub fn offset(&self) -> LayoutVector2D {
    902        debug_assert!(self.offsets.len() > 0, "There should be at least one sampled offset!");
    903 
    904        if self.has_scroll_linked_effect == HasScrollLinkedEffect::No {
    905            // If there's no scroll-linked effect, use the one-frame delay offset.
    906            return self.offsets.first().map_or(LayoutVector2D::zero(), |sampled| sampled.offset);
    907        }
    908 
    909        match self.offsets.iter().find(|sampled| sampled.generation == self.offset_generation) {
    910            // If we found an offset having the same generation, use it.
    911            Some(sampled) => sampled.offset,
    912            // If we don't have any offset having the same generation, i.e.
    913            // the generation of this scroll frame is behind sampled offsets,
    914            // use the first queued sampled offset.
    915            _ => self.offsets.first().map_or(LayoutVector2D::zero(), |sampled| sampled.offset),
    916        }
    917    }
    918 }
    919 
    920 /// Contains information about reference frames.
    921 #[derive(Copy, Clone, Debug, PartialEq)]
    922 #[cfg_attr(feature = "capture", derive(Serialize))]
    923 #[cfg_attr(feature = "replay", derive(Deserialize))]
    924 pub struct ReferenceFrameInfo {
    925    /// The source transform and perspective matrices provided by the stacking context
    926    /// that forms this reference frame. We maintain the property binding information
    927    /// here so that we can resolve the animated transform and update the tree each
    928    /// frame.
    929    pub source_transform: PropertyBinding<LayoutTransform>,
    930    pub transform_style: TransformStyle,
    931    pub kind: ReferenceFrameKind,
    932 
    933    /// The original, not including the transform and relative to the parent reference frame,
    934    /// origin of this reference frame. This is already rolled into the `transform' property, but
    935    /// we also store it here to properly transform the viewport for sticky positioning.
    936    pub origin_in_parent_reference_frame: LayoutVector2D,
    937 
    938    /// True if this is the root reference frame for a given pipeline. This is only used
    939    /// by the hit-test code, perhaps we can change the interface to not require this.
    940    pub is_pipeline_root: bool,
    941 }
    942 
    943 #[derive(Clone, Debug, PartialEq)]
    944 #[cfg_attr(feature = "capture", derive(Serialize))]
    945 #[cfg_attr(feature = "replay", derive(Deserialize))]
    946 pub struct StickyFrameInfo {
    947  pub margins: SideOffsets2D<Option<f32>, LayoutPixel>,
    948  pub frame_rect: LayoutRect,
    949    pub vertical_offset_bounds: StickyOffsetBounds,
    950    pub horizontal_offset_bounds: StickyOffsetBounds,
    951    pub previously_applied_offset: LayoutVector2D,
    952    pub current_offset: LayoutVector2D,
    953    pub transform: Option<PropertyBinding<LayoutTransform>>,
    954 }
    955 
    956 impl StickyFrameInfo {
    957    pub fn new(
    958        frame_rect: LayoutRect,
    959        margins: SideOffsets2D<Option<f32>, LayoutPixel>,
    960        vertical_offset_bounds: StickyOffsetBounds,
    961        horizontal_offset_bounds: StickyOffsetBounds,
    962        previously_applied_offset: LayoutVector2D,
    963        transform: Option<PropertyBinding<LayoutTransform>>,
    964    ) -> StickyFrameInfo {
    965        StickyFrameInfo {
    966            frame_rect,
    967            margins,
    968            vertical_offset_bounds,
    969            horizontal_offset_bounds,
    970            previously_applied_offset,
    971            current_offset: LayoutVector2D::zero(),
    972            transform,
    973        }
    974    }
    975 }
    976 
    977 #[test]
    978 fn test_cst_perspective_relative_scroll() {
    979    // Verify that when computing the offset from a perspective transform
    980    // to a relative scroll node that any external scroll offset is
    981    // ignored. This is because external scroll offsets are not
    982    // propagated across reference frame boundaries.
    983 
    984    // It's not currently possible to verify this with a wrench reftest,
    985    // since wrench doesn't understand external scroll ids. When wrench
    986    // supports this, we could also verify with a reftest.
    987 
    988    use crate::spatial_tree::{SceneSpatialTree, SpatialTree};
    989    use euclid::Angle;
    990 
    991    let mut cst = SceneSpatialTree::new();
    992    let pipeline_id = PipelineId::dummy();
    993    let ext_scroll_id = ExternalScrollId(1, pipeline_id);
    994    let transform = LayoutTransform::rotation(0.0, 0.0, 1.0, Angle::degrees(45.0));
    995    let pid = PipelineInstanceId::new(0);
    996 
    997    let root = cst.add_reference_frame(
    998        cst.root_reference_frame_index(),
    999        TransformStyle::Flat,
   1000        PropertyBinding::Value(LayoutTransform::identity()),
   1001        ReferenceFrameKind::Transform {
   1002            is_2d_scale_translation: false,
   1003            should_snap: false,
   1004            paired_with_perspective: false,
   1005        },
   1006        LayoutVector2D::zero(),
   1007        pipeline_id,
   1008        SpatialNodeUid::external(SpatialTreeItemKey::new(0, 0), PipelineId::dummy(), pid),
   1009    );
   1010 
   1011    let scroll_frame_1 = cst.add_scroll_frame(
   1012        root,
   1013        ext_scroll_id,
   1014        pipeline_id,
   1015        &LayoutRect::from_size(LayoutSize::new(100.0, 100.0)),
   1016        &LayoutSize::new(100.0, 500.0),
   1017        ScrollFrameKind::Explicit,
   1018        LayoutVector2D::zero(),
   1019        APZScrollGeneration::default(),
   1020        HasScrollLinkedEffect::No,
   1021        SpatialNodeUid::external(SpatialTreeItemKey::new(0, 1), PipelineId::dummy(), pid),
   1022    );
   1023 
   1024    let scroll_frame_2 = cst.add_scroll_frame(
   1025        scroll_frame_1,
   1026        ExternalScrollId(2, pipeline_id),
   1027        pipeline_id,
   1028        &LayoutRect::from_size(LayoutSize::new(100.0, 100.0)),
   1029        &LayoutSize::new(100.0, 500.0),
   1030        ScrollFrameKind::Explicit,
   1031        LayoutVector2D::new(0.0, 50.0),
   1032        APZScrollGeneration::default(),
   1033        HasScrollLinkedEffect::No,
   1034        SpatialNodeUid::external(SpatialTreeItemKey::new(0, 3), PipelineId::dummy(), pid),
   1035    );
   1036 
   1037    let ref_frame = cst.add_reference_frame(
   1038        scroll_frame_2,
   1039        TransformStyle::Preserve3D,
   1040        PropertyBinding::Value(transform),
   1041        ReferenceFrameKind::Perspective {
   1042            scrolling_relative_to: Some(ext_scroll_id),
   1043        },
   1044        LayoutVector2D::zero(),
   1045        pipeline_id,
   1046        SpatialNodeUid::external(SpatialTreeItemKey::new(0, 4), PipelineId::dummy(), pid),
   1047    );
   1048 
   1049    let mut st = SpatialTree::new();
   1050    st.apply_updates(cst.end_frame_and_get_pending_updates());
   1051    st.update_tree(&SceneProperties::new());
   1052 
   1053    let world_transform = st.get_world_transform(ref_frame).into_transform().cast_unit();
   1054    let ref_transform = transform.then_translate(LayoutVector3D::new(0.0, -50.0, 0.0));
   1055    assert!(world_transform.approx_eq(&ref_transform));
   1056 }