commit cf03ca40e8851d529c1c5a07e05cd3fa40375901
parent 7c4b3317be721ed9cc21942ca1723c6c37535781
Author: Glenn Watson <git@chillybin.org>
Date: Wed, 5 Nov 2025 19:25:37 +0000
Bug 1998317 - Ensure scroll offsets used in nested sticky frames are snapped to device pixels r=gfx-reviewers,lsalzman
Differential Revision: https://phabricator.services.mozilla.com/D271337
Diffstat:
5 files changed, 64 insertions(+), 1 deletion(-)
diff --git a/gfx/wr/webrender/src/spatial_node.rs b/gfx/wr/webrender/src/spatial_node.rs
@@ -12,7 +12,8 @@ use crate::spatial_tree::{CoordinateSystem, SpatialNodeIndex, TransformUpdateSta
use crate::spatial_tree::CoordinateSystemId;
use euclid::{Vector2D, SideOffsets2D};
use crate::scene::SceneProperties;
-use crate::util::{LayoutFastTransform, MatrixHelpers, ScaleOffset, TransformedRectKind, PointHelpers};
+use crate::util::{LayoutFastTransform, MatrixHelpers, ScaleOffset, TransformedRectKind};
+use crate::util::{PointHelpers, VectorHelpers};
/// The kind of a spatial node uid. These are required because we currently create external
/// nodes during DL building, but the internal nodes aren't created until scene building.
@@ -338,6 +339,13 @@ impl SpatialNode {
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 scrolling.offsets == offsets {
diff --git a/gfx/wr/webrender/src/util.rs b/gfx/wr/webrender/src/util.rs
@@ -583,6 +583,22 @@ impl<U> PointHelpers<U> for Point2D<f32, U> {
}
}
+pub trait VectorHelpers<U>
+where
+ Self: Sized,
+{
+ fn snap(&self) -> Self;
+}
+
+impl<U> VectorHelpers<U> for Vector2D<f32, U> {
+ fn snap(&self) -> Self {
+ Vector2D::new(
+ self.x.round(),
+ self.y.round(),
+ )
+ }
+}
+
pub trait RectHelpers<U>
where
Self: Sized,
diff --git a/gfx/wr/wrench/reftests/scrolling/fractional-sticky-ref.yaml b/gfx/wr/wrench/reftests/scrolling/fractional-sticky-ref.yaml
@@ -0,0 +1,11 @@
+root:
+ items:
+ - type: rect
+ bounds: [0, 0, 400, 400]
+ color: black
+ - type: rect
+ bounds: [0, 379, 20, 20]
+ color: green
+ - type: rect
+ bounds: [0, 400, 20, 20]
+ color: red
diff --git a/gfx/wr/wrench/reftests/scrolling/fractional-sticky.yaml b/gfx/wr/wrench/reftests/scrolling/fractional-sticky.yaml
@@ -0,0 +1,27 @@
+# Verify that fractional scroll offsets are correctly snapped before
+# being included in nested sticky frames.
+root:
+ items:
+ - type: scroll-frame
+ id: 2
+ bounds: [0, 0, 1000, 1000]
+ content-size: [1000, 8000]
+ scroll-offset: [0, 1000.7]
+ items:
+ - type: rect
+ bounds: [0, 0, 200, 800.6667]
+ color: blue
+ - type: rect
+ bounds: [0, 800.6667, 400, 600]
+ color: black
+ - type: sticky-frame
+ bounds: [0, 818.1667, 20, 20]
+ margin-top: 399
+ vertical-offset-bounds: [0, 561.5]
+ items:
+ - type: rect
+ bounds: [0, 819.1667, 20, 20]
+ color: green
+ - type: rect
+ bounds: [0, 1400.6667, 20, 20]
+ color: red
diff --git a/gfx/wr/wrench/reftests/scrolling/reftest.list b/gfx/wr/wrench/reftests/scrolling/reftest.list
@@ -24,3 +24,4 @@
== scroll-generation-3.yaml scroll-generation-ref.yaml
== scroll-generation-4.yaml scroll-generation-ref.yaml
== ext-scroll-offset-rounded-clip.yaml ext-scroll-offset-rounded-clip-ref.yaml
+== fractional-sticky.yaml fractional-sticky-ref.yaml