fune/gfx/webrender/src/spatial_node.rs
2018-11-21 12:29:04 +00:00

673 lines
29 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use api::{ExternalScrollId, LayoutPixel, LayoutPoint, LayoutRect, LayoutSize, LayoutTransform};
use api::{LayoutVector2D, PipelineId, PropertyBinding, ScrollClamping, ScrollLocation};
use api::{ScrollSensitivity, StickyOffsetBounds};
use clip_scroll_tree::{CoordinateSystem, CoordinateSystemId, SpatialNodeIndex, TransformUpdateState};
use euclid::SideOffsets2D;
use gpu_types::TransformPalette;
use scene::SceneProperties;
use util::{LayoutFastTransform, LayoutToWorldFastTransform, ScaleOffset, TransformedRectKind};
#[derive(Clone, Debug)]
pub enum 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),
}
/// Contains information common among all types of ClipScrollTree nodes.
#[derive(Clone, Debug)]
pub struct SpatialNode {
/// 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. For reference frames, we also include
/// whatever local transformation this reference frame provides.
pub world_viewport_transform: LayoutToWorldFastTransform,
/// World transform for content transformed by this node.
pub world_content_transform: LayoutToWorldFastTransform,
/// The current transform kind of world_content_transform.
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>,
/// Child layers
pub children: Vec<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,
/// The axis-aligned coordinate system id of this node.
pub coordinate_system_id: CoordinateSystemId,
/// The transformation from the coordinate system which established our compatible coordinate
/// system (same coordinate system id) and us. This can change via scroll offsets and via new
/// reference frame transforms.
pub coordinate_system_relative_scale_offset: ScaleOffset,
}
impl SpatialNode {
pub fn new(
pipeline_id: PipelineId,
parent_index: Option<SpatialNodeIndex>,
node_type: SpatialNodeType,
) -> Self {
SpatialNode {
world_viewport_transform: LayoutToWorldFastTransform::identity(),
world_content_transform: LayoutToWorldFastTransform::identity(),
transform_kind: TransformedRectKind::AxisAligned,
parent: parent_index,
children: Vec::new(),
pipeline_id,
node_type,
invertible: true,
coordinate_system_id: CoordinateSystemId(0),
coordinate_system_relative_scale_offset: ScaleOffset::identity(),
}
}
pub fn new_scroll_frame(
pipeline_id: PipelineId,
parent_index: SpatialNodeIndex,
external_id: Option<ExternalScrollId>,
frame_rect: &LayoutRect,
content_size: &LayoutSize,
scroll_sensitivity: ScrollSensitivity,
) -> Self {
let node_type = SpatialNodeType::ScrollFrame(ScrollFrameInfo::new(
*frame_rect,
scroll_sensitivity,
LayoutSize::new(
(content_size.width - frame_rect.size.width).max(0.0),
(content_size.height - frame_rect.size.height).max(0.0)
),
external_id,
)
);
Self::new(pipeline_id, Some(parent_index), node_type)
}
pub fn new_reference_frame(
parent_index: Option<SpatialNodeIndex>,
source_transform: Option<PropertyBinding<LayoutTransform>>,
source_perspective: Option<LayoutTransform>,
origin_in_parent_reference_frame: LayoutVector2D,
pipeline_id: PipelineId,
) -> Self {
let identity = LayoutTransform::identity();
let source_perspective = source_perspective.map_or_else(
LayoutFastTransform::identity, |perspective| perspective.into());
let info = ReferenceFrameInfo {
source_transform: source_transform.unwrap_or(PropertyBinding::Value(identity)),
source_perspective,
origin_in_parent_reference_frame,
invertible: true,
};
Self::new(pipeline_id, parent_index, SpatialNodeType:: ReferenceFrame(info))
}
pub fn new_sticky_frame(
parent_index: SpatialNodeIndex,
sticky_frame_info: StickyFrameInfo,
pipeline_id: PipelineId,
) -> Self {
Self::new(pipeline_id, Some(parent_index), SpatialNodeType::StickyFrame(sticky_frame_info))
}
pub fn add_child(&mut self, child: SpatialNodeIndex) {
self.children.push(child);
}
pub fn apply_old_scrolling_state(&mut self, old_scroll_info: &ScrollFrameInfo) {
match self.node_type {
SpatialNodeType::ScrollFrame(ref mut scrolling) => {
*scrolling = scrolling.combine_with_old_scroll_info(old_scroll_info);
}
_ if old_scroll_info.offset != LayoutVector2D::zero() => {
warn!("Tried to scroll a non-scroll node.")
}
_ => {}
}
}
pub fn set_scroll_origin(&mut self, origin: &LayoutPoint, clamp: ScrollClamping) -> bool {
let scrollable_size = self.scrollable_size();
let scrollable_width = scrollable_size.width;
let scrollable_height = scrollable_size.height;
let scrolling = match self.node_type {
SpatialNodeType::ScrollFrame(ref mut scrolling) => scrolling,
_ => {
warn!("Tried to scroll a non-scroll node.");
return false;
}
};
let new_offset = match clamp {
ScrollClamping::ToContentBounds => {
if scrollable_height <= 0. && scrollable_width <= 0. {
return false;
}
let origin = LayoutPoint::new(origin.x.max(0.0), origin.y.max(0.0));
LayoutVector2D::new(
(-origin.x).max(-scrollable_width).min(0.0).round(),
(-origin.y).max(-scrollable_height).min(0.0).round(),
)
}
ScrollClamping::NoClamping => LayoutPoint::zero() - *origin,
};
if new_offset == scrolling.offset {
return false;
}
scrolling.offset = new_offset;
true
}
pub fn mark_uninvertible(
&mut self,
state: &TransformUpdateState,
) {
self.invertible = false;
self.coordinate_system_id = state.current_coordinate_system_id;
self.world_content_transform = LayoutToWorldFastTransform::identity();
self.world_viewport_transform = LayoutToWorldFastTransform::identity();
}
pub fn push_gpu_data(
&mut self,
transform_palette: &mut TransformPalette,
node_index: SpatialNodeIndex,
) {
if self.invertible {
transform_palette.set_world_transform(
node_index,
self.world_content_transform
.to_transform()
.into_owned()
);
}
}
pub fn update(
&mut self,
state: &mut TransformUpdateState,
coord_systems: &mut Vec<CoordinateSystem>,
scene_properties: &SceneProperties,
) {
// 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;
}
self.update_transform(state, coord_systems, scene_properties);
self.transform_kind = self.world_content_transform.kind();
// If this node is a reference frame, we check if it has a non-invertible matrix.
// For non-reference-frames we assume that they will produce only additional
// translations which should be invertible.
match self.node_type {
SpatialNodeType::ReferenceFrame(info) if !info.invertible => {
self.mark_uninvertible(state);
return;
}
_ => self.invertible = true,
}
}
pub fn update_transform(
&mut self,
state: &mut TransformUpdateState,
coord_systems: &mut Vec<CoordinateSystem>,
scene_properties: &SceneProperties,
) {
match self.node_type {
SpatialNodeType::ReferenceFrame(ref mut info) => {
// Resolve the transform against any property bindings.
let source_transform = scene_properties.resolve_layout_transform(&info.source_transform);
// Do a change-basis operation on the perspective matrix using
// the scroll offset.
let scrolled_perspective = info.source_perspective
.pre_translate(&state.parent_accumulated_scroll_offset)
.post_translate(-state.parent_accumulated_scroll_offset);
let resolved_transform =
LayoutFastTransform::with_vector(info.origin_in_parent_reference_frame)
.pre_mul(&source_transform.into())
.pre_mul(&scrolled_perspective);
// 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
.post_translate(state.parent_accumulated_scroll_offset)
.to_transform()
.with_destination::<LayoutPixel>();
self.world_viewport_transform =
state.parent_reference_frame_transform.pre_mul(&relative_transform.into());
self.world_content_transform = self.world_viewport_transform;
info.invertible = self.world_viewport_transform.is_invertible();
if info.invertible {
// 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) => {
self.coordinate_system_relative_scale_offset =
state.coordinate_system_relative_scale_offset.accumulate(scale_offset);
}
None => {
// 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.
self.coordinate_system_relative_scale_offset = ScaleOffset::identity();
let transform = state.coordinate_system_relative_scale_offset
.to_transform()
.pre_mul(&relative_transform);
// Push that new coordinate system and record the new id.
let coord_system = CoordinateSystem {
transform,
parent: Some(state.current_coordinate_system_id),
};
state.current_coordinate_system_id = CoordinateSystemId(coord_systems.len() as u32);
coord_systems.push(coord_system);
}
}
}
// 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 = state.current_coordinate_system_id;
}
_ => {
// We calculate this here to avoid a double-borrow later.
let sticky_offset = self.calculate_sticky_offset(
&state.nearest_scrolling_ancestor_offset,
&state.nearest_scrolling_ancestor_viewport,
);
// 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;
self.world_viewport_transform = if accumulated_offset != LayoutVector2D::zero() {
state.parent_reference_frame_transform.pre_translate(&accumulated_offset)
} else {
state.parent_reference_frame_transform
};
// The transformation for any content inside of us is the viewport transformation, plus
// whatever scrolling offset we supply as well.
let scroll_offset = self.scroll_offset();
self.world_content_transform = if scroll_offset != LayoutVector2D::zero() {
self.world_viewport_transform.pre_translate(&scroll_offset)
} else {
self.world_viewport_transform
};
let added_offset = state.parent_accumulated_scroll_offset + sticky_offset + scroll_offset;
self.coordinate_system_relative_scale_offset =
state.coordinate_system_relative_scale_offset.offset(added_offset.to_untyped());
if let SpatialNodeType::StickyFrame(ref mut info) = self.node_type {
info.current_offset = sticky_offset;
}
self.coordinate_system_id = state.current_coordinate_system_id;
}
}
}
fn calculate_sticky_offset(
&self,
viewport_scroll_offset: &LayoutVector2D,
viewport_rect: &LayoutRect,
) -> LayoutVector2D {
let info = match self.node_type {
SpatialNodeType::StickyFrame(ref info) => info,
_ => return LayoutVector2D::zero(),
};
if info.margins.top.is_none() && info.margins.bottom.is_none() &&
info.margins.left.is_none() && info.margins.right.is_none() {
return LayoutVector2D::zero();
}
// 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.
let sticky_rect = info.frame_rect.translate(viewport_scroll_offset);
let mut sticky_offset = LayoutVector2D::zero();
if let 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();
} else if info.previously_applied_offset.y > 0.0 &&
sticky_rect.min_y() > top_viewport_edge {
// However, if the sticky rect is positioned *below* the top edge of the viewport
// and there is already some offset applied to the sticky rect's position, then
// we need to move it up so that it remains at the correct position. This
// makes sticky_offset.y negative and effectively reduces the amount of the
// offset that was already applied. We limit the reduction so that it can, at most,
// cancel out the already-applied offset, but should never end up adjusting the
// position the other way.
sticky_offset.y = top_viewport_edge - sticky_rect.min_y();
sticky_offset.y = sticky_offset.y.max(-info.previously_applied_offset.y);
}
debug_assert!(sticky_offset.y + info.previously_applied_offset.y >= 0.0);
}
// If we don't have a sticky-top offset (sticky_offset.y + info.previously_applied_offset.y
// == 0), or if we have a previously-applied bottom offset (previously_applied_offset.y < 0)
// then we check for handling the bottom margin case.
if sticky_offset.y + info.previously_applied_offset.y <= 0.0 {
if let Some(margin) = info.margins.bottom {
// Same as the above case, but inverted for bottom-sticky items. Here
// we adjust items upwards, resulting in a negative sticky_offset.y,
// or reduce the already-present upward adjustment, resulting in a positive
// 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();
} else if info.previously_applied_offset.y < 0.0 &&
sticky_rect.max_y() < bottom_viewport_edge {
sticky_offset.y = bottom_viewport_edge - sticky_rect.max_y();
sticky_offset.y = sticky_offset.y.min(-info.previously_applied_offset.y);
}
debug_assert!(sticky_offset.y + info.previously_applied_offset.y <= 0.0);
}
}
// Same as above, but for the x-axis.
if let 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();
} else if info.previously_applied_offset.x > 0.0 &&
sticky_rect.min_x() > left_viewport_edge {
sticky_offset.x = left_viewport_edge - sticky_rect.min_x();
sticky_offset.x = sticky_offset.x.max(-info.previously_applied_offset.x);
}
debug_assert!(sticky_offset.x + info.previously_applied_offset.x >= 0.0);
}
if sticky_offset.x + info.previously_applied_offset.x <= 0.0 {
if let Some(margin) = info.margins.right {
let right_viewport_edge = viewport_rect.max_x() - margin;
if sticky_rect.max_x() > right_viewport_edge {
sticky_offset.x = right_viewport_edge - sticky_rect.max_x();
} else if info.previously_applied_offset.x < 0.0 &&
sticky_rect.max_x() < right_viewport_edge {
sticky_offset.x = right_viewport_edge - sticky_rect.max_x();
sticky_offset.x = sticky_offset.x.min(-info.previously_applied_offset.x);
}
debug_assert!(sticky_offset.x + info.previously_applied_offset.x <= 0.0);
}
}
// The total "sticky offset" (which is the sum that was already applied by
// the calling code, stored in info.previously_applied_offset, and the extra amount we
// computed as a result of scrolling, stored in sticky_offset) needs to be
// clamped to the provided bounds.
let clamp_adjusted = |value: f32, adjust: f32, bounds: &StickyOffsetBounds| {
(value + adjust).max(bounds.min).min(bounds.max) - adjust
};
sticky_offset.y = clamp_adjusted(sticky_offset.y,
info.previously_applied_offset.y,
&info.vertical_offset_bounds);
sticky_offset.x = clamp_adjusted(sticky_offset.x,
info.previously_applied_offset.x,
&info.horizontal_offset_bounds);
sticky_offset
}
pub fn prepare_state_for_children(&self, state: &mut TransformUpdateState) {
if !self.invertible {
state.invertible = false;
return;
}
// 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.
match self.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 + state.parent_accumulated_scroll_offset;
}
SpatialNodeType::ScrollFrame(ref scrolling) => {
state.parent_accumulated_scroll_offset =
scrolling.offset + state.parent_accumulated_scroll_offset;
state.nearest_scrolling_ancestor_offset = scrolling.offset;
state.nearest_scrolling_ancestor_viewport = scrolling.viewport_rect;
}
SpatialNodeType::ReferenceFrame(ref info) => {
state.parent_reference_frame_transform = self.world_viewport_transform;
state.parent_accumulated_scroll_offset = LayoutVector2D::zero();
state.coordinate_system_relative_scale_offset = self.coordinate_system_relative_scale_offset;
let translation = -info.origin_in_parent_reference_frame;
state.nearest_scrolling_ancestor_viewport =
state.nearest_scrolling_ancestor_viewport
.translate(&translation);
}
}
}
pub fn scrollable_size(&self) -> LayoutSize {
match self.node_type {
SpatialNodeType::ScrollFrame(state) => state.scrollable_size,
_ => LayoutSize::zero(),
}
}
pub fn scroll(&mut self, scroll_location: ScrollLocation) -> bool {
let scrolling = match self.node_type {
SpatialNodeType::ScrollFrame(ref mut scrolling) => scrolling,
_ => return false,
};
let delta = match scroll_location {
ScrollLocation::Delta(delta) => delta,
ScrollLocation::Start => {
if scrolling.offset.y.round() >= 0.0 {
// Nothing to do on this layer.
return false;
}
scrolling.offset.y = 0.0;
return true;
}
ScrollLocation::End => {
let end_pos = -scrolling.scrollable_size.height;
if scrolling.offset.y.round() <= end_pos {
// Nothing to do on this layer.
return false;
}
scrolling.offset.y = end_pos;
return true;
}
};
let scrollable_width = scrolling.scrollable_size.width;
let scrollable_height = scrolling.scrollable_size.height;
let original_layer_scroll_offset = scrolling.offset;
if scrollable_width > 0. {
scrolling.offset.x = (scrolling.offset.x + delta.x)
.min(0.0)
.max(-scrollable_width)
.round();
}
if scrollable_height > 0. {
scrolling.offset.y = (scrolling.offset.y + delta.y)
.min(0.0)
.max(-scrollable_height)
.round();
}
scrolling.offset != original_layer_scroll_offset
}
pub fn scroll_offset(&self) -> LayoutVector2D {
match self.node_type {
SpatialNodeType::ScrollFrame(ref scrolling) => scrolling.offset,
_ => LayoutVector2D::zero(),
}
}
pub fn matches_external_id(&self, external_id: ExternalScrollId) -> bool {
match self.node_type {
SpatialNodeType::ScrollFrame(info) if info.external_id == Some(external_id) => true,
_ => false,
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct ScrollFrameInfo {
/// The rectangle of the viewport of this scroll frame. This is important for
/// positioning of items inside child StickyFrames.
pub viewport_rect: LayoutRect,
pub offset: LayoutVector2D,
pub scroll_sensitivity: ScrollSensitivity,
/// 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: Option<ExternalScrollId>,
}
/// Manages scrolling offset.
impl ScrollFrameInfo {
pub fn new(
viewport_rect: LayoutRect,
scroll_sensitivity: ScrollSensitivity,
scrollable_size: LayoutSize,
external_id: Option<ExternalScrollId>,
) -> ScrollFrameInfo {
ScrollFrameInfo {
viewport_rect,
offset: LayoutVector2D::zero(),
scroll_sensitivity,
scrollable_size,
external_id,
}
}
pub fn sensitive_to_input_events(&self) -> bool {
match self.scroll_sensitivity {
ScrollSensitivity::ScriptAndInputEvents => true,
ScrollSensitivity::Script => false,
}
}
pub fn combine_with_old_scroll_info(
self,
old_scroll_info: &ScrollFrameInfo
) -> ScrollFrameInfo {
ScrollFrameInfo {
viewport_rect: self.viewport_rect,
offset: old_scroll_info.offset,
scroll_sensitivity: self.scroll_sensitivity,
scrollable_size: self.scrollable_size,
external_id: self.external_id,
}
}
}
/// Contains information about reference frames.
#[derive(Copy, Clone, Debug)]
pub struct 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 source_perspective: LayoutFastTransform,
/// 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 the resolved transform is invertible.
pub invertible: bool,
}
#[derive(Clone, Debug)]
pub struct StickyFrameInfo {
pub frame_rect: LayoutRect,
pub margins: SideOffsets2D<Option<f32>>,
pub vertical_offset_bounds: StickyOffsetBounds,
pub horizontal_offset_bounds: StickyOffsetBounds,
pub previously_applied_offset: LayoutVector2D,
pub current_offset: LayoutVector2D,
}
impl StickyFrameInfo {
pub fn new(
frame_rect: LayoutRect,
margins: SideOffsets2D<Option<f32>>,
vertical_offset_bounds: StickyOffsetBounds,
horizontal_offset_bounds: StickyOffsetBounds,
previously_applied_offset: LayoutVector2D
) -> StickyFrameInfo {
StickyFrameInfo {
frame_rect,
margins,
vertical_offset_bounds,
horizontal_offset_bounds,
previously_applied_offset,
current_offset: LayoutVector2D::zero(),
}
}
}