<commit_before>
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use api::{ExternalScrollId, PipelineId, PropertyBinding, PropertyBindingId, ReferenceFrameKind};
use api::{APZScrollGeneration, HasScrollLinkedEffect, ScrollLocation};
use api::{TransformStyle, StickyOffsetBounds, SpatialTreeItemKey};
use api::units::*;
use crate::internal_types::PipelineInstanceId;
use crate::spatial_tree::{CoordinateSystem, SpatialNodeIndex, TransformUpdateState};
use crate::spatial_tree::{CoordinateSystemId};
use euclid::{Vector2D, SideOffsets2D};
use crate::scene::SceneProperties;
use crate::util::{LayoutFastTransform, MatrixHelpers, ScaleOffset, TransformedRectKind, PointHelpers};

/// The kind of a spatial node uid. These are required because we currently create external
/// nodes during DL building, but the internal nodes aren't created until scene building.
/// TODO(gw): The internal scroll and reference frames are not used in any important way
//            by Gecko - they were primarily useful for Servo. So we should plan to remove
//            them completely.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub enum SpatialNodeUidKind {
    /// The root node of the entire spatial tree
    Root,
    /// Internal scroll frame created during scene building for each iframe
    InternalScrollFrame,
    /// Internal reference frame created during scene building for each iframe
    InternalReferenceFrame,
    /// A normal spatial node uid, defined by a caller provided unique key
    External {
        key: SpatialTreeItemKey,
    },
}

/// A unique identifier for a spatial node, that is stable across display lists
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct SpatialNodeUid {
    /// The unique key for a given pipeline for this uid
    pub kind: SpatialNodeUidKind,
    /// Pipeline id to namespace key kinds
    pub pipeline_id: PipelineId,
    /// Instance of this pipeline id
    pub instance_id: PipelineInstanceId,
}

impl SpatialNodeUid {
    pub fn root() -> Self {
        SpatialNodeUid {
            kind: SpatialNodeUidKind::Root,
            pipeline_id: PipelineId::dummy(),
            instance_id: PipelineInstanceId::new(0),
        }
    }

    pub fn root_scroll_frame(
        pipeline_id: PipelineId,
        instance_id: PipelineInstanceId,
    ) -> Self {
        SpatialNodeUid {
            kind: SpatialNodeUidKind::InternalScrollFrame,
            pipeline_id,
            instance_id,
        }
    }

    pub fn root_reference_frame(
        pipeline_id: PipelineId,
        instance_id: PipelineInstanceId,
    ) -> Self {
        SpatialNodeUid {
            kind: SpatialNodeUidKind::InternalReferenceFrame,
            pipeline_id,
            instance_id,
        }
    }

    pub fn external(
        key: SpatialTreeItemKey,
        pipeline_id: PipelineId,
        instance_id: PipelineInstanceId,
    ) -> Self {
        SpatialNodeUid {
            kind: SpatialNodeUidKind::External {
                key,
            },
            pipeline_id,
            instance_id,
        }
    }
}

/// Defines the content of a spatial node. If the values in the descriptor don't
/// change, that means the rest of the fields in a spatial node will end up with
/// the same result
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct SpatialNodeDescriptor {
    /// The type of this node and any data associated with that node type.
    pub node_type: SpatialNodeType,

    /// Pipeline that this layer belongs to
    pub pipeline_id: PipelineId,
}

#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub enum SpatialNodeType {
    /// A special kind of node that adjusts its position based on the position
    /// of its parent node and a given set of sticky positioning offset bounds.
    /// Sticky positioned is described in the CSS Positioned Layout Module Level 3 here:
    /// https://www.w3.org/TR/css-position-3/#sticky-pos
    StickyFrame(StickyFrameInfo),

    /// Transforms it's content, but doesn't clip it. Can also be adjusted
    /// by scroll events or setting scroll offsets.
    ScrollFrame(ScrollFrameInfo),

    /// A reference frame establishes a new coordinate space in the tree.
    ReferenceFrame(ReferenceFrameInfo),
}

/// Information about a spatial node that can be queried during either scene of
/// frame building.
pub struct SpatialNodeInfo<'a> {
    /// The type of this node and any data associated with that node type.
    pub node_type: &'a SpatialNodeType,

    /// Parent spatial node. If this is None, we are the root node.
    pub parent: Option<SpatialNodeIndex>,

    /// Snapping scale/offset relative to the coordinate system. If None, then
    /// we should not snap entities bound to this spatial node.
    pub snapping_transform: Option<ScaleOffset>,
}

/// Scene building specific representation of a spatial node, which is a much
/// lighter subset of a full spatial node constructed and used for frame building
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
#[derive(PartialEq)]
pub struct SceneSpatialNode {
    /// Snapping scale/offset relative to the coordinate system. If None, then
    /// we should not snap entities bound to this spatial node.
    pub snapping_transform: Option<ScaleOffset>,

    /// Parent spatial node. If this is None, we are the root node.
    pub parent: Option<SpatialNodeIndex>,

    /// Descriptor describing how this spatial node behaves
    pub descriptor: SpatialNodeDescriptor,

    /// If true, this spatial node is known to exist in the root coordinate
    /// system in all cases (it has no animated or complex transforms)
    pub is_root_coord_system: bool,
}

impl SceneSpatialNode {
    pub fn new_reference_frame(
        parent_index: Option<SpatialNodeIndex>,
        transform_style: TransformStyle,
        source_transform: PropertyBinding<LayoutTransform>,
        kind: ReferenceFrameKind,
        origin_in_parent_reference_frame: LayoutVector2D,
        pipeline_id: PipelineId,
        is_root_coord_system: bool,
        is_pipeline_root: bool,
    ) -> Self {
        let info = ReferenceFrameInfo {
            transform_style,
            source_transform,
            kind,
            origin_in_parent_reference_frame,
            is_pipeline_root,
        };
        Self::new(
            pipeline_id,
            parent_index,
            SpatialNodeType::ReferenceFrame(info),
            is_root_coord_system,
        )
    }

    pub fn new_scroll_frame(
        pipeline_id: PipelineId,
        parent_index: SpatialNodeIndex,
        external_id: ExternalScrollId,
        frame_rect: &LayoutRect,
        content_size: &LayoutSize,
        frame_kind: ScrollFrameKind,
        external_scroll_offset: LayoutVector2D,
        offset_generation: APZScrollGeneration,
        has_scroll_linked_effect: HasScrollLinkedEffect,
        is_root_coord_system: bool,
    ) -> Self {
        let node_type = SpatialNodeType::ScrollFrame(ScrollFrameInfo::new(
                *frame_rect,
                LayoutSize::new(
                    (content_size.width - frame_rect.width()).max(0.0),
                    (content_size.height - frame_rect.height()).max(0.0)
                ),
                external_id,
                frame_kind,
                external_scroll_offset,
                offset_generation,
                has_scroll_linked_effect,
            )
        );

        Self::new(
            pipeline_id,
            Some(parent_index),
            node_type,
            is_root_coord_system,
        )
    }

