/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use api::{ExternalScrollId, PipelineId, PropertyBinding, PropertyBindingId, ReferenceFrameKind}; use api::{APZScrollGeneration, HasScrollLinkedEffect, SampledScrollOffset}; use api::{TransformStyle, StickyOffsetBounds}; use api::units::*; usecrate::spatial_tree::{CoordinateSystem, SpatialNodeIndex, TransformUpdateState}; usecrate::spatial_tree::CoordinateSystemId; use euclid::{Vector2D, SideOffsets2D}; usecrate::scene::SceneProperties; usecrate::util::{LayoutFastTransform, MatrixHelpers, ScaleOffset, TransformedRectKind}; usecrate::util::VectorHelpers;
/// Defines the content of a spatial node. If the values in the descriptor don't /// change, that means the rest of the fields in a spatial node will end up with /// the same result #[derive(Clone, PartialEq)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pubstruct SpatialNodeDescriptor { /// The type of this node and any data associated with that node type. pub node_type: SpatialNodeType,
/// Pipeline that this layer belongs to pub pipeline_id: PipelineId,
}
#[derive(Clone, PartialEq)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pubenum SpatialNodeType { /// A special kind of node that adjusts its position based on the position /// of its parent node and a given set of sticky positioning offset bounds. /// Sticky positioned is described in the CSS Positioned Layout Module Level 3 here: /// https://www.w3.org/TR/css-position-3/#sticky-pos
StickyFrame(StickyFrameInfo),
/// Transforms it's content, but doesn't clip it. Can also be adjusted /// by scroll events or setting scroll offsets.
ScrollFrame(ScrollFrameInfo),
/// A reference frame establishes a new coordinate space in the tree.
ReferenceFrame(ReferenceFrameInfo),
}
/// Information about a spatial node that can be queried during either scene of /// frame building. pubstruct SpatialNodeInfo<'a> { /// The type of this node and any data associated with that node type. pub node_type: &'a SpatialNodeType,
/// Parent spatial node. If this is None, we are the root node. pub parent: Option<SpatialNodeIndex>,
}
/// Scene building specific representation of a spatial node, which is a much /// lighter subset of a full spatial node constructed and used for frame building #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] #[derive(PartialEq)] pubstruct SceneSpatialNode { /// Parent spatial node. If this is None, we are the root node. pub parent: Option<SpatialNodeIndex>,
/// Descriptor describing how this spatial node behaves pub descriptor: SpatialNodeDescriptor,
/// If true, this spatial node is known to exist in the root coordinate /// system in all cases (it has no animated or complex transforms) pub is_root_coord_system: bool,
}
/// Contains information common among all types of SpatialTree nodes. #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pubstruct SpatialNode { /// The scale/offset of the viewport for this spatial node, relative to the /// coordinate system. Includes any accumulated scrolling offsets from nodes /// between our reference frame and this node. pub viewport_transform: ScaleOffset,
/// Content scale/offset relative to the coordinate system. pub content_transform: ScaleOffset,
/// The axis-aligned coordinate system id of this node. pub coordinate_system_id: CoordinateSystemId,
/// The current transform kind of this node. pub transform_kind: TransformedRectKind,
/// Pipeline that this layer belongs to pub pipeline_id: PipelineId,
/// Parent layer. If this is None, we are the root node. pub parent: Option<SpatialNodeIndex>,
/// The type of this node and any data associated with that node type. pub node_type: SpatialNodeType,
/// True if this node is transformed by an invertible transform. If not, display items /// transformed by this node will not be displayed and display items not transformed by this /// node will not be clipped by clips that are transformed by this node. pub invertible: bool,
/// Whether this specific node is currently being async zoomed. /// Should be set when a SetIsTransformAsyncZooming FrameMsg is received. pub is_async_zooming: bool,
/// Whether this node or any of its ancestors is being pinch zoomed. /// This is calculated in update(). This will be used to decide whether /// to override corresponding picture's raster space as an optimisation. pub is_ancestor_or_self_zooming: bool,
}
/// Snap an offset to be incorporated into a transform, where the local space /// may be considered the world space. We assume raster scale is 1.0, which /// may not always be correct if there are intermediate surfaces used, however /// those are either cases where snapping is not important (e.g. has perspective /// or is not axis aligned), or an edge case (e.g. SVG filters) which we can accept /// imperfection for now. fn snap_offset<OffsetUnits, ScaleUnits>(
offset: Vector2D<f32, OffsetUnits>,
scale: Vector2D<f32, ScaleUnits>,
) -> Vector2D<f32, OffsetUnits> { // Snap the accumulated (composite) offset of a spatial node to the device // pixel grid. Per-prim rect snapping at frame time happens in each surface's // raster space (`SpaceSnapper`), which excludes a scroll/sticky slice's own // composite offset; that offset must still be snapped here so the slice // composites on an integer boundary. Scroll slices already land integer // (their sampled offset is pre-snapped); sticky slices rely on this. let snapped_x = (offset.x * scale.x).round(); let snapped_y = (offset.y * scale.y).round();
Vector2D::new( if scale.x != 0.0 { snapped_x / scale.x } else { offset.x }, if scale.y != 0.0 { snapped_y / scale.y } else { offset.y },
)
}
let scrolling = matchself.node_type {
SpatialNodeType::ScrollFrame(refmut scrolling) => scrolling,
_ => {
warn!("Tried to scroll a non-scroll node."); returnfalse;
}
};
for element in offsets.iter_mut() {
element.offset = -element.offset - scrolling.external_scroll_offset;
// Once the final scroll offset (APZ + content external offset) is // calculated, we need to snap it to a device pixel. We already snap // the final transforms in `update_transform`. However, we need to // ensure the offsets are also snapped so that if the offset is used // in a nested sticky frame, it is pre-snapped.
element.offset = element.offset.snap();
}
// If any of our parents was not rendered, we are not rendered either and can just // quit here. if !state.invertible { self.mark_uninvertible(state); return;
}
// Resolve the transform against any property bindings. let source_transform = { let source_transform = scene_properties.resolve_layout_transform(&info.source_transform); iflet ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } = info.kind {
assert!(source_transform.is_2d_scale_translation(), "Reference frame was marked as only having 2d scale or translation");
}
LayoutFastTransform::from(source_transform)
};
// Do a change-basis operation on the perspective matrix using // the scroll offset. let source_transform = match info.kind {
ReferenceFrameKind::Perspective { scrolling_relative_to: Some(external_id) } => { letmut scroll_offset = LayoutVector2D::zero();
for parent_state in state_stack.iter().rev() { iflet Some(parent_external_id) = parent_state.external_id { if parent_external_id == external_id { break;
}
}
scroll_offset += parent_state.scroll_offset;
}
// Do a change-basis operation on the // perspective matrix using the scroll offset.
source_transform
.pre_translate(scroll_offset)
.then_translate(-scroll_offset)
}
ReferenceFrameKind::Perspective { scrolling_relative_to: None } |
ReferenceFrameKind::Transform { .. } => source_transform,
};
// An axis-aligned reference frame composes into a `ScaleOffset`, // so its accumulated device offset is snapped below (the // `should_snap` round on `cs_scale_offset`); the origin is used // as-is here. A non-axis-aligned frame (skew / rotation / // perspective) doesn't compose into a `ScaleOffset`, so that // path can't reach it, and the frame-time rect pass can't either // (`SpaceSnapper` won't snap across a non-axis-aligned frame), so // snap the origin's device position here instead - otherwise a // fractional origin shifts all content below it. let parent_origin = match info.source_transform {
PropertyBinding::Value(ref value) if ScaleOffset::from_transform(value).is_none() =>
{
snap_offset(
info.origin_in_parent_reference_frame,
state.coordinate_system_relative_scale_offset.scale,
)
}
_ => info.origin_in_parent_reference_frame,
};
let resolved_transform =
LayoutFastTransform::with_vector(parent_origin)
.pre_transform(&source_transform);
// The transformation for this viewport in world coordinates is the transformation for // our parent reference frame, plus any accumulated scrolling offsets from nodes // between our reference frame and this node. Finally, we also include // whatever local transformation this reference frame provides. let relative_transform = resolved_transform
.then_translate(snap_offset(state.parent_accumulated_scroll_offset, state.coordinate_system_relative_scale_offset.scale))
.to_transform()
.with_destination::<LayoutPixel>();
// We reset the coordinate system upon either crossing the preserve-3d context boundary, // or simply a 3D transformation. if !reset_cs_id { // Try to update our compatible coordinate system transform. If we cannot, start a new // incompatible coordinate system. match ScaleOffset::from_transform(&relative_transform) {
Some(ref scale_offset) => { // Compose with the accumulated parent transform first, // then snap the *accumulated* device offset for // `should_snap` frames. Snapping the local offset before // composing (as this used to) ignores a fractional // ancestor transform and leaves the frame on a sub-pixel // boundary; see bug 1580534. The composed offset is already // in device space, so round it directly — `snap_offset` // would re-apply the accumulated scale and mis-round under a // non-unit-scale ancestor (e.g. `transform: scale(0.5)`, // bug 637852). We generally do not snap animated transforms // as it causes jitter, but we do want to snap the visual // viewport offset when scrolling.
cs_scale_offset = scale_offset.then(&state.coordinate_system_relative_scale_offset); iflet ReferenceFrameKind::Transform { should_snap: true, .. } = info.kind {
cs_scale_offset.offset = cs_scale_offset.offset.round();
}
}
None => reset_cs_id = true,
}
} if reset_cs_id { // If we break 2D axis alignment or have a perspective component, we need to start a // new incompatible coordinate system with which we cannot share clips without masking. let transform = relative_transform.then(
&state.coordinate_system_relative_scale_offset.to_transform()
);
// Push that new coordinate system and record the new id. let coord_system = { let parent_system = &coord_systems[state.current_coordinate_system_id.0as usize]; letmut cur_transform = transform; if parent_system.should_flatten {
cur_transform.flatten_z_output();
} let world_transform = cur_transform.then(&parent_system.world_transform); let determinant = world_transform.determinant(); self.invertible = determinant != 0.0 && !determinant.is_nan();
// Ensure that the current coordinate system ID is propagated to child // nodes, even if we encounter a node that is not invertible. This ensures // that the invariant in get_relative_transform is not violated. self.coordinate_system_id = coordinate_system_id; self.viewport_transform = cs_scale_offset; self.content_transform = cs_scale_offset;
}
SpatialNodeType::StickyFrame(refmut info) => { let animated_offset = iflet Some(transform_binding) = info.transform { let transform = scene_properties.resolve_layout_transform(&transform_binding); match ScaleOffset::from_transform(&transform) {
Some(ref scale_offset) => {
debug_assert!(scale_offset.scale == Vector2D::new(1.0, 1.0), "Can only animate a translation on sticky elements");
LayoutVector2D::from_untyped(scale_offset.offset)
}
None => {
debug_assert!(false, "Can only animate a translation on sticky elements");
LayoutVector2D::zero()
}
}
} else {
LayoutVector2D::zero()
};
let sticky_offset = Self::calculate_sticky_offset(
&state.nearest_scrolling_ancestor_offset,
&state.nearest_scrolling_ancestor_viewport,
info,
);
// The transformation for the bounds of our viewport is the parent reference frame // transform, plus any accumulated scroll offset from our parents, plus any offset // provided by our own sticky positioning. let accumulated_offset = state.parent_accumulated_scroll_offset + sticky_offset + animated_offset; self.viewport_transform = state.coordinate_system_relative_scale_offset
.pre_offset(snap_offset(accumulated_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped()); self.content_transform = self.viewport_transform;
self.coordinate_system_id = state.current_coordinate_system_id;
}
SpatialNodeType::ScrollFrame(_) => { // The transformation for the bounds of our viewport is the parent reference frame // transform, plus any accumulated scroll offset from our parents. let accumulated_offset = state.parent_accumulated_scroll_offset; self.viewport_transform = state.coordinate_system_relative_scale_offset
.pre_offset(snap_offset(accumulated_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());
// The transformation for any content inside of us is the viewport transformation, plus // whatever scrolling offset we supply as well. let added_offset = accumulated_offset + self.scroll_offset(); self.content_transform = state.coordinate_system_relative_scale_offset
.pre_offset(snap_offset(added_offset, state.coordinate_system_relative_scale_offset.scale).to_untyped());
// The viewport and margins of the item establishes the maximum amount that it can // be offset in order to keep it on screen. Since we care about the relationship // between the scrolled content and unscrolled viewport we adjust the viewport's // position by the scroll offset in order to work with their relative positions on the // page. `frame_rect` is the item's natural (unstuck) position: the display-list // builder removed the offset layout had already applied, so we compute the full // sticky offset here rather than a delta on top of a pre-applied amount. letmut sticky_rect = info.frame_rect.translate(*viewport_scroll_offset);
letmut sticky_offset = LayoutVector2D::zero(); iflet Some(margin) = info.margins.top { let top_viewport_edge = viewport_rect.min.y + margin; if sticky_rect.min.y < top_viewport_edge { // If the sticky rect is positioned above the top edge of the viewport (plus margin) // we move it down so that it is fully inside the viewport.
sticky_offset.y = top_viewport_edge - sticky_rect.min.y;
}
}
// If we don't have a sticky-top offset, check for handling the bottom margin case. if sticky_offset.y <= 0.0 { iflet Some(margin) = info.margins.bottom { // If sticky_offset.y is nonzero that means we must have set it // in the sticky-top handling code above, so this item must have // both top and bottom sticky margins. We adjust the item's rect // by the top-sticky offset, and then combine any offset from // the bottom-sticky calculation into sticky_offset below.
sticky_rect.min.y += sticky_offset.y;
sticky_rect.max.y += sticky_offset.y;
// Same as the above case, but inverted for bottom-sticky items. Here // we adjust items upwards, resulting in a negative sticky_offset.y. let bottom_viewport_edge = viewport_rect.max.y - margin; if sticky_rect.max.y > bottom_viewport_edge {
sticky_offset.y += bottom_viewport_edge - sticky_rect.max.y;
}
}
}
// Same as above, but for the x-axis. iflet Some(margin) = info.margins.left { let left_viewport_edge = viewport_rect.min.x + margin; if sticky_rect.min.x < left_viewport_edge {
sticky_offset.x = left_viewport_edge - sticky_rect.min.x;
}
}
// Clamp the sticky offset to the provided bounds, which describe how far // the item can travel from its natural position. let clamp = |value: f32, bounds: &StickyOffsetBounds| {
value.max(bounds.min).min(bounds.max)
};
sticky_offset.y = clamp(sticky_offset.y, &info.vertical_offset_bounds);
sticky_offset.x = clamp(sticky_offset.x, &info.horizontal_offset_bounds);
// The transformation we are passing is the transformation of the parent // reference frame and the offset is the accumulated offset of all the nodes // between us and the parent reference frame. If we are a reference frame, // we need to reset both these values. matchself.node_type {
SpatialNodeType::StickyFrame(ref info) => { // We don't translate the combined rect by the sticky offset, because sticky // offsets actually adjust the node position itself, whereas scroll offsets // only apply to contents inside the node.
state.parent_accumulated_scroll_offset += info.current_offset; // We want nested sticky items to take into account the shift // we applied as well.
state.nearest_scrolling_ancestor_offset += info.current_offset;
state.preserves_3d = false;
state.external_id = None;
state.scroll_offset = info.current_offset;
}
SpatialNodeType::ScrollFrame(ref scrolling) => {
state.parent_accumulated_scroll_offset += scrolling.offset();
state.nearest_scrolling_ancestor_offset = scrolling.offset();
state.nearest_scrolling_ancestor_viewport = scrolling.viewport_rect;
state.preserves_3d = false;
state.external_id = Some(scrolling.external_id);
state.scroll_offset = scrolling.offset() + scrolling.external_scroll_offset;
}
SpatialNodeType::ReferenceFrame(ref info) => {
state.external_id = None;
state.scroll_offset = LayoutVector2D::zero();
state.preserves_3d = info.transform_style == TransformStyle::Preserve3D;
state.parent_accumulated_scroll_offset = LayoutVector2D::zero();
state.coordinate_system_relative_scale_offset = self.content_transform; let translation = -info.origin_in_parent_reference_frame;
state.nearest_scrolling_ancestor_viewport =
state.nearest_scrolling_ancestor_viewport
.translate(translation);
}
}
}
/// Returns true for ReferenceFrames whose source_transform is /// bound to the property binding id. pubfn is_transform_bound_to_property(&self, id: PropertyBindingId) -> bool { iflet SpatialNodeType::ReferenceFrame(ref info) = self.node_type { iflet PropertyBinding::Binding(key, _) = info.source_transform {
id == key.id
} else { false
}
} else { false
}
}
}
/// Defines whether we have an implicit scroll frame for a pipeline root, /// or an explicitly defined scroll frame from the display list. #[derive(Copy, Clone, Debug, PartialEq)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pubenum ScrollFrameKind {
PipelineRoot {
is_root_pipeline: bool,
},
Explicit,
}
#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pubstruct ScrollFrameInfo { /// The rectangle of the viewport of this scroll frame. This is important for /// positioning of items inside child StickyFrames. pub viewport_rect: LayoutRect,
/// Amount that this ScrollFrame can scroll in both directions. pub scrollable_size: LayoutSize,
/// An external id to identify this scroll frame to API clients. This /// allows setting scroll positions via the API without relying on ClipsIds /// which may change between frames. pub external_id: ExternalScrollId,
/// Stores whether this is a scroll frame added implicitly by WR when adding /// a pipeline (either the root or an iframe). We need to exclude these /// when searching for scroll roots we care about for picture caching. /// TODO(gw): I think we can actually completely remove the implicit /// scroll frame being added by WR, and rely on the embedder /// to define scroll frames. However, that involves API changes /// so we will use this as a temporary hack! pub frame_kind: ScrollFrameKind,
/// Amount that visual components attached to this scroll node have been /// pre-scrolled in their local coordinates. pub external_scroll_offset: LayoutVector2D,
/// A set of a pair of negated scroll offset and scroll generation of this /// scroll node. The negated scroll offset is including the pre-scrolled /// amount. If, for example, a scroll node was pre-scrolled to y=10 (10 /// pixels down from the initial unscrolled position), then /// `external_scroll_offset` would be (0,10), and this `offset` field would /// be (0,-10). If WebRender is then asked to change the scroll position by /// an additional 10 pixels (without changing the pre-scroll amount in the /// display list), `external_scroll_offset` would remain at (0,10) and /// `offset` would change to (0,-20). pub offsets: Vec<SampledScrollOffset>,
/// The generation of the external_scroll_offset. /// This is used to pick up the most appropriate scroll offset sampled /// off the main thread. pub offset_generation: APZScrollGeneration,
/// Whether the document containing this scroll frame has any scroll-linked /// effect or not. pub has_scroll_linked_effect: HasScrollLinkedEffect,
}
/// Manages scrolling offset. impl ScrollFrameInfo { pubfn new(
viewport_rect: LayoutRect,
scrollable_size: LayoutSize,
external_id: ExternalScrollId,
frame_kind: ScrollFrameKind,
external_scroll_offset: LayoutVector2D,
offset_generation: APZScrollGeneration,
has_scroll_linked_effect: HasScrollLinkedEffect,
) -> ScrollFrameInfo {
ScrollFrameInfo {
viewport_rect,
scrollable_size,
external_id,
frame_kind,
external_scroll_offset,
offsets: vec![SampledScrollOffset{ // If this scroll frame is a newly created one, using // `external_scroll_offset` and `offset_generation` is correct. // If this scroll frame is a result of updating an existing // scroll frame and if there have already been sampled async // scroll offsets by APZ, then these offsets will be replaced in // SpatialTree::set_scroll_offsets via a // RenderBackend::update_document call.
offset: -external_scroll_offset,
generation: offset_generation.clone(),
}],
offset_generation,
has_scroll_linked_effect,
}
}
pubfn offset(&self) -> LayoutVector2D {
debug_assert!(self.offsets.len() > 0, "There should be at least one sampled offset!");
ifself.has_scroll_linked_effect == HasScrollLinkedEffect::No { // If there's no scroll-linked effect, use the one-frame delay offset. returnself.offsets.first().map_or(LayoutVector2D::zero(), |sampled| sampled.offset);
}
matchself.offsets.iter().find(|sampled| sampled.generation == self.offset_generation) { // If we found an offset having the same generation, use it.
Some(sampled) => sampled.offset, // If we don't have any offset having the same generation, i.e. // the generation of this scroll frame is behind sampled offsets, // use the first queued sampled offset.
_ => self.offsets.first().map_or(LayoutVector2D::zero(), |sampled| sampled.offset),
}
}
}
/// Contains information about reference frames. #[derive(Copy, Clone, Debug, PartialEq)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pubstruct ReferenceFrameInfo { /// The source transform and perspective matrices provided by the stacking context /// that forms this reference frame. We maintain the property binding information /// here so that we can resolve the animated transform and update the tree each /// frame. pub source_transform: PropertyBinding<LayoutTransform>, pub transform_style: TransformStyle, pub kind: ReferenceFrameKind,
/// The original, not including the transform and relative to the parent reference frame, /// origin of this reference frame. This is already rolled into the `transform' property, but /// we also store it here to properly transform the viewport for sticky positioning. pub origin_in_parent_reference_frame: LayoutVector2D,
/// True if this is the root reference frame for a given pipeline. This is only used /// by the hit-test code, perhaps we can change the interface to not require this. pub is_pipeline_root: bool,
}
#[test] fn test_cst_perspective_relative_scroll() { // Verify that when computing the offset from a perspective transform // to a relative scroll node that any external scroll offset is // ignored. This is because external scroll offsets are not // propagated across reference frame boundaries.
// It's not currently possible to verify this with a wrench reftest, // since wrench doesn't understand external scroll ids. When wrench // supports this, we could also verify with a reftest.
usecrate::spatial_tree::{SceneSpatialTree, SpatialTree}; use euclid::Angle;
letmut cst = SceneSpatialTree::new(); let pipeline_id = PipelineId::dummy(); let ext_scroll_id = ExternalScrollId(1, pipeline_id); let transform = LayoutTransform::rotation(0.0, 0.0, 1.0, Angle::degrees(45.0));
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.