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