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