Skip to main content

webrender/
spatial_node.rs

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