    pub fn new_sticky_frame(
        parent_index: SpatialNodeIndex,
        sticky_frame_info: StickyFrameInfo,
        pipeline_id: PipelineId,
        is_root_coord_system: bool,
    ) -> Self {
        Self::new(
            pipeline_id,
            Some(parent_index),
            SpatialNodeType::StickyFrame(sticky_frame_info),
            is_root_coord_system,
        )
    }

    fn new(
        pipeline_id: PipelineId,
        parent_index: Option<SpatialNodeIndex>,
        node_type: SpatialNodeType,
        is_root_coord_system: bool,
    ) -> Self {
        SceneSpatialNode {
            parent: parent_index,
            descriptor: SpatialNodeDescriptor {
                pipeline_id,
                node_type,
            },
            snapping_transform: None,
            is_root_coord_system,
        }
    }
}

/// Contains information common among all types of SpatialTree nodes.
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct SpatialNode {
    /// The scale/offset of the viewport for this spatial node, relative to the
    /// coordinate system. Includes any accumulated scrolling offsets from nodes
    /// between our reference frame and this node.
    pub viewport_transform: ScaleOffset,

    /// Content scale/offset relative to the coordinate system.
    pub content_transform: ScaleOffset,

    /// Snapping scale/offset relative to the coordinate system. If None, then
    /// we should not snap entities bound to this spatial node.
    pub snapping_transform: Option<ScaleOffset>,

    /// The axis-aligned coordinate system id of this node.
    pub coordinate_system_id: CoordinateSystemId,

    /// The current transform kind of this node.
    pub transform_kind: TransformedRectKind,

    /// Pipeline that this layer belongs to
    pub pipeline_id: PipelineId,

    /// Parent layer. If this is None, we are the root node.
    pub parent: Option<SpatialNodeIndex>,

    /// Child layers
    pub children: Vec<SpatialNodeIndex>,

    /// The type of this node and any data associated with that node type.
    pub node_type: SpatialNodeType,

    /// True if this node is transformed by an invertible transform.  If not, display items
    /// transformed by this node will not be displayed and display items not transformed by this
    /// node will not be clipped by clips that are transformed by this node.
    pub invertible: bool,

    /// Whether this specific node is currently being async zoomed.
    /// Should be set when a SetIsTransformAsyncZooming FrameMsg is received.
    pub is_async_zooming: bool,

    /// Whether this node or any of its ancestors is being pinch zoomed.
    /// This is calculated in update(). This will be used to decide whether
    /// to override corresponding picture's raster space as an optimisation.
    pub is_ancestor_or_self_zooming: bool,
}

/// Snap an offset to be incorporated into a transform, where the local space
/// may be considered the world space. We assume raster scale is 1.0, which
/// may not always be correct if there are intermediate surfaces used, however
/// those are either cases where snapping is not important (e.g. has perspective
/// or is not axis aligned), or an edge case (e.g. SVG filters) which we can accept
/// imperfection for now.
fn snap_offset<OffsetUnits, ScaleUnits>(
    offset: Vector2D<f32, OffsetUnits>,
    scale: Vector2D<f32, ScaleUnits>,
) -> Vector2D<f32, OffsetUnits> {
    let world_offset = WorldPoint::new(offset.x * scale.x, offset.y * scale.y);
    let snapped_world_offset = world_offset.snap();
    Vector2D::new(
        if scale.x != 0.0 { snapped_world_offset.x / scale.x } else { offset.x },
        if scale.y != 0.0 { snapped_world_offset.y / scale.y } else { offset.y },
    )
}

impl SpatialNode {
    pub fn add_child(&mut self, child: SpatialNodeIndex) {
        self.children.push(child);
    }

    pub fn set_scroll_offset(&mut self, offset: &LayoutVector2D) -> bool {
        let scrolling = match self.node_type {
            SpatialNodeType::ScrollFrame(ref mut scrolling) => scrolling,
            _ => {
                warn!("Tried to scroll a non-scroll node.");
                return false;
            }
        };

        let new_offset = -*offset - scrolling.external_scroll_offset;

        if new_offset == scrolling.offset {
            return false;
        }

        scrolling.offset = new_offset;
        true
    }

    pub fn mark_uninvertible(
        &mut self,
        state: &TransformUpdateState,
    ) {
        self.invertible = false;
        self.viewport_transform = ScaleOffset::identity();
        self.content_transform = ScaleOffset::identity();
        self.coordinate_system_id = state.current_coordinate_system_id;
    }

    pub fn update(
        &mut self,
        state_stack: &[TransformUpdateState],
        coord_systems: &mut Vec<CoordinateSystem>,
        scene_properties: &SceneProperties,
    ) {
        let state = state_stack.last().unwrap();

        self.is_ancestor_or_self_zooming = self.is_async_zooming | state.is_ancestor_or_self_zooming;

        // If any of our parents was not rendered, we are not rendered either and can just
        // quit here.
        if !state.invertible {
            self.mark_uninvertible(state);
            return;
        }

        self.update_transform(
            state_stack,
            coord_systems,
            scene_properties,
        );

        if !self.invertible {
            self.mark_uninvertible(state);
        }
    }

