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