Initial attempts

John Tur created

Change summary

crates/editor/src/editor.rs      |   7 
crates/editor/src/element.rs     |  95 +++++++++++++---
crates/gpui/src/elements/text.rs |   8 
crates/gpui/src/style.rs         |  60 ----------
crates/gpui/src/window.rs        | 194 +++++++++++++++++++++++++--------
5 files changed, 235 insertions(+), 129 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -25260,7 +25260,9 @@ impl Editor {
         window: &mut Window,
         cx: &mut App,
     ) -> Option<gpui::Point<Pixels>> {
-        let line_height = self.style(cx).text.line_height_in_pixels(window.rem_size());
+        let line_height = window.round_to_nearest_device_pixel(
+            self.style(cx).text.line_height_in_pixels(window.rem_size()),
+        );
         let text_layout_details = self.text_layout_details(window, cx);
         let scroll_top = text_layout_details
             .scroll_anchor
@@ -25313,7 +25315,8 @@ impl Editor {
         let style = &text_layout_details.editor_style;
         let font_id = window.text_system().resolve_font(&style.text.font());
         let font_size = style.text.font_size.to_pixels(window.rem_size());
-        let line_height = style.text.line_height_in_pixels(window.rem_size());
+        let line_height = window
+            .round_to_nearest_device_pixel(style.text.line_height_in_pixels(window.rem_size()));
         let em_width = window.text_system().em_width(font_id, font_size).unwrap();
         let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
 

crates/editor/src/element.rs 🔗

@@ -2193,7 +2193,7 @@ impl EditorElement {
         let rem_size = self.rem_size(cx).unwrap_or(window.rem_size());
         let mut text_style = self.style.text.clone();
         text_style.font_size = font_size;
-        text_style.line_height_in_pixels(rem_size)
+        rounded_line_height(window, text_style.line_height_in_pixels(rem_size))
     }
 
     fn get_minimap_width(
@@ -5899,7 +5899,7 @@ impl EditorElement {
 
     fn paint_background(&self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
         window.paint_layer(layout.hitbox.bounds, |window| {
-            let scroll_top = layout.position_map.snapshot.scroll_position().y;
+            let scroll_top = layout.position_map.scroll_position.y;
             let gutter_bg = cx.theme().colors().editor_gutter_background;
             window.paint_quad(fill(layout.gutter_hitbox.bounds, gutter_bg));
             window.paint_quad(fill(
@@ -8849,7 +8849,10 @@ impl LineWithInvisibles {
                             window,
                             max_width: text_width,
                         });
-                        let line_height = text_style.line_height_in_pixels(window.rem_size());
+                        let line_height = rounded_line_height(
+                            window,
+                            text_style.line_height_in_pixels(window.rem_size()),
+                        );
                         let size = element.layout_as_root(
                             size(available_width, AvailableSpace::Definite(line_height)),
                             window,
@@ -9580,7 +9583,10 @@ impl Element for EditorElement {
                 let layout_id = match editor.mode {
                     EditorMode::SingleLine => {
                         let rem_size = window.rem_size();
-                        let height = self.style.text.line_height_in_pixels(rem_size);
+                        let height = rounded_line_height(
+                            window,
+                            self.style.text.line_height_in_pixels(rem_size),
+                        );
                         let mut style = Style::default();
                         style.size.height = height.into();
                         style.size.width = relative(1.).into();
@@ -9623,8 +9629,10 @@ impl Element for EditorElement {
                         style.size.width = relative(1.).into();
                         if sizing_behavior == SizingBehavior::SizeByContent {
                             let snapshot = editor.snapshot(window, cx);
-                            let line_height =
-                                self.style.text.line_height_in_pixels(window.rem_size());
+                            let line_height = rounded_line_height(
+                                window,
+                                self.style.text.line_height_in_pixels(window.rem_size()),
+                            );
                             let scroll_height =
                                 (snapshot.max_point().row().next_row().0 as f32) * line_height;
                             style.size.height = scroll_height.into();
@@ -9677,7 +9685,8 @@ impl Element for EditorElement {
                     let rem_size = window.rem_size();
                     let font_id = window.text_system().resolve_font(&style.text.font());
                     let font_size = style.text.font_size.to_pixels(rem_size);
-                    let line_height = style.text.line_height_in_pixels(rem_size);
+                    let line_height =
+                        rounded_line_height(window, style.text.line_height_in_pixels(rem_size));
                     let em_width = window.text_system().em_width(font_id, font_size).unwrap();
                     let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
                     let em_layout_width = window.text_system().em_layout_width(font_id, font_size);
@@ -10338,9 +10347,11 @@ impl Element for EditorElement {
                     let start_buffer_row = MultiBufferRow(start_anchor.to_point(&buffer).row);
                     let end_buffer_row = MultiBufferRow(end_anchor.to_point(&buffer).row);
 
-                    let preliminary_scroll_pixel_position = point(
-                        scroll_position.x * f64::from(em_layout_width),
-                        scroll_position.y * f64::from(line_height),
+                    let preliminary_scroll_pixel_position = rounded_scroll_pixel_position(
+                        window,
+                        scroll_position,
+                        em_layout_width,
+                        line_height,
                     );
                     let indent_guides = self.layout_indent_guides(
                         content_origin,
@@ -10462,9 +10473,16 @@ impl Element for EditorElement {
                         }
                     });
 
-                    let scroll_pixel_position = point(
-                        scroll_position.x * f64::from(em_layout_width),
-                        scroll_position.y * f64::from(line_height),
+                    let scroll_pixel_position = rounded_scroll_pixel_position(
+                        window,
+                        scroll_position,
+                        em_layout_width,
+                        line_height,
+                    );
+                    let scroll_position = scroll_position_from_rounded_pixels(
+                        scroll_pixel_position,
+                        em_layout_width,
+                        line_height,
                     );
                     let sticky_headers = if !is_minimap
                         && is_singleton
@@ -11937,7 +11955,7 @@ impl PointForPosition {
 impl PositionMap {
     pub(crate) fn point_for_position(&self, position: gpui::Point<Pixels>) -> PointForPosition {
         let text_bounds = self.text_hitbox.bounds;
-        let scroll_position = self.snapshot.scroll_position();
+        let scroll_position = self.scroll_position;
         let position = position - text_bounds.origin;
         let y = position.y.max(px(0.)).min(self.size.height);
         let x = position.x + (scroll_position.x as f32 * self.em_layout_width);
@@ -12345,6 +12363,46 @@ pub fn register_action<T: Action>(
 
 /// Shared between `prepaint` and `compute_auto_height_layout` to ensure
 /// both full and auto-height editors compute wrap widths consistently.
+#[inline]
+fn rounded_line_height(window: &Window, line_height: Pixels) -> Pixels {
+    window.round_to_nearest_device_pixel(line_height)
+}
+
+#[inline]
+fn rounded_scroll_pixel_position(
+    window: &Window,
+    scroll_position: gpui::Point<ScrollOffset>,
+    em_layout_width: Pixels,
+    line_height: Pixels,
+) -> gpui::Point<ScrollPixelOffset> {
+    let dpi_scale = f64::from(window.scale_factor());
+    let snap_to_device_pixels = |value: f64| (value * dpi_scale).round() / dpi_scale;
+    point(
+        snap_to_device_pixels(scroll_position.x * f64::from(em_layout_width)),
+        snap_to_device_pixels(scroll_position.y * f64::from(line_height)),
+    )
+}
+
+#[inline]
+fn scroll_position_from_rounded_pixels(
+    scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
+    em_layout_width: Pixels,
+    line_height: Pixels,
+) -> gpui::Point<ScrollOffset> {
+    point(
+        if em_layout_width.is_zero() {
+            0.0
+        } else {
+            scroll_pixel_position.x / f64::from(em_layout_width)
+        },
+        if line_height.is_zero() {
+            0.0
+        } else {
+            scroll_pixel_position.y / f64::from(line_height)
+        },
+    )
+}
+
 fn calculate_wrap_width(
     soft_wrap: SoftWrap,
     editor_width: Pixels,
@@ -12384,7 +12442,8 @@ fn compute_auto_height_layout(
     let style = editor.style.as_ref().unwrap();
     let font_id = window.text_system().resolve_font(&style.text.font());
     let font_size = style.text.font_size.to_pixels(window.rem_size());
-    let line_height = style.text.line_height_in_pixels(window.rem_size());
+    let line_height =
+        rounded_line_height(window, style.text.line_height_in_pixels(window.rem_size()));
     let em_width = window.text_system().em_width(font_id, font_size).unwrap();
 
     let mut snapshot = editor.snapshot(window, cx);
@@ -12505,7 +12564,7 @@ mod tests {
         let style = editor.update(cx, |editor, cx| editor.style(cx).clone());
         let line_height = window
             .update(cx, |_, window, _| {
-                style.text.line_height_in_pixels(window.rem_size())
+                rounded_line_height(window, style.text.line_height_in_pixels(window.rem_size()))
             })
             .unwrap();
         let element = EditorElement::new(&editor, style);
@@ -12666,7 +12725,7 @@ mod tests {
         let style = editor.update(cx, |editor, cx| editor.style(cx).clone());
         let line_height = window
             .update(cx, |_, window, _| {
-                style.text.line_height_in_pixels(window.rem_size())
+                rounded_line_height(window, style.text.line_height_in_pixels(window.rem_size()))
             })
             .unwrap();
         let element = EditorElement::new(&editor, style);
@@ -12743,7 +12802,7 @@ mod tests {
         let style = editor.update(cx, |editor, cx| editor.style(cx).clone());
         let line_height = window
             .update(cx, |_, window, _| {
-                style.text.line_height_in_pixels(window.rem_size())
+                rounded_line_height(window, style.text.line_height_in_pixels(window.rem_size()))
             })
             .unwrap();
         let element = EditorElement::new(&editor, style);

crates/gpui/src/elements/text.rs 🔗

@@ -340,9 +340,11 @@ impl TextLayout {
     ) -> LayoutId {
         let text_style = window.text_style();
         let font_size = text_style.font_size.to_pixels(window.rem_size());
-        let line_height = text_style
-            .line_height
-            .to_pixels(font_size.into(), window.rem_size());
+        let line_height = window.round_to_nearest_device_pixel(
+            text_style
+                .line_height
+                .to_pixels(font_size.into(), window.rem_size()),
+        );
 
         let runs = if let Some(runs) = runs {
             runs

crates/gpui/src/style.rs 🔗

@@ -666,72 +666,16 @@ impl Style {
 
         if self.is_border_visible() {
             let border_widths = self.border_widths.to_pixels(rem_size);
-            let max_border_width = border_widths.max();
-            let max_corner_radius = corner_radii.max();
-            let zero_size = Size {
-                width: Pixels::ZERO,
-                height: Pixels::ZERO,
-            };
-
-            let mut top_bounds = Bounds::from_corners(
-                bounds.origin,
-                bounds.top_right() + point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
-            );
-            top_bounds.size = top_bounds.size.max(&zero_size);
-            let mut bottom_bounds = Bounds::from_corners(
-                bounds.bottom_left() - point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
-                bounds.bottom_right(),
-            );
-            bottom_bounds.size = bottom_bounds.size.max(&zero_size);
-            let mut left_bounds = Bounds::from_corners(
-                top_bounds.bottom_left(),
-                bottom_bounds.origin + point(max_border_width, Pixels::ZERO),
-            );
-            left_bounds.size = left_bounds.size.max(&zero_size);
-            let mut right_bounds = Bounds::from_corners(
-                top_bounds.bottom_right() - point(max_border_width, Pixels::ZERO),
-                bottom_bounds.top_right(),
-            );
-            right_bounds.size = right_bounds.size.max(&zero_size);
-
             let mut background = self.border_color.unwrap_or_default();
             background.a = 0.;
-            let quad = quad(
+            window.paint_quad(quad(
                 bounds,
                 corner_radii,
                 background,
                 border_widths,
                 self.border_color.unwrap_or_default(),
                 self.border_style,
-            );
-
-            window.with_content_mask(Some(ContentMask { bounds: top_bounds }), |window| {
-                window.paint_quad(quad.clone());
-            });
-            window.with_content_mask(
-                Some(ContentMask {
-                    bounds: right_bounds,
-                }),
-                |window| {
-                    window.paint_quad(quad.clone());
-                },
-            );
-            window.with_content_mask(
-                Some(ContentMask {
-                    bounds: bottom_bounds,
-                }),
-                |window| {
-                    window.paint_quad(quad.clone());
-                },
-            );
-            window.with_content_mask(
-                Some(ContentMask {
-                    bounds: left_bounds,
-                }),
-                |window| {
-                    window.paint_quad(quad);
-                },
-            );
+            ));
         }
 
         #[cfg(debug_assertions)]

crates/gpui/src/window.rs 🔗

@@ -11,13 +11,13 @@ use crate::{
     MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
     PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, Priority, PromptButton,
     PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams,
-    Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y,
-    ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, SubpixelSprite,
-    SubscriberSet, Subscription, SystemWindowTab, SystemWindowTabController, TabStopMap,
-    TaffyLayoutEngine, Task, TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState,
-    TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance,
-    WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
-    point, prelude::*, px, rems, size, transparent_black,
+    Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS_X, ScaledPixels, Scene, Shadow,
+    SharedString, Size, StrikethroughStyle, Style, SubpixelSprite, SubscriberSet, Subscription,
+    SystemWindowTab, SystemWindowTabController, TabStopMap, TaffyLayoutEngine, Task,
+    TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState, TransformationMatrix,
+    Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
+    WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, point,
+    prelude::*, px, rems, size, transparent_black,
 };
 use anyhow::{Context as _, Result, anyhow};
 use collections::{FxHashMap, FxHashSet};
@@ -2108,7 +2108,94 @@ impl Window {
 
     /// The line height associated with the current text style.
     pub fn line_height(&self) -> Pixels {
-        self.text_style().line_height_in_pixels(self.rem_size())
+        self.round_to_nearest_device_pixel(self.text_style().line_height_in_pixels(self.rem_size()))
+    }
+
+    /// Rounds a logical size or coordinate to the nearest device pixel for this window.
+    #[inline]
+    pub fn round_to_nearest_device_pixel(&self, value: Pixels) -> Pixels {
+        let scale_factor = self.scale_factor();
+        px((value.0 * scale_factor).round() / scale_factor)
+    }
+
+    #[inline]
+    fn round_point_to_device_pixels(&self, position: Point<Pixels>) -> Point<Pixels> {
+        point(
+            self.round_to_nearest_device_pixel(position.x),
+            self.round_to_nearest_device_pixel(position.y),
+        )
+    }
+
+    #[inline]
+    fn bounds_from_device_edges(
+        &self,
+        left: f32,
+        top: f32,
+        right: f32,
+        bottom: f32,
+    ) -> Bounds<ScaledPixels> {
+        let right = right.max(left);
+        let bottom = bottom.max(top);
+        Bounds::from_corners(
+            point(ScaledPixels(left), ScaledPixels(top)),
+            point(ScaledPixels(right), ScaledPixels(bottom)),
+        )
+    }
+
+    #[inline]
+    fn round_bounds_to_device_pixels(&self, bounds: Bounds<Pixels>) -> Bounds<ScaledPixels> {
+        let scale_factor = self.scale_factor();
+        let left = (bounds.left().0 * scale_factor).round();
+        let top = (bounds.top().0 * scale_factor).round();
+        let right = (bounds.right().0 * scale_factor).round();
+        let bottom = (bounds.bottom().0 * scale_factor).round();
+        self.bounds_from_device_edges(left, top, right, bottom)
+    }
+
+    #[inline]
+    fn outset_bounds_to_device_pixels(&self, bounds: Bounds<Pixels>) -> Bounds<ScaledPixels> {
+        let scale_factor = self.scale_factor();
+        let left = (bounds.left().0 * scale_factor).floor();
+        let top = (bounds.top().0 * scale_factor).floor();
+        let right = (bounds.right().0 * scale_factor).ceil();
+        let bottom = (bounds.bottom().0 * scale_factor).ceil();
+        self.bounds_from_device_edges(left, top, right, bottom)
+    }
+
+    #[inline]
+    fn outset_content_mask_to_device_pixels(
+        &self,
+        content_mask: ContentMask<Pixels>,
+    ) -> ContentMask<ScaledPixels> {
+        ContentMask {
+            bounds: self.outset_bounds_to_device_pixels(content_mask.bounds),
+        }
+    }
+
+    #[inline]
+    fn round_edges_to_device_pixels(&self, edges: Edges<Pixels>) -> Edges<ScaledPixels> {
+        let scale_factor = self.scale_factor();
+        Edges {
+            top: ScaledPixels((edges.top.0 * scale_factor).round()),
+            right: ScaledPixels((edges.right.0 * scale_factor).round()),
+            bottom: ScaledPixels((edges.bottom.0 * scale_factor).round()),
+            left: ScaledPixels((edges.left.0 * scale_factor).round()),
+        }
+    }
+
+    #[inline]
+    fn round_length_to_device_pixels(&self, value: Pixels) -> ScaledPixels {
+        ScaledPixels((value.0 * self.scale_factor()).round())
+    }
+
+    #[inline]
+    fn round_nonzero_length_to_device_pixels(&self, value: Pixels) -> ScaledPixels {
+        let rounded = self.round_length_to_device_pixels(value).0;
+        if value.is_zero() {
+            ScaledPixels(0.0)
+        } else {
+            ScaledPixels(rounded.max(1.0))
+        }
     }
 
     /// Call to prevent the default action of an event. Currently only used to prevent
@@ -2693,7 +2780,7 @@ impl Window {
             return f(self);
         };
 
-        let abs_offset = self.element_offset() + offset;
+        let abs_offset = self.round_point_to_device_pixels(self.element_offset() + offset);
         self.with_absolute_element_offset(abs_offset, f)
     }
 
@@ -2706,7 +2793,8 @@ impl Window {
         f: impl FnOnce(&mut Self) -> R,
     ) -> R {
         self.invalidator.debug_assert_prepaint();
-        self.element_offset_stack.push(offset);
+        self.element_offset_stack
+            .push(self.round_point_to_device_pixels(offset));
         let result = f(self);
         self.element_offset_stack.pop();
         result
@@ -3063,13 +3151,12 @@ impl Window {
     pub fn paint_layer<R>(&mut self, bounds: Bounds<Pixels>, f: impl FnOnce(&mut Self) -> R) -> R {
         self.invalidator.debug_assert_paint();
 
-        let scale_factor = self.scale_factor();
         let content_mask = self.content_mask();
         let clipped_bounds = bounds.intersect(&content_mask.bounds);
         if !clipped_bounds.is_empty() {
             self.next_frame
                 .scene
-                .push_layer(clipped_bounds.scale(scale_factor));
+                .push_layer(self.outset_bounds_to_device_pixels(clipped_bounds));
         }
 
         let result = f(self);
@@ -3092,16 +3179,16 @@ impl Window {
     ) {
         self.invalidator.debug_assert_paint();
 
-        let scale_factor = self.scale_factor();
         let content_mask = self.content_mask();
+        let scale_factor = self.scale_factor();
         let opacity = self.element_opacity();
         for shadow in shadows {
             let shadow_bounds = (bounds + shadow.offset).dilate(shadow.spread_radius);
             self.next_frame.scene.insert_primitive(Shadow {
                 order: 0,
                 blur_radius: shadow.blur_radius.scale(scale_factor),
-                bounds: shadow_bounds.scale(scale_factor),
-                content_mask: content_mask.scale(scale_factor),
+                bounds: self.outset_bounds_to_device_pixels(shadow_bounds),
+                content_mask: self.outset_content_mask_to_device_pixels(content_mask.clone()),
                 corner_radii: corner_radii.scale(scale_factor),
                 color: shadow.color.opacity(opacity),
             });
@@ -3120,17 +3207,16 @@ impl Window {
     pub fn paint_quad(&mut self, quad: PaintQuad) {
         self.invalidator.debug_assert_paint();
 
-        let scale_factor = self.scale_factor();
         let content_mask = self.content_mask();
         let opacity = self.element_opacity();
         self.next_frame.scene.insert_primitive(Quad {
             order: 0,
-            bounds: quad.bounds.scale(scale_factor),
-            content_mask: content_mask.scale(scale_factor),
+            bounds: self.round_bounds_to_device_pixels(quad.bounds),
+            content_mask: self.outset_content_mask_to_device_pixels(content_mask),
             background: quad.background.opacity(opacity),
             border_color: quad.border_color.opacity(opacity),
-            corner_radii: quad.corner_radii.scale(scale_factor),
-            border_widths: quad.border_widths.scale(scale_factor),
+            corner_radii: quad.corner_radii.scale(self.scale_factor()),
+            border_widths: self.round_edges_to_device_pixels(quad.border_widths),
             border_style: quad.border_style,
         });
     }
@@ -3170,8 +3256,14 @@ impl Window {
             style.thickness
         };
         let bounds = Bounds {
-            origin,
-            size: size(width, height),
+            origin: point(
+                ScaledPixels((origin.x.0 * scale_factor).round()),
+                ScaledPixels((origin.y.0 * scale_factor).round()),
+            ),
+            size: size(
+                self.round_nonzero_length_to_device_pixels(width),
+                self.round_nonzero_length_to_device_pixels(height),
+            ),
         };
         let content_mask = self.content_mask();
         let element_opacity = self.element_opacity();
@@ -3179,10 +3271,10 @@ impl Window {
         self.next_frame.scene.insert_primitive(Underline {
             order: 0,
             pad: 0,
-            bounds: bounds.scale(scale_factor),
-            content_mask: content_mask.scale(scale_factor),
+            bounds,
+            content_mask: self.outset_content_mask_to_device_pixels(content_mask),
             color: style.color.unwrap_or_default().opacity(element_opacity),
-            thickness: style.thickness.scale(scale_factor),
+            thickness: self.round_nonzero_length_to_device_pixels(style.thickness),
             wavy: if style.wavy { 1 } else { 0 },
         });
     }
@@ -3201,8 +3293,14 @@ impl Window {
         let scale_factor = self.scale_factor();
         let height = style.thickness;
         let bounds = Bounds {
-            origin,
-            size: size(width, height),
+            origin: point(
+                ScaledPixels((origin.x.0 * scale_factor).round()),
+                ScaledPixels((origin.y.0 * scale_factor).round()),
+            ),
+            size: size(
+                self.round_nonzero_length_to_device_pixels(width),
+                self.round_nonzero_length_to_device_pixels(height),
+            ),
         };
         let content_mask = self.content_mask();
         let opacity = self.element_opacity();
@@ -3210,9 +3308,9 @@ impl Window {
         self.next_frame.scene.insert_primitive(Underline {
             order: 0,
             pad: 0,
-            bounds: bounds.scale(scale_factor),
-            content_mask: content_mask.scale(scale_factor),
-            thickness: style.thickness.scale(scale_factor),
+            bounds,
+            content_mask: self.outset_content_mask_to_device_pixels(content_mask),
+            thickness: self.round_nonzero_length_to_device_pixels(style.thickness),
             color: style.color.unwrap_or_default().opacity(opacity),
             wavy: 0,
         });
@@ -3242,7 +3340,10 @@ impl Window {
 
         let subpixel_variant = Point {
             x: (glyph_origin.x.0.fract() * SUBPIXEL_VARIANTS_X as f32).floor() as u8,
-            y: (glyph_origin.y.0.fract() * SUBPIXEL_VARIANTS_Y as f32).floor() as u8,
+            // Keep vertical glyph rasterization stable while scrolling. Y-position is
+            // snapped at quad placement time, so varying Y subpixel glyph variants only
+            // introduces frame-to-frame wobble.
+            y: 0,
         };
         let subpixel_rendering = self.should_use_subpixel_rendering(font_id, font_size);
         let params = RenderGlyphParams {
@@ -3265,10 +3366,11 @@ impl Window {
                 })?
                 .expect("Callback above only errors or returns Some");
             let bounds = Bounds {
-                origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into),
+                origin: point(glyph_origin.x.floor(), glyph_origin.y.round())
+                    + raster_bounds.origin.map(Into::into),
                 size: tile.bounds.size.map(Into::into),
             };
-            let content_mask = self.content_mask().scale(scale_factor);
+            let content_mask = self.outset_content_mask_to_device_pixels(self.content_mask());
 
             if subpixel_rendering {
                 self.next_frame.scene.insert_primitive(SubpixelSprite {
@@ -3355,10 +3457,11 @@ impl Window {
                 .expect("Callback above only errors or returns Some");
 
             let bounds = Bounds {
-                origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into),
+                origin: point(glyph_origin.x.floor(), glyph_origin.y.round())
+                    + raster_bounds.origin.map(Into::into),
                 size: tile.bounds.size.map(Into::into),
             };
-            let content_mask = self.content_mask().scale(scale_factor);
+            let content_mask = self.outset_content_mask_to_device_pixels(self.content_mask());
             let opacity = self.element_opacity();
 
             self.next_frame.scene.insert_primitive(PolychromeSprite {
@@ -3390,9 +3493,8 @@ impl Window {
         self.invalidator.debug_assert_paint();
 
         let element_opacity = self.element_opacity();
-        let scale_factor = self.scale_factor();
 
-        let bounds = bounds.scale(scale_factor);
+        let bounds = self.round_bounds_to_device_pixels(bounds);
         let params = RenderSvgParams {
             path,
             size: bounds.size.map(|pixels| {
@@ -3412,7 +3514,7 @@ impl Window {
         else {
             return Ok(());
         };
-        let content_mask = self.content_mask().scale(scale_factor);
+        let content_mask = self.outset_content_mask_to_device_pixels(self.content_mask());
         let svg_bounds = Bounds {
             origin: bounds.center()
                 - Point::new(
@@ -3454,8 +3556,7 @@ impl Window {
     ) -> Result<()> {
         self.invalidator.debug_assert_paint();
 
-        let scale_factor = self.scale_factor();
-        let bounds = bounds.scale(scale_factor);
+        let bounds = self.outset_bounds_to_device_pixels(bounds);
         let params = RenderImageParams {
             image_id: data.id,
             frame_index,
@@ -3473,17 +3574,15 @@ impl Window {
                 )))
             })?
             .expect("Callback above only returns Some");
-        let content_mask = self.content_mask().scale(scale_factor);
-        let corner_radii = corner_radii.scale(scale_factor);
+        let content_mask = self.outset_content_mask_to_device_pixels(self.content_mask());
+        let corner_radii = corner_radii.scale(self.scale_factor());
         let opacity = self.element_opacity();
 
         self.next_frame.scene.insert_primitive(PolychromeSprite {
             order: 0,
             pad: 0,
             grayscale,
-            bounds: bounds
-                .map_origin(|origin| origin.floor())
-                .map_size(|size| size.ceil()),
+            bounds,
             content_mask,
             corner_radii,
             tile,
@@ -3501,9 +3600,8 @@ impl Window {
 
         self.invalidator.debug_assert_paint();
 
-        let scale_factor = self.scale_factor();
-        let bounds = bounds.scale(scale_factor);
-        let content_mask = self.content_mask().scale(scale_factor);
+        let bounds = self.round_bounds_to_device_pixels(bounds);
+        let content_mask = self.outset_content_mask_to_device_pixels(self.content_mask());
         self.next_frame.scene.insert_primitive(PaintSurface {
             order: 0,
             bounds,