    pub fn update_transform(
        &mut self,
        state_stack: &[TransformUpdateState],
        coord_systems: &mut Vec<CoordinateSystem>,
        scene_properties: &SceneProperties,
    ) {
        let state = state_stack.last().unwrap();

        // Start by assuming we're invertible
        self.invertible = true;

        match self.node_type {
            SpatialNodeType::ReferenceFrame(ref mut info) => {
                let mut cs_scale_offset = ScaleOffset::identity();
                let mut coordinate_system_id = state.current_coordinate_system_id;

                // Resolve the transform against any property bindings.
                let source_transform = {
                    let source_transform = scene_properties.resolve_layout_transform(&info.source_transform);
                    if let ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } = info.kind {
                        assert!(source_transform.is_2d_scale_translation(), "Reference frame was marked as only having 2d scale or translation");
                    }

                    LayoutFastTransform::from(source_transform)
                };

                // Do a change-basis operation on the perspective matrix using
                // the scroll offset.
                let source_transform = match info.kind {
                    ReferenceFrameKind::Perspective { scrolling_relative_to: Some(external_id) } => {
                        let mut scroll_offset = LayoutVector2D::zero();

                        for parent_state in state_stack.iter().rev() {
                            if let Some(parent_external_id) = parent_state.external_id {
                                if parent_external_id == external_id {
                                    break;
                                }
                            }

                            scroll_offset += parent_state.scroll_offset;
                        }

                        // Do a change-basis operation on the
                        // perspective matrix using the scroll offset.
                        source_transform
                            .pre_translate(scroll_offset)
                            .then_translate(-scroll_offset)
                    }
                    ReferenceFrameKind::Perspective { scrolling_relative_to: None } |
                    ReferenceFrameKind::Transform { .. } => source_transform,
                };

                let resolved_transform =
                    LayoutFastTransform::with_vector(info.origin_in_parent_reference_frame)
                        .pre_transform(&source_transform);

                // The transformation for this viewport in world coordinates is the transformation for
                // our parent reference frame, plus any accumulated scrolling offsets from nodes
                // between our reference frame and this node. Finally, we also include
                // whatever local transformation this reference frame provides.
                let relative_transform = resolved_transform
                    .then_translate(snap_offset(state.parent_accumulated_scroll_offset, state.coordinate_system_relative_scale_offset.scale))
                    .to_transform()
                    .with_destination::<LayoutPixel>();

                let mut reset_cs_id = match info.transform_style {
                    TransformStyle::Preserve3D => !state.preserves_3d,
                    TransformStyle::Flat => state.preserves_3d,
                };

                // We reset the coordinate system upon either crossing the preserve-3d context boundary,
                // or simply a 3D transformation.
                if !reset_cs_id {
                    // Try to update our compatible coordinate system transform. If we cannot, start a new
                    // incompatible coordinate system.
                    match ScaleOffset::from_transform(&relative_transform) {
                        Some(ref scale_offset) => {
                            // We generally do not want to snap animated transforms as it causes jitter.
                            // However, we do want to snap the visual viewport offset when scrolling.
                            // This may still cause jitter when zooming, unfortunately.
                            let mut maybe_snapped = scale_offset.clone();
                            if let ReferenceFrameKind::Transform { should_snap: true, .. } = info.kind {
                                maybe_snapped.offset = snap_offset(
                                    scale_offset.offset,
                                    state.coordinate_system_relative_scale_offset.scale,
                                );
                            }
                            cs_scale_offset =
                                state.coordinate_system_relative_scale_offset.accumulate(&maybe_snapped);
                        }
                        None => reset_cs_id = true,
                    }
                }
                if reset_cs_id {
                    // If we break 2D axis alignment or have a perspective component, we need to start a
                    // new incompatible coordinate system with which we cannot share clips without masking.
                    let transform = relative_transform.then(
                        &state.coordinate_system_relative_scale_offset.to_transform()
                    );

                    // Push that new coordinate system and record the new id.
                    let coord_system = {
                        let parent_system = &coord_systems[state.current_coordinate_system_id.0 as usize];
                        let mut cur_transform = transform;
                        if parent_system.should_flatten {
                            cur_transform.flatten_z_output();
                        }
                        let world_transform = cur_transform.then(&parent_system.world_transform);
                        let determinant = world_transform.determinant();
                        self.invertible = determinant != 0.0 && !determinant.is_nan();

                        CoordinateSystem {
                            transform,
                            world_transform,
                            should_flatten: match (info.transform_style, info.kind) {
                                (TransformStyle::Flat, ReferenceFrameKind::Transform { .. }) => true,
                                (_, _) => false,
                            },
                            parent: Some(state.current_coordinate_system_id),
                        }
                    };
                    coordinate_system_id = CoordinateSystemId(coord_systems.len() as u32);
                    coord_systems.push(coord_system);
                }

                // Ensure that the current coordinate system ID is propagated to child
                // nodes, even if we encounter a node that is not invertible. This ensures
                // that the invariant in get_relative_transform is not violated.
                self.coordinate_system_id = coordinate_system_id;
                self.viewport_transform = cs_scale_offset;
                self.content_transform = cs_scale_offset;
            }
            _ => {
                // We calculate this here to avoid a double-borrow later.
                let sticky_offset = self.calculate_sticky_offset(
                    &state.nearest_scrolling_ancestor_offset,
                    &state.nearest_scrolling_ancestor_viewport,
                );

                // The transformation for the bounds of our viewport is the parent reference frame
                // transform, plus any accumulated scroll offset from our parents, plus any offset
                // provided by our own sticky positioning.
                let accumulated_offset = state.parent_accumulated_scroll_offset + sticky_offset;
                self.viewport_transform = state.coordinate_system_relative_scale_offset
                    .offset(snap_offset(accumulated_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());

                // The transformation for any content inside of us is the viewport transformation, plus
                // whatever scrolling offset we supply as well.
                let added_offset = accumulated_offset + self.scroll_offset();
                self.content_transform = state.coordinate_system_relative_scale_offset
                    .offset(snap_offset(added_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());

                if let SpatialNodeType::StickyFrame(ref mut info) = self.node_type {
                    info.current_offset = sticky_offset;
                }

                self.coordinate_system_id = state.current_coordinate_system_id;
            }
        }

        //TODO: remove the field entirely?
        self.transform_kind = if self.coordinate_system_id.0 == 0 {
            TransformedRectKind::AxisAligned
        } else {
            TransformedRectKind::Complex
        };
    }

    fn calculate_sticky_offset(
        &self,
        viewport_scroll_offset: &LayoutVector2D,
        viewport_rect: &LayoutRect,
    ) -> LayoutVector2D {
        let info = match self.node_type {
            SpatialNodeType::StickyFrame(ref info) => info,
            _ => return LayoutVector2D::zero(),
        };

        if info.margins.top.is_none() && info.margins.bottom.is_none() &&
            info.margins.left.is_none() && info.margins.right.is_none() {
            return LayoutVector2D::zero();
        }

        // The viewport and margins of the item establishes the maximum amount that it can
        // be offset in order to keep it on screen. Since we care about the relationship
        // between the scrolled content and unscrolled viewport we adjust the viewport's
        // position by the scroll offset in order to work with their relative positions on the
        // page.
        let mut sticky_rect = info.frame_rect.translate(*viewport_scroll_offset);

        let mut sticky_offset = LayoutVector2D::zero();
        if let Some(margin) = info.margins.top {
            let top_viewport_edge = viewport_rect.min.y + margin;
            if sticky_rect.min.y < top_viewport_edge {
                // If the sticky rect is positioned above the top edge of the viewport (plus margin)
                // we move it down so that it is fully inside the viewport.
                sticky_offset.y = top_viewport_edge - sticky_rect.min.y;
            } else if info.previously_applied_offset.y > 0.0 &&
                sticky_rect.min.y > top_viewport_edge {
                // However, if the sticky rect is positioned *below* the top edge of the viewport
                // and there is already some offset applied to the sticky rect's position, then
                // we need to move it up so that it remains at the correct position. This
                // makes sticky_offset.y negative and effectively reduces the amount of the
                // offset that was already applied. We limit the reduction so that it can, at most,
                // cancel out the already-applied offset, but should never end up adjusting the
                // position the other way.
                sticky_offset.y = top_viewport_edge - sticky_rect.min.y;
                sticky_offset.y = sticky_offset.y.max(-info.previously_applied_offset.y);
            }
        }

        // If we don't have a sticky-top offset (sticky_offset.y + info.previously_applied_offset.y
        // == 0), or if we have a previously-applied bottom offset (previously_applied_offset.y < 0)
        // then we check for handling the bottom margin case. Note that the "don't have a sticky-top
        // offset" case includes the case where we *had* a sticky-top offset but we reduced it to
        // zero in the above block.
        if sticky_offset.y + info.previously_applied_offset.y <= 0.0 {
            if let Some(margin) = info.margins.bottom {
                // If sticky_offset.y is nonzero that means we must have set it
                // in the sticky-top handling code above, so this item must have
                // both top and bottom sticky margins. We adjust the item's rect
                // by the top-sticky offset, and then combine any offset from
                // the bottom-sticky calculation into sticky_offset below.
                sticky_rect.min.y += sticky_offset.y;
                sticky_rect.max.y += sticky_offset.y;

                // Same as the above case, but inverted for bottom-sticky items. Here
                // we adjust items upwards, resulting in a negative sticky_offset.y,
                // or reduce the already-present upward adjustment, resulting in a positive
                // sticky_offset.y.
                let bottom_viewport_edge = viewport_rect.max.y - margin;
                if sticky_rect.max.y > bottom_viewport_edge {
                    sticky_offset.y += bottom_viewport_edge - sticky_rect.max.y;
                } else if info.previously_applied_offset.y < 0.0 &&
                    sticky_rect.max.y < bottom_viewport_edge {
                    sticky_offset.y += bottom_viewport_edge - sticky_rect.max.y;
                    sticky_offset.y = sticky_offset.y.min(-info.previously_applied_offset.y);
                }
            }
        }

        // Same as above, but for the x-axis.
        if let Some(margin) = info.margins.left {
            let left_viewport_edge = viewport_rect.min.x + margin;
            if sticky_rect.min.x < left_viewport_edge {
                sticky_offset.x = left_viewport_edge - sticky_rect.min.x;
            } else if info.previously_applied_offset.x > 0.0 &&
                sticky_rect.min.x > left_viewport_edge {
                sticky_offset.x = left_viewport_edge - sticky_rect.min.x;
                sticky_offset.x = sticky_offset.x.max(-info.previously_applied_offset.x);
            }
        }

        if sticky_offset.x + info.previously_applied_offset.x <= 0.0 {
            if let Some(margin) = info.margins.right {
                sticky_rect.min.x += sticky_offset.x;
                sticky_rect.max.x += sticky_offset.x;
                let right_viewport_edge = viewport_rect.max.x - margin;
                if sticky_rect.max.x > right_viewport_edge {
                    sticky_offset.x += right_viewport_edge - sticky_rect.max.x;
                } else if info.previously_applied_offset.x < 0.0 &&
                    sticky_rect.max.x < right_viewport_edge {
                    sticky_offset.x += right_viewport_edge - sticky_rect.max.x;
                    sticky_offset.x = sticky_offset.x.min(-info.previously_applied_offset.x);
                }
            }
        }

        // The total "sticky offset" (which is the sum that was already applied by
        // the calling code, stored in info.previously_applied_offset, and the extra amount we
        // computed as a result of scrolling, stored in sticky_offset) needs to be
        // clamped to the provided bounds.
        let clamp_adjusted = |value: f32, adjust: f32, bounds: &StickyOffsetBounds| {
            (value + adjust).max(bounds.min).min(bounds.max) - adjust
        };
        sticky_offset.y = clamp_adjusted(sticky_offset.y,
                                         info.previously_applied_offset.y,
                                         &info.vertical_offset_bounds);
        sticky_offset.x = clamp_adjusted(sticky_offset.x,
                                         info.previously_applied_offset.x,
                                         &info.horizontal_offset_bounds);

        sticky_offset
    }

    pub fn prepare_state_for_children(&self, state: &mut TransformUpdateState) {
        state.current_coordinate_system_id = self.coordinate_system_id;
        state.is_ancestor_or_self_zooming = self.is_async_zooming;
        state.invertible &= self.invertible;

        // The transformation we are passing is the transformation of the parent
        // reference frame and the offset is the accumulated offset of all the nodes
        // between us and the parent reference frame. If we are a reference frame,
        // we need to reset both these values.
        match self.node_type {
            SpatialNodeType::StickyFrame(ref info) => {
                // We don't translate the combined rect by the sticky offset, because sticky
                // offsets actually adjust the node position itself, whereas scroll offsets
                // only apply to contents inside the node.
                state.parent_accumulated_scroll_offset += info.current_offset;
                // We want nested sticky items to take into account the shift
                // we applied as well.
                state.nearest_scrolling_ancestor_offset += info.current_offset;
                state.preserves_3d = false;
                state.external_id = None;
                state.scroll_offset = info.current_offset;
            }
            SpatialNodeType::ScrollFrame(ref scrolling) => {
                state.parent_accumulated_scroll_offset += scrolling.offset;
                state.nearest_scrolling_ancestor_offset = scrolling.offset;
                state.nearest_scrolling_ancestor_viewport = scrolling.viewport_rect;
                state.preserves_3d = false;
                state.external_id = Some(scrolling.external_id);
                state.scroll_offset = scrolling.offset + scrolling.external_scroll_offset;
            }
            SpatialNodeType::ReferenceFrame(ref info) => {
                state.external_id = None;
                state.scroll_offset = LayoutVector2D::zero();
                state.preserves_3d = info.transform_style == TransformStyle::Preserve3D;
                state.parent_accumulated_scroll_offset = LayoutVector2D::zero();
                state.coordinate_system_relative_scale_offset = self.content_transform;
                let translation = -info.origin_in_parent_reference_frame;
                state.nearest_scrolling_ancestor_viewport =
                    state.nearest_scrolling_ancestor_viewport
                       .translate(translation);
            }
        }
    }

    pub fn scroll(&mut self, scroll_location: ScrollLocation) -> bool {
        // TODO(gw): This scroll method doesn't currently support
        //           scroll nodes with non-zero external scroll
        //           offsets. However, it's never used by Gecko,
        //           which is the only client that requires
        //           non-zero external scroll offsets.

        let scrolling = match self.node_type {
            SpatialNodeType::ScrollFrame(ref mut scrolling) => scrolling,
            _ => return false,
        };

        let delta = match scroll_location {
            ScrollLocation::Delta(delta) => delta,
            ScrollLocation::Start => {
                if scrolling.offset.y.round() >= 0.0 {
                    // Nothing to do on this layer.
                    return false;
                }

                scrolling.offset.y = 0.0;
                return true;
            }
            ScrollLocation::End => {
                let end_pos = -scrolling.scrollable_size.height;
                if scrolling.offset.y.round() <= end_pos {
                    // Nothing to do on this layer.
                    return false;
                }

                scrolling.offset.y = end_pos;
                return true;
            }
        };

        let scrollable_width = scrolling.scrollable_size.width;
        let scrollable_height = scrolling.scrollable_size.height;
        let original_layer_scroll_offset = scrolling.offset;

        if scrollable_width > 0. {
            scrolling.offset.x = (scrolling.offset.x + delta.x)
                .min(0.0)
                .max(-scrollable_width);
        }

        if scrollable_height > 0. {
            scrolling.offset.y = (scrolling.offset.y + delta.y)
                .min(0.0)
                .max(-scrollable_height);
        }

        scrolling.offset != original_layer_scroll_offset
    }

    pub fn scroll_offset(&self) -> LayoutVector2D {
        match self.node_type {
            SpatialNodeType::ScrollFrame(ref scrolling) => scrolling.offset,
            _ => LayoutVector2D::zero(),
        }
    }

    pub fn matches_external_id(&self, external_id: ExternalScrollId) -> bool {
        match self.node_type {
            SpatialNodeType::ScrollFrame(ref info) if info.external_id == external_id => true,
            _ => false,
        }
    }

    /// Returns true for ReferenceFrames whose source_transform is
    /// bound to the property binding id.
    pub fn is_transform_bound_to_property(&self, id: PropertyBindingId) -> bool {
        if let SpatialNodeType::ReferenceFrame(ref info) = self.node_type {
            if let PropertyBinding::Binding(key, _) = info.source_transform {
                id == key.id
            } else {
                false
            }
        } else {
            false
        }
    }
}<commit_msg>Bug 1571758 - Drop scroll method from SpatialNode. r=gw<commit_after>
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use api::{ExternalScrollId, PipelineId, PropertyBinding, PropertyBindingId, ReferenceFrameKind};
use api::{APZScrollGeneration, HasScrollLinkedEffect};
use api::{TransformStyle, StickyOffsetBounds, SpatialTreeItemKey};
use api::units::*;
use crate::internal_types::PipelineInstanceId;
use crate::spatial_tree::{CoordinateSystem, SpatialNodeIndex, TransformUpdateState};
use crate::spatial_tree::{CoordinateSystemId};
use euclid::{Vector2D, SideOffsets2D};
use crate::scene::SceneProperties;
use crate::util::{LayoutFastTransform, MatrixHelpers, ScaleOffset, TransformedRectKind, PointHelpers};

/// The kind of a spatial node uid. These are required because we currently create external
/// nodes during DL building, but the internal nodes aren't created until scene building.
/// TODO(gw): The internal scroll and reference frames are not used in any important way
//            by Gecko - they were primarily useful for Servo. So we should plan to remove
//            them completely.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub enum SpatialNodeUidKind {
    /// The root node of the entire spatial tree
    Root,
    /// Internal scroll frame created during scene building for each iframe
    InternalScrollFrame,
    /// Internal reference frame created during scene building for each iframe
    InternalReferenceFrame,
    /// A normal spatial node uid, defined by a caller provided unique key
    External {
        key: SpatialTreeItemKey,
    },
}

