summaryrefslogtreecommitdiffstats
path: root/gfx/wr/webrender/src/spatial_node.rs
diff options
context:
space:
mode:
Diffstat (limited to 'gfx/wr/webrender/src/spatial_node.rs')
-rw-r--r--gfx/wr/webrender/src/spatial_node.rs963
1 files changed, 963 insertions, 0 deletions
diff --git a/gfx/wr/webrender/src/spatial_node.rs b/gfx/wr/webrender/src/spatial_node.rs
new file mode 100644
index 0000000000..6df2b402e3
--- /dev/null
+++ b/gfx/wr/webrender/src/spatial_node.rs
@@ -0,0 +1,963 @@
+
+/* 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, ScrollClamping, ScrollLocation};
+use api::{TransformStyle, ScrollSensitivity, StickyOffsetBounds};
+use api::units::*;
+use crate::spatial_tree::{CoordinateSystem, CoordinateSystemId, SpatialNodeIndex, TransformUpdateState};
+use euclid::{Point2D, Vector2D, SideOffsets2D};
+use crate::scene::SceneProperties;
+use crate::util::{LayoutFastTransform, MatrixHelpers, ScaleOffset, TransformedRectKind, PointHelpers};
+
+#[derive(Clone, Debug)]
+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),
+}
+
+/// Contains information common among all types of SpatialTree nodes.
+#[derive(Clone, Debug)]
+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,
+}
+
+fn compute_offset_from(
+ mut current: Option<SpatialNodeIndex>,
+ external_id: ExternalScrollId,
+ previous_spatial_nodes: &[SpatialNode],
+) -> LayoutVector2D {
+ let mut offset = LayoutVector2D::zero();
+ while let Some(parent_index) = current {
+ let ancestor = &previous_spatial_nodes[parent_index.0 as usize];
+ match ancestor.node_type {
+ SpatialNodeType::ReferenceFrame(..) => {
+ // We don't want to scroll across reference frames.
+ break;
+ },
+ SpatialNodeType::ScrollFrame(ref info) => {
+ if info.external_id == external_id {
+ break;
+ }
+
+ // External scroll offsets are not propagated across
+ // reference frame boundaries, so undo them here.
+ offset += info.offset + info.external_scroll_offset;
+ },
+ SpatialNodeType::StickyFrame(ref info) => {
+ offset += info.current_offset;
+ },
+ }
+ current = ancestor.parent;
+ }
+ offset
+}
+
+/// Snap an offset to be incorporated into a transform, where the local space
+/// may be considered the world space. We convert from world space to device
+/// space using the global device pixel scale, 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>,
+ global_device_pixel_scale: DevicePixelScale,
+) -> Vector2D<f32, OffsetUnits> {
+ let world_offset = Point2D::new(offset.x * scale.x, offset.y * scale.y);
+ let snapped_device_offset = (world_offset * global_device_pixel_scale).snap();
+ let snapped_world_offset = snapped_device_offset / global_device_pixel_scale;
+ 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 new(
+ pipeline_id: PipelineId,
+ parent_index: Option<SpatialNodeIndex>,
+ node_type: SpatialNodeType,
+ ) -> Self {
+ SpatialNode {
+ viewport_transform: ScaleOffset::identity(),
+ content_transform: ScaleOffset::identity(),
+ snapping_transform: None,
+ coordinate_system_id: CoordinateSystemId(0),
+ transform_kind: TransformedRectKind::AxisAligned,
+ parent: parent_index,
+ children: Vec::new(),
+ pipeline_id,
+ node_type,
+ invertible: true,
+ is_async_zooming: false,
+ is_ancestor_or_self_zooming: false,
+ }
+ }
+
+ pub fn new_scroll_frame(
+ pipeline_id: PipelineId,
+ parent_index: SpatialNodeIndex,
+ external_id: ExternalScrollId,
+ frame_rect: &LayoutRect,
+ content_size: &LayoutSize,
+ scroll_sensitivity: ScrollSensitivity,
+ frame_kind: ScrollFrameKind,
+ external_scroll_offset: LayoutVector2D,
+ ) -> Self {
+ let node_type = SpatialNodeType::ScrollFrame(ScrollFrameInfo::new(
+ *frame_rect,
+ scroll_sensitivity,
+ LayoutSize::new(
+ (content_size.width - frame_rect.size.width).max(0.0),
+ (content_size.height - frame_rect.size.height).max(0.0)
+ ),
+ external_id,
+ frame_kind,
+ external_scroll_offset,
+ )
+ );
+
+ Self::new(pipeline_id, Some(parent_index), node_type)
+ }
+
+ 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,
+ ) -> Self {
+ let info = ReferenceFrameInfo {
+ transform_style,
+ source_transform,
+ kind,
+ origin_in_parent_reference_frame,
+ invertible: true,
+ };
+ Self::new(pipeline_id, parent_index, SpatialNodeType::ReferenceFrame(info))
+ }
+
+ pub fn new_sticky_frame(
+ parent_index: SpatialNodeIndex,
+ sticky_frame_info: StickyFrameInfo,
+ pipeline_id: PipelineId,
+ ) -> Self {
+ Self::new(pipeline_id, Some(parent_index), SpatialNodeType::StickyFrame(sticky_frame_info))
+ }
+
+ pub fn add_child(&mut self, child: SpatialNodeIndex) {
+ self.children.push(child);
+ }
+
+ pub fn apply_old_scrolling_state(&mut self, old_scroll_info: &ScrollFrameInfo) {
+ match self.node_type {
+ SpatialNodeType::ScrollFrame(ref mut scrolling) => {
+ *scrolling = scrolling.combine_with_old_scroll_info(old_scroll_info);
+ }
+ _ if old_scroll_info.offset != LayoutVector2D::zero() => {
+ warn!("Tried to scroll a non-scroll node.")
+ }
+ _ => {}
+ }
+ }
+
+ pub fn set_scroll_origin(&mut self, origin: &LayoutPoint, clamp: ScrollClamping) -> bool {
+ let scrolling = match self.node_type {
+ SpatialNodeType::ScrollFrame(ref mut scrolling) => scrolling,
+ _ => {
+ warn!("Tried to scroll a non-scroll node.");
+ return false;
+ }
+ };
+
+ let normalized_offset = match clamp {
+ ScrollClamping::ToContentBounds => {
+ let scrollable_size = scrolling.scrollable_size;
+ let scrollable_width = scrollable_size.width;
+ let scrollable_height = scrollable_size.height;
+
+ if scrollable_height <= 0. && scrollable_width <= 0. {
+ return false;
+ }
+
+ let origin = LayoutPoint::new(origin.x.max(0.0), origin.y.max(0.0));
+ LayoutVector2D::new(
+ (-origin.x).max(-scrollable_width).min(0.0),
+ (-origin.y).max(-scrollable_height).min(0.0),
+ )
+ }
+ ScrollClamping::NoClamping => LayoutPoint::zero() - *origin,
+ };
+
+ let new_offset = normalized_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: &mut TransformUpdateState,
+ coord_systems: &mut Vec<CoordinateSystem>,
+ global_device_pixel_scale: DevicePixelScale,
+ scene_properties: &SceneProperties,
+ previous_spatial_nodes: &[SpatialNode],
+ ) {
+ // 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, coord_systems, global_device_pixel_scale, scene_properties, previous_spatial_nodes);
+ //TODO: remove the field entirely?
+ self.transform_kind = if self.coordinate_system_id.0 == 0 {
+ TransformedRectKind::AxisAligned
+ } else {
+ TransformedRectKind::Complex
+ };
+
+ let is_parent_zooming = match self.parent {
+ Some(parent) => previous_spatial_nodes[parent.0 as usize].is_ancestor_or_self_zooming,
+ _ => false,
+ };
+ self.is_ancestor_or_self_zooming = self.is_async_zooming | is_parent_zooming;
+
+ // If this node is a reference frame, we check if it has a non-invertible matrix.
+ // For non-reference-frames we assume that they will produce only additional
+ // translations which should be invertible.
+ match self.node_type {
+ SpatialNodeType::ReferenceFrame(info) if !info.invertible => {
+ self.mark_uninvertible(state);
+ }
+ _ => self.invertible = true,
+ }
+ }
+
+ pub fn update_transform(
+ &mut self,
+ state: &mut TransformUpdateState,
+ coord_systems: &mut Vec<CoordinateSystem>,
+ global_device_pixel_scale: DevicePixelScale,
+ scene_properties: &SceneProperties,
+ previous_spatial_nodes: &[SpatialNode],
+ ) {
+ match self.node_type {
+ SpatialNodeType::ReferenceFrame(ref mut info) => {
+ let mut cs_scale_offset = ScaleOffset::identity();
+
+ if info.invertible {
+ // Resolve the transform against any property bindings.
+ let source_transform = LayoutFastTransform::from(
+ scene_properties.resolve_layout_transform(&info.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 scroll_offset = compute_offset_from(
+ self.parent,
+ external_id,
+ previous_spatial_nodes,
+ );
+
+ // 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 | ReferenceFrameKind::Zoom => 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, global_device_pixel_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.
+ // Therefore only snap the transform for Zoom reference frames. This may still
+ // cause jitter when zooming, unfortunately.
+ let mut maybe_snapped = scale_offset.clone();
+ if info.kind == ReferenceFrameKind::Zoom {
+ maybe_snapped.offset = snap_offset(
+ scale_offset.offset,
+ state.coordinate_system_relative_scale_offset.scale,
+ global_device_pixel_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();
+ info.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),
+ }
+ };
+ state.current_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 = state.current_coordinate_system_id;
+ self.viewport_transform = cs_scale_offset;
+ self.content_transform = cs_scale_offset;
+ self.invertible = info.invertible;
+ }
+ _ => {
+ // 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, global_device_pixel_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, global_device_pixel_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;
+ }
+ }
+ }
+
+ 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.origin.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.origin.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) {
+ if !self.invertible {
+ state.invertible = false;
+ return;
+ }
+
+ // 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;
+ }
+ 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;
+ }
+ SpatialNodeType::ReferenceFrame(ref info) => {
+ 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(info) if info.external_id == external_id => true,
+ _ => false,
+ }
+ }
+
+ /// Updates the snapping transform.
+ pub fn update_snapping(
+ &mut self,
+ parent: Option<&SpatialNode>,
+ ) {
+ // Reset in case of an early return.
+ self.snapping_transform = None;
+
+ // We need to incorporate the parent scale/offset with the child.
+ // If the parent does not have a scale/offset, then we know we are
+ // not 2d axis aligned and thus do not need to snap its children
+ // either.
+ let parent_scale_offset = match parent {
+ Some(parent) => {
+ match parent.snapping_transform {
+ Some(scale_offset) => scale_offset,
+ None => return,
+ }
+ },
+ _ => ScaleOffset::identity(),
+ };
+
+ let scale_offset = match self.node_type {
+ SpatialNodeType::ReferenceFrame(ref info) => {
+ match info.source_transform {
+ PropertyBinding::Value(ref value) => {
+ // We can only get a ScaleOffset if the transform is 2d axis
+ // aligned.
+ match ScaleOffset::from_transform(value) {
+ Some(scale_offset) => {
+ let origin_offset = info.origin_in_parent_reference_frame;
+ ScaleOffset::from_offset(origin_offset.to_untyped())
+ .accumulate(&scale_offset)
+ }
+ None => return,
+ }
+ }
+
+ // Assume animations start at the identity transform for snapping purposes.
+ // We still want to incorporate the reference frame offset however.
+ // TODO(aosmond): Is there a better known starting point?
+ PropertyBinding::Binding(..) => {
+ let origin_offset = info.origin_in_parent_reference_frame;
+ ScaleOffset::from_offset(origin_offset.to_untyped())
+ }
+ }
+ }
+ _ => ScaleOffset::identity(),
+ };
+
+ self.snapping_transform = Some(parent_scale_offset.accumulate(&scale_offset));
+ }
+
+ /// 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
+ }
+ }
+}
+
+/// Defines whether we have an implicit scroll frame for a pipeline root,
+/// or an explicitly defined scroll frame from the display list.
+#[derive(Copy, Clone, Debug)]
+pub enum ScrollFrameKind {
+ PipelineRoot {
+ is_root_pipeline: bool,
+ },
+ Explicit,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct ScrollFrameInfo {
+ /// The rectangle of the viewport of this scroll frame. This is important for
+ /// positioning of items inside child StickyFrames.
+ pub viewport_rect: LayoutRect,
+
+ pub scroll_sensitivity: ScrollSensitivity,
+
+ /// Amount that this ScrollFrame can scroll in both directions.
+ pub scrollable_size: LayoutSize,
+
+ /// An external id to identify this scroll frame to API clients. This
+ /// allows setting scroll positions via the API without relying on ClipsIds
+ /// which may change between frames.
+ pub external_id: ExternalScrollId,
+
+ /// Stores whether this is a scroll frame added implicitly by WR when adding
+ /// a pipeline (either the root or an iframe). We need to exclude these
+ /// when searching for scroll roots we care about for picture caching.
+ /// TODO(gw): I think we can actually completely remove the implicit
+ /// scroll frame being added by WR, and rely on the embedder
+ /// to define scroll frames. However, that involves API changes
+ /// so we will use this as a temporary hack!
+ pub frame_kind: ScrollFrameKind,
+
+ /// Amount that visual components attached to this scroll node have been
+ /// pre-scrolled in their local coordinates.
+ pub external_scroll_offset: LayoutVector2D,
+
+ /// The negated scroll offset of this scroll node. including the
+ /// pre-scrolled amount. If, for example, a scroll node was pre-scrolled
+ /// to y=10 (10 pixels down from the initial unscrolled position), then
+ /// `external_scroll_offset` would be (0,10), and this `offset` field would
+ /// be (0,-10). If WebRender is then asked to change the scroll position by
+ /// an additional 10 pixels (without changing the pre-scroll amount in the
+ /// display list), `external_scroll_offset` would remain at (0,10) and
+ /// `offset` would change to (0,-20).
+ pub offset: LayoutVector2D,
+}
+
+/// Manages scrolling offset.
+impl ScrollFrameInfo {
+ pub fn new(
+ viewport_rect: LayoutRect,
+ scroll_sensitivity: ScrollSensitivity,
+ scrollable_size: LayoutSize,
+ external_id: ExternalScrollId,
+ frame_kind: ScrollFrameKind,
+ external_scroll_offset: LayoutVector2D,
+ ) -> ScrollFrameInfo {
+ ScrollFrameInfo {
+ viewport_rect,
+ offset: -external_scroll_offset,
+ scroll_sensitivity,
+ scrollable_size,
+ external_id,
+ frame_kind,
+ external_scroll_offset,
+ }
+ }
+
+ pub fn sensitive_to_input_events(&self) -> bool {
+ match self.scroll_sensitivity {
+ ScrollSensitivity::ScriptAndInputEvents => true,
+ ScrollSensitivity::Script => false,
+ }
+ }
+
+ pub fn combine_with_old_scroll_info(
+ self,
+ old_scroll_info: &ScrollFrameInfo
+ ) -> ScrollFrameInfo {
+ ScrollFrameInfo {
+ viewport_rect: self.viewport_rect,
+ offset: old_scroll_info.offset,
+ scroll_sensitivity: self.scroll_sensitivity,
+ scrollable_size: self.scrollable_size,
+ external_id: self.external_id,
+ frame_kind: self.frame_kind,
+ external_scroll_offset: self.external_scroll_offset,
+ }
+ }
+}
+
+/// Contains information about reference frames.
+#[derive(Copy, Clone, Debug)]
+pub struct ReferenceFrameInfo {
+ /// The source transform and perspective matrices provided by the stacking context
+ /// that forms this reference frame. We maintain the property binding information
+ /// here so that we can resolve the animated transform and update the tree each
+ /// frame.
+ pub source_transform: PropertyBinding<LayoutTransform>,
+ pub transform_style: TransformStyle,
+ pub kind: ReferenceFrameKind,
+
+ /// The original, not including the transform and relative to the parent reference frame,
+ /// origin of this reference frame. This is already rolled into the `transform' property, but
+ /// we also store it here to properly transform the viewport for sticky positioning.
+ pub origin_in_parent_reference_frame: LayoutVector2D,
+
+ /// True if the resolved transform is invertible.
+ pub invertible: bool,
+}
+
+#[derive(Clone, Debug)]
+pub struct StickyFrameInfo {
+ pub frame_rect: LayoutRect,
+ pub margins: SideOffsets2D<Option<f32>, LayoutPixel>,
+ pub vertical_offset_bounds: StickyOffsetBounds,
+ pub horizontal_offset_bounds: StickyOffsetBounds,
+ pub previously_applied_offset: LayoutVector2D,
+ pub current_offset: LayoutVector2D,
+}
+
+impl StickyFrameInfo {
+ pub fn new(
+ frame_rect: LayoutRect,
+ margins: SideOffsets2D<Option<f32>, LayoutPixel>,
+ vertical_offset_bounds: StickyOffsetBounds,
+ horizontal_offset_bounds: StickyOffsetBounds,
+ previously_applied_offset: LayoutVector2D
+ ) -> StickyFrameInfo {
+ StickyFrameInfo {
+ frame_rect,
+ margins,
+ vertical_offset_bounds,
+ horizontal_offset_bounds,
+ previously_applied_offset,
+ current_offset: LayoutVector2D::zero(),
+ }
+ }
+}
+
+#[test]
+fn test_cst_perspective_relative_scroll() {
+ // Verify that when computing the offset from a perspective transform
+ // to a relative scroll node that any external scroll offset is
+ // ignored. This is because external scroll offsets are not
+ // propagated across reference frame boundaries.
+
+ // It's not currently possible to verify this with a wrench reftest,
+ // since wrench doesn't understand external scroll ids. When wrench
+ // supports this, we could also verify with a reftest.
+
+ use crate::spatial_tree::SpatialTree;
+ use euclid::approxeq::ApproxEq;
+
+ let mut cst = SpatialTree::new();
+ let pipeline_id = PipelineId::dummy();
+ let ext_scroll_id = ExternalScrollId(1, pipeline_id);
+ let transform = LayoutTransform::perspective(100.0);
+
+ let root = cst.add_reference_frame(
+ None,
+ TransformStyle::Flat,
+ PropertyBinding::Value(LayoutTransform::identity()),
+ ReferenceFrameKind::Transform,
+ LayoutVector2D::zero(),
+ pipeline_id,
+ );
+
+ let scroll_frame_1 = cst.add_scroll_frame(
+ root,
+ ext_scroll_id,
+ pipeline_id,
+ &LayoutRect::new(LayoutPoint::zero(), LayoutSize::new(100.0, 100.0)),
+ &LayoutSize::new(100.0, 500.0),
+ ScrollSensitivity::Script,
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::zero(),
+ );
+
+ let scroll_frame_2 = cst.add_scroll_frame(
+ scroll_frame_1,
+ ExternalScrollId(2, pipeline_id),
+ pipeline_id,
+ &LayoutRect::new(LayoutPoint::zero(), LayoutSize::new(100.0, 100.0)),
+ &LayoutSize::new(100.0, 500.0),
+ ScrollSensitivity::Script,
+ ScrollFrameKind::Explicit,
+ LayoutVector2D::new(0.0, 50.0),
+ );
+
+ let ref_frame = cst.add_reference_frame(
+ Some(scroll_frame_2),
+ TransformStyle::Preserve3D,
+ PropertyBinding::Value(transform),
+ ReferenceFrameKind::Perspective {
+ scrolling_relative_to: Some(ext_scroll_id),
+ },
+ LayoutVector2D::zero(),
+ pipeline_id,
+ );
+
+ cst.update_tree(WorldPoint::zero(), DevicePixelScale::new(1.0), &SceneProperties::new());
+
+ let scroll_offset = compute_offset_from(
+ cst.spatial_nodes[ref_frame.0 as usize].parent,
+ ext_scroll_id,
+ &cst.spatial_nodes,
+ );
+
+ assert!(scroll_offset.x.approx_eq(&0.0));
+ assert!(scroll_offset.y.approx_eq(&0.0));
+}