Source code

Revision control

Other Tools

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