/// A unique identifier for a spatial node, that is stable across display lists
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct SpatialNodeUid {
    /// The unique key for a given pipeline for this uid
    pub kind: SpatialNodeUidKind,
    /// Pipeline id to namespace key kinds
    pub pipeline_id: PipelineId,
    /// Instance of this pipeline id
    pub instance_id: PipelineInstanceId,
}

impl SpatialNodeUid {
    pub fn root() -> Self {
        SpatialNodeUid {
            kind: SpatialNodeUidKind::Root,
            pipeline_id: PipelineId::dummy(),
            instance_id: PipelineInstanceId::new(0),
        }
    }

    pub fn root_scroll_frame(
        pipeline_id: PipelineId,
        instance_id: PipelineInstanceId,
    ) -> Self {
        SpatialNodeUid {
            kind: SpatialNodeUidKind::InternalScrollFrame,
            pipeline_id,
            instance_id,
        }
    }

    pub fn root_reference_frame(
        pipeline_id: PipelineId,
        instance_id: PipelineInstanceId,
    ) -> Self {
        SpatialNodeUid {
            kind: SpatialNodeUidKind::InternalReferenceFrame,
            pipeline_id,
            instance_id,
        }
    }

    pub fn external(
        key: SpatialTreeItemKey,
        pipeline_id: PipelineId,
        instance_id: PipelineInstanceId,
    ) -> Self {
        SpatialNodeUid {
            kind: SpatialNodeUidKind::External {
                key,
            },
            pipeline_id,
            instance_id,
        }
    }
}

