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