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