/// Defines the content of a spatial node. If the values in the descriptor don't
/// change, that means the rest of the fields in a spatial node will end up with
/// the same result
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct SpatialNodeDescriptor {
    /// The type of this node and any data associated with that node type.
    pub node_type: SpatialNodeType,

    /// Pipeline that this layer belongs to
    pub pipeline_id: PipelineId,
}

#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub enum SpatialNodeType {
    /// A special kind of node that adjusts its position based on the position
    /// of its parent node and a given set of sticky positioning offset bounds.
    /// Sticky positioned is described in the CSS Positioned Layout Module Level 3 here:
    /// https://www.w3.org/TR/css-position-3/#sticky-pos
    StickyFrame(StickyFrameInfo),

    /// Transforms it's content, but doesn't clip it. Can also be adjusted
    /// by scroll events or setting scroll offsets.
    ScrollFrame(ScrollFrameInfo),

    /// A reference frame establishes a new coordinate space in the tree.
    ReferenceFrame(ReferenceFrameInfo),
}

/// Information about a spatial node that can be queried during either scene of
/// frame building.
pub struct SpatialNodeInfo<'a> {
    /// The type of this node and any data associated with that node type.
    pub node_type: &'a SpatialNodeType,

    /// Parent spatial node. If this is None, we are the root node.
    pub parent: Option<SpatialNodeIndex>,

    /// Snapping scale/offset relative to the coordinate system. If None, then
    /// we should not snap entities bound to this spatial node.
    pub snapping_transform: Option<ScaleOffset>,
}

/// Scene building specific representation of a spatial node, which is a much
/// lighter subset of a full spatial node constructed and used for frame building
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
#[derive(PartialEq)]
pub struct SceneSpatialNode {
    /// Snapping scale/offset relative to the coordinate system. If None, then
    /// we should not snap entities bound to this spatial node.
    pub snapping_transform: Option<ScaleOffset>,

    /// Parent spatial node. If this is None, we are the root node.
    pub parent: Option<SpatialNodeIndex>,

    /// Descriptor describing how this spatial node behaves
    pub descriptor: SpatialNodeDescriptor,

    /// If true, this spatial node is known to exist in the root coordinate
    /// system in all cases (it has no animated or complex transforms)
    pub is_root_coord_system: bool,
}

impl SceneSpatialNode {
    pub fn new_reference_frame(
        parent_index: Option<SpatialNodeIndex>,
        transform_style: TransformStyle,
        source_transform: PropertyBinding<LayoutTransform>,
        kind: ReferenceFrameKind,
        origin_in_parent_reference_frame: LayoutVector2D,
        pipeline_id: PipelineId,
        is_root_coord_system: bool,
        is_pipeline_root: bool,
    ) -> Self {
        let info = ReferenceFrameInfo {
            transform_style,
            source_transform,
            kind,
            origin_in_parent_reference_frame,
            is_pipeline_root,
        };
        Self::new(
            pipeline_id,
            parent_index,
            SpatialNodeType::ReferenceFrame(info),
            is_root_coord_system,
        )
    }

    pub fn new_scroll_frame(
        pipeline_id: PipelineId,
        parent_index: SpatialNodeIndex,
        external_id: ExternalScrollId,
        frame_rect: &LayoutRect,
        content_size: &LayoutSize,
        frame_kind: ScrollFrameKind,
        external_scroll_offset: LayoutVector2D,
        offset_generation: APZScrollGeneration,
        has_scroll_linked_effect: HasScrollLinkedEffect,
        is_root_coord_system: bool,
    ) -> Self {
        let node_type = SpatialNodeType::ScrollFrame(ScrollFrameInfo::new(
                *frame_rect,
                LayoutSize::new(
                    (content_size.width - frame_rect.width()).max(0.0),
                    (content_size.height - frame_rect.height()).max(0.0)
                ),
                external_id,
                frame_kind,
                external_scroll_offset,
                offset_generation,
                has_scroll_linked_effect,
            )
        );

        Self::new(
            pipeline_id,
            Some(parent_index),
            node_type,
            is_root_coord_system,
        )
    }

    pub fn new_sticky_frame(
        parent_index: SpatialNodeIndex,
        sticky_frame_info: StickyFrameInfo,
        pipeline_id: PipelineId,
        is_root_coord_system: bool,
    ) -> Self {
        Self::new(
            pipeline_id,
            Some(parent_index),
            SpatialNodeType::StickyFrame(sticky_frame_info),
            is_root_coord_system,
        )
    }

    fn new(
        pipeline_id: PipelineId,
        parent_index: Option<SpatialNodeIndex>,
        node_type: SpatialNodeType,
        is_root_coord_system: bool,
    ) -> Self {
        SceneSpatialNode {
            parent: parent_index,
            descriptor: SpatialNodeDescriptor {
                pipeline_id,
                node_type,
            },
            snapping_transform: None,
            is_root_coord_system,
        }
    }
}

/// Contains information common among all types of SpatialTree nodes.
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct SpatialNode {
    /// The scale/offset of the viewport for this spatial node, relative to the
    /// coordinate system. Includes any accumulated scrolling offsets from nodes
    /// between our reference frame and this node.
    pub viewport_transform: ScaleOffset,

    /// Content scale/offset relative to the coordinate system.
    pub content_transform: ScaleOffset,

    /// Snapping scale/offset relative to the coordinate system. If None, then
    /// we should not snap entities bound to this spatial node.
    pub snapping_transform: Option<ScaleOffset>,

    /// The axis-aligned coordinate system id of this node.
    pub coordinate_system_id: CoordinateSystemId,

    /// The current transform kind of this node.
    pub transform_kind: TransformedRectKind,

    /// Pipeline that this layer belongs to
    pub pipeline_id: PipelineId,

    /// Parent layer. If this is None, we are the root node.
    pub parent: Option<SpatialNodeIndex>,

    /// Child layers
    pub children: Vec<SpatialNodeIndex>,

    /// The type of this node and any data associated with that node type.
    pub node_type: SpatialNodeType,

    /// True if this node is transformed by an invertible transform.  If not, display items
    /// transformed by this node will not be displayed and display items not transformed by this
    /// node will not be clipped by clips that are transformed by this node.
    pub invertible: bool,

    /// Whether this specific node is currently being async zoomed.
    /// Should be set when a SetIsTransformAsyncZooming FrameMsg is received.
    pub is_async_zooming: bool,

    /// Whether this node or any of its ancestors is being pinch zoomed.
    /// This is calculated in update(). This will be used to decide whether
    /// to override corresponding picture's raster space as an optimisation.
    pub is_ancestor_or_self_zooming: bool,
}

/// Snap an offset to be incorporated into a transform, where the local space
/// may be considered the world space. We assume raster scale is 1.0, which
/// may not always be correct if there are intermediate surfaces used, however
/// those are either cases where snapping is not important (e.g. has perspective
/// or is not axis aligned), or an edge case (e.g. SVG filters) which we can accept
/// imperfection for now.
fn snap_offset<OffsetUnits, ScaleUnits>(
    offset: Vector2D<f32, OffsetUnits>,
    scale: Vector2D<f32, ScaleUnits>,
) -> Vector2D<f32, OffsetUnits> {
    let world_offset = WorldPoint::new(offset.x * scale.x, offset.y * scale.y);
    let snapped_world_offset = world_offset.snap();
    Vector2D::new(
        if scale.x != 0.0 { snapped_world_offset.x / scale.x } else { offset.x },
        if scale.y != 0.0 { snapped_world_offset.y / scale.y } else { offset.y },
    )
}

impl SpatialNode {
    pub fn add_child(&mut self, child: SpatialNodeIndex) {
        self.children.push(child);
    }

    pub fn set_scroll_offset(&mut self, offset: &LayoutVector2D) -> bool {
        let scrolling = match self.node_type {
            SpatialNodeType::ScrollFrame(ref mut scrolling) => scrolling,
            _ => {
                warn!("Tried to scroll a non-scroll node.");
                return false;
            }
        };

        let new_offset = -*offset - scrolling.external_scroll_offset;

        if new_offset == scrolling.offset {
            return false;
        }

        scrolling.offset = new_offset;
        true
    }

    pub fn mark_uninvertible(
        &mut self,
        state: &TransformUpdateState,
    ) {
        self.invertible = false;
        self.viewport_transform = ScaleOffset::identity();
        self.content_transform = ScaleOffset::identity();
        self.coordinate_system_id = state.current_coordinate_system_id;
    }

    pub fn update(
        &mut self,
        state_stack: &[TransformUpdateState],
        coord_systems: &mut Vec<CoordinateSystem>,
        scene_properties: &SceneProperties,
    ) {
        let state = state_stack.last().unwrap();

        self.is_ancestor_or_self_zooming = self.is_async_zooming | state.is_ancestor_or_self_zooming;

        // If any of our parents was not rendered, we are not rendered either and can just
        // quit here.
        if !state.invertible {
            self.mark_uninvertible(state);
            return;
        }

        self.update_transform(
            state_stack,
            coord_systems,
            scene_properties,
        );

        if !self.invertible {
            self.mark_uninvertible(state);
        }
    }

    pub fn update_transform(
        &mut self,
        state_stack: &[TransformUpdateState],
        coord_systems: &mut Vec<CoordinateSystem>,
        scene_properties: &SceneProperties,
    ) {
        let state = state_stack.last().unwrap();

        // Start by assuming we're invertible
        self.invertible = true;

        match self.node_type {
            SpatialNodeType::ReferenceFrame(ref mut info) => {
                let mut cs_scale_offset = ScaleOffset::identity();
                let mut coordinate_system_id = state.current_coordinate_system_id;

                // Resolve the transform against any property bindings.
                let source_transform = {
                    let source_transform = scene_properties.resolve_layout_transform(&info.source_transform);
                    if let ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } = info.kind {
                        assert!(source_transform.is_2d_scale_translation(), "Reference frame was marked as only having 2d scale or translation");
                    }

                    LayoutFastTransform::from(source_transform)
                };

                // Do a change-basis operation on the perspective matrix using
                // the scroll offset.
                let source_transform = match info.kind {
                    ReferenceFrameKind::Perspective { scrolling_relative_to: Some(external_id) } => {
                        let mut scroll_offset = LayoutVector2D::zero();

                        for parent_state in state_stack.iter().rev() {
                            if let Some(parent_external_id) = parent_state.external_id {
                                if parent_external_id == external_id {
                                    break;
                                }
                            }

                            scroll_offset += parent_state.scroll_offset;
                        }

                        // Do a change-basis operation on the
                        // perspective matrix using the scroll offset.
                        source_transform
                            .pre_translate(scroll_offset)
                            .then_translate(-scroll_offset)
                    }
                    ReferenceFrameKind::Perspective { scrolling_relative_to: None } |
                    ReferenceFrameKind::Transform { .. } => source_transform,
                };

                let resolved_transform =
                    LayoutFastTransform::with_vector(info.origin_in_parent_reference_frame)
                        .pre_transform(&source_transform);

                // The transformation for this viewport in world coordinates is the transformation for
                // our parent reference frame, plus any accumulated scrolling offsets from nodes
                // between our reference frame and this node. Finally, we also include
                // whatever local transformation this reference frame provides.
                let relative_transform = resolved_transform
                    .then_translate(snap_offset(state.parent_accumulated_scroll_offset, state.coordinate_system_relative_scale_offset.scale))
                    .to_transform()
                    .with_destination::<LayoutPixel>();

                let mut reset_cs_id = match info.transform_style {
                    TransformStyle::Preserve3D => !state.preserves_3d,
                    TransformStyle::Flat => state.preserves_3d,
                };

                // We reset the coordinate system upon either crossing the preserve-3d context boundary,
                // or simply a 3D transformation.
                if !reset_cs_id {
                    // Try to update our compatible coordinate system transform. If we cannot, start a new
                    // incompatible coordinate system.
                    match ScaleOffset::from_transform(&relative_transform) {
                        Some(ref scale_offset) => {
                            // We generally do not want to snap animated transforms as it causes jitter.
                            // However, we do want to snap the visual viewport offset when scrolling.
                            // This may still cause jitter when zooming, unfortunately.
                            let mut maybe_snapped = scale_offset.clone();
                            if let ReferenceFrameKind::Transform { should_snap: true, .. } = info.kind {
                                maybe_snapped.offset = snap_offset(
                                    scale_offset.offset,
                                    state.coordinate_system_relative_scale_offset.scale,
                                );
                            }
                            cs_scale_offset =
                                state.coordinate_system_relative_scale_offset.accumulate(&maybe_snapped);
                        }
                        None => reset_cs_id = true,
                    }
                }
                if reset_cs_id {
                    // If we break 2D axis alignment or have a perspective component, we need to start a
                    // new incompatible coordinate system with which we cannot share clips without masking.
                    let transform = relative_transform.then(
                        &state.coordinate_system_relative_scale_offset.to_transform()
                    );

                    // Push that new coordinate system and record the new id.
                    let coord_system = {
                        let parent_system = &coord_systems[state.current_coordinate_system_id.0 as usize];
                        let mut cur_transform = transform;
                        if parent_system.should_flatten {
                            cur_transform.flatten_z_output();
                        }
                        let world_transform = cur_transform.then(&parent_system.world_transform);
                        let determinant = world_transform.determinant();
                        self.invertible = determinant != 0.0 && !determinant.is_nan();

                        CoordinateSystem {
                            transform,
                            world_transform,
                            should_flatten: match (info.transform_style, info.kind) {
                                (TransformStyle::Flat, ReferenceFrameKind::Transform { .. }) => true,
                                (_, _) => false,
                            },
                            parent: Some(state.current_coordinate_system_id),
                        }
                    };
                    coordinate_system_id = CoordinateSystemId(coord_systems.len() as u32);
                    coord_systems.push(coord_system);
                }

                // Ensure that the current coordinate system ID is propagated to child
                // nodes, even if we encounter a node that is not invertible. This ensures
                // that the invariant in get_relative_transform is not violated.
                self.coordinate_system_id = coordinate_system_id;
                self.viewport_transform = cs_scale_offset;
                self.content_transform = cs_scale_offset;
            }
            _ => {
                // We calculate this here to avoid a double-borrow later.
                let sticky_offset = self.calculate_sticky_offset(
                    &state.nearest_scrolling_ancestor_offset,
                    &state.nearest_scrolling_ancestor_viewport,
                );

                // The transformation for the bounds of our viewport is the parent reference frame
                // transform, plus any accumulated scroll offset from our parents, plus any offset
                // provided by our own sticky positioning.
                let accumulated_offset = state.parent_accumulated_scroll_offset + sticky_offset;
                self.viewport_transform = state.coordinate_system_relative_scale_offset
                    .offset(snap_offset(accumulated_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());

                // The transformation for any content inside of us is the viewport transformation, plus
                // whatever scrolling offset we supply as well.
                let added_offset = accumulated_offset + self.scroll_offset();
                self.content_transform = state.coordinate_system_relative_scale_offset
                    .offset(snap_offset(added_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());

                if let SpatialNodeType::StickyFrame(ref mut info) = self.node_type {
                    info.current_offset = sticky_offset;
                }

                self.coordinate_system_id = state.current_coordinate_system_id;
            }
        }

        //TODO: remove the field entirely?
        self.transform_kind = if self.coordinate_system_id.0 == 0 {
            TransformedRectKind::AxisAligned
        } else {
            TransformedRectKind::Complex
        };
    }

    fn calculate_sticky_offset(
        &self,
        viewport_scroll_offset: &LayoutVector2D,
        viewport_rect: &LayoutRect,
    ) -> LayoutVector2D {
        let info = match self.node_type {
            SpatialNodeType::StickyFrame(ref info) => info,
            _ => return LayoutVector2D::zero(),
        };

        if info.margins.top.is_none() && info.margins.bottom.is_none() &&
            info.margins.left.is_none() && info.margins.right.is_none() {
            return LayoutVector2D::zero();
        }

        // The viewport and margins of the item establishes the maximum amount that it can
        // be offset in order to keep it on screen. Since we care about the relationship
        // between the scrolled content and unscrolled viewport we adjust the viewport's
        // position by the scroll offset in order to work with their relative positions on the
        // page.
        let mut sticky_rect = info.frame_rect.translate(*viewport_scroll_offset);

        let mut sticky_offset = LayoutVector2D::zero();
        if let Some(margin) = info.margins.top {
            let top_viewport_edge = viewport_rect.min.y + margin;
            if sticky_rect.min.y < top_viewport_edge {
                // If the sticky rect is positioned above the top edge of the viewport (plus margin)
                // we move it down so that it is fully inside the viewport.
                sticky_offset.y = top_viewport_edge - sticky_rect.min.y;
            } else if info.previously_applied_offset.y > 0.0 &&
                sticky_rect.min.y > top_viewport_edge {
                // However, if the sticky rect is positioned *below* the top edge of the viewport
                // and there is already some offset applied to the sticky rect's position, then
                // we need to move it up so that it remains at the correct position. This
                // makes sticky_offset.y negative and effectively reduces the amount of the
                // offset that was already applied. We limit the reduction so that it can, at most,
                // cancel out the already-applied offset, but should never end up adjusting the
                // position the other way.
                sticky_offset.y = top_viewport_edge - sticky_rect.min.y;
                sticky_offset.y = sticky_offset.y.max(-info.previously_applied_offset.y);
            }
        }

        // If we don't have a sticky-top offset (sticky_offset.y + info.previously_applied_offset.y
        // == 0), or if we have a previously-applied bottom offset (previously_applied_offset.y < 0)
        // then we check for handling the bottom margin case. Note that the "don't have a sticky-top
        // offset" case includes the case where we *had* a sticky-top offset but we reduced it to
        // zero in the above block.
        if sticky_offset.y + info.previously_applied_offset.y <= 0.0 {
            if let Some(margin) = info.margins.bottom {
                // If sticky_offset.y is nonzero that means we must have set it
                // in the sticky-top handling code above, so this item must have
                // both top and bottom sticky margins. We adjust the item's rect
                // by the top-sticky offset, and then combine any offset from
                // the bottom-sticky calculation into sticky_offset below.
                sticky_rect.min.y += sticky_offset.y;
                sticky_rect.max.y += sticky_offset.y;

                // Same as the above case, but inverted for bottom-sticky items. Here
                // we adjust items upwards, resulting in a negative sticky_offset.y,
                // or reduce the already-present upward adjustment, resulting in a positive
                // sticky_offset.y.
                let bottom_viewport_edge = viewport_rect.max.y - margin;
                if sticky_rect.max.y > bottom_viewport_edge {
                    sticky_offset.y += bottom_viewport_edge - sticky_rect.max.y;
                } else if info.previously_applied_offset.y < 0.0 &&
                    sticky_rect.max.y < bottom_viewport_edge {
                    sticky_offset.y += bottom_viewport_edge - sticky_rect.max.y;
                    sticky_offset.y = sticky_offset.y.min(-info.previously_applied_offset.y);
                }
            }
        }

        // Same as above, but for the x-axis.
        if let Some(margin) = info.margins.left {
            let left_viewport_edge = viewport_rect.min.x + margin;
            if sticky_rect.min.x < left_viewport_edge {
                sticky_offset.x = left_viewport_edge - sticky_rect.min.x;
            } else if info.previously_applied_offset.x > 0.0 &&
                sticky_rect.min.x > left_viewport_edge {
                sticky_offset.x = left_viewport_edge - sticky_rect.min.x;
                sticky_offset.x = sticky_offset.x.max(-info.previously_applied_offset.x);
            }
        }

        if sticky_offset.x + info.previously_applied_offset.x <= 0.0 {
            if let Some(margin) = info.margins.right {
                sticky_rect.min.x += sticky_offset.x;
                sticky_rect.max.x += sticky_offset.x;
                let right_viewport_edge = viewport_rect.max.x - margin;
                if sticky_rect.max.x > right_viewport_edge {
                    sticky_offset.x += right_viewport_edge - sticky_rect.max.x;
                } else if info.previously_applied_offset.x < 0.0 &&
                    sticky_rect.max.x < right_viewport_edge {
                    sticky_offset.x += right_viewport_edge - sticky_rect.max.x;
                    sticky_offset.x = sticky_offset.x.min(-info.previously_applied_offset.x);
                }
            }
        }

        // The total "sticky offset" (which is the sum that was already applied by
        // the calling code, stored in info.previously_applied_offset, and the extra amount we
        // computed as a result of scrolling, stored in sticky_offset) needs to be
        // clamped to the provided bounds.
        let clamp_adjusted = |value: f32, adjust: f32, bounds: &StickyOffsetBounds| {
            (value + adjust).max(bounds.min).min(bounds.max) - adjust
        };
        sticky_offset.y = clamp_adjusted(sticky_offset.y,
                                         info.previously_applied_offset.y,
                                         &info.vertical_offset_bounds);
        sticky_offset.x = clamp_adjusted(sticky_offset.x,
                                         info.previously_applied_offset.x,
                                         &info.horizontal_offset_bounds);

        sticky_offset
    }

    pub fn prepare_state_for_children(&self, state: &mut TransformUpdateState) {
        state.current_coordinate_system_id = self.coordinate_system_id;
        state.is_ancestor_or_self_zooming = self.is_async_zooming;
        state.invertible &= self.invertible;

        // The transformation we are passing is the transformation of the parent
        // reference frame and the offset is the accumulated offset of all the nodes
        // between us and the parent reference frame. If we are a reference frame,
        // we need to reset both these values.
        match self.node_type {
            SpatialNodeType::StickyFrame(ref info) => {
                // We don't translate the combined rect by the sticky offset, because sticky
                // offsets actually adjust the node position itself, whereas scroll offsets
                // only apply to contents inside the node.
                state.parent_accumulated_scroll_offset += info.current_offset;
                // We want nested sticky items to take into account the shift
                // we applied as well.
                state.nearest_scrolling_ancestor_offset += info.current_offset;
                state.preserves_3d = false;
                state.external_id = None;
                state.scroll_offset = info.current_offset;
            }
            SpatialNodeType::ScrollFrame(ref scrolling) => {
                state.parent_accumulated_scroll_offset += scrolling.offset;
                state.nearest_scrolling_ancestor_offset = scrolling.offset;
                state.nearest_scrolling_ancestor_viewport = scrolling.viewport_rect;
                state.preserves_3d = false;
                state.external_id = Some(scrolling.external_id);
                state.scroll_offset = scrolling.offset + scrolling.external_scroll_offset;
            }
            SpatialNodeType::ReferenceFrame(ref info) => {
                state.external_id = None;
                state.scroll_offset = LayoutVector2D::zero();
                state.preserves_3d = info.transform_style == TransformStyle::Preserve3D;
                state.parent_accumulated_scroll_offset = LayoutVector2D::zero();
                state.coordinate_system_relative_scale_offset = self.content_transform;
                let translation = -info.origin_in_parent_reference_frame;
                state.nearest_scrolling_ancestor_viewport =
                    state.nearest_scrolling_ancestor_viewport
                       .translate(translation);
            }
        }
    }

    pub fn scroll_offset(&self) -> LayoutVector2D {
        match self.node_type {
            SpatialNodeType::ScrollFrame(ref scrolling) => scrolling.offset,
            _ => LayoutVector2D::zero(),
        }
    }

    pub fn matches_external_id(&self, external_id: ExternalScrollId) -> bool {
        match self.node_type {
            SpatialNodeType::ScrollFrame(ref info) if info.external_id == external_id => true,
            _ => false,
        }
    }

    /// Returns true for ReferenceFrames whose source_transform is
    /// bound to the property binding id.
    pub fn is_transform_bound_to_property(&self, id: PropertyBindingId) -> bool {
        if let SpatialNodeType::ReferenceFrame(ref info) = self.node_type {
            if let PropertyBinding::Binding(key, _) = info.source_transform {
                id == key.id
            } else {
                false
            }
        } else {
            false
        }
    }
}
{'fraction_non_alphanumeric': 0.07572333732110417, 'fraction_numerical': 0.0026437443691198844, 'mean_word_length': 1.9596538007605893, 'pattern_counts': {'":': 0, '<': 58, '<?xml version=': 0, '>': 130, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 32, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7365774', 'n_tokens_mistral': 17996, 'n_tokens_neox': 16988, 'n_words': 5008}