Caret · whole device pixels at a device pixel: gpui snapped a 1.5px bar's edges apart, so it painted 1 or 2 pixels by where it stood (#7)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 21:45:52 -04:00
1 parent cbf63c6945
commit 33df23c02a
6 files changed
+90 -12

No files matched your search

+4 -4
View File
@@ -8,6 +8,7 @@ use gpui::{
use super::peers::Peer;
use crate::{
forms::TextInput,
primitives::caret_bar,
theme::{ActiveTheme, Radius, TextSize},
typography::LEADING,
};
@@ -61,10 +62,9 @@ impl RenderOnce for RemoteCursor {
return;
}
window.with_content_mask(Some(ContentMask { bounds }), |window| {
window.paint_quad(fill(
gpui::Bounds::new(caret.origin, size(bar, caret.size.height)),
color,
));
let scale = window.scale_factor();
let bar = caret_bar(caret.origin, bar, caret.size.height, scale);
window.paint_quad(fill(bar, color));
});
let font = gpui::Font {
weight: FontWeight::MEDIUM,
+12 -3
View File
@@ -8,7 +8,10 @@ use gpui::{
};
use super::{Highlight, Layout, TextInput};
use crate::theme::ActiveTheme;
use crate::{
primitives::{caret_bar, caret_width},
theme::ActiveTheme,
};
pub(crate) struct TextElement {
input: Entity<TextInput>,
@@ -214,7 +217,10 @@ impl Element for TextElement {
) -> Prepaint {
let theme = cx.theme();
let (selection_color, caret_color) = (theme.colors.selection, theme.colors.focus);
let caret_width = theme.caret_width().to_pixels(window.rem_size());
let caret_width = caret_width(
theme.caret_width().to_pixels(window.rem_size()),
window.scale_factor(),
);
let input = self.input.read(cx);
let (text, placeholder, runs) = shown_runs(input, window, cx);
let font_size = window.text_style().font_size.to_pixels(window.rem_size());
@@ -262,7 +268,10 @@ impl Element for TextElement {
};
let caret = caret_at
.filter(|_| focused && caret_on && selection.is_empty())
.map(|at| Bounds::new(bounds.origin + at - scroll, size(caret_width, line_height)));
.map(|at| {
let at = bounds.origin + at - scroll;
caret_bar(at, caret_width, line_height, window.scale_factor())
});
self.input.update(cx, |input, _| input.scroll = scroll);
Prepaint {
lines,
+65
View File
@@ -0,0 +1,65 @@
use gpui::{Bounds, Pixels, Point, size};
fn snap(logical: Pixels, least: f32, scale: f32) -> Pixels {
Pixels::from((logical.as_f32() * scale).round().max(least) / scale)
}
/// The width a caret paints at: whole device pixels, at least one.
pub(crate) fn caret_width(width: Pixels, scale: f32) -> Pixels {
snap(width, 1.0, scale)
}
/// A caret bar at `origin`, `width` wide, snapped whole to device pixels so it keeps one width.
pub(crate) fn caret_bar(
origin: Point<Pixels>,
width: Pixels,
height: Pixels,
scale: f32,
) -> Bounds<Pixels> {
Bounds::new(
Point::new(snap(origin.x, f32::MIN, scale), origin.y),
size(caret_width(width, scale), height),
)
}
#[cfg(test)]
mod tests {
use gpui::{point, px};
use super::caret_bar;
/// gpui's paint snapping: each edge alone, half toward zero.
fn device(logical: f32, scale: f32) -> f32 {
let value = logical * scale;
(value.abs() - 0.5).ceil().copysign(value)
}
#[test]
fn a_caret_keeps_one_device_width_wherever_it_stands() {
for scale in [1.0, 1.25, 1.5, 2.0] {
let widths: Vec<f32> = (0..200)
.map(|step| {
let bar = caret_bar(
point(px(10.0 + step as f32 * 0.037), px(3.3)),
px(1.5),
px(18.0),
scale,
);
device(bar.right().as_f32(), scale) - device(bar.left().as_f32(), scale)
})
.collect();
let first = widths[0];
assert!(first >= 1.0, "at {scale}x the caret shows");
assert!(
widths.iter().all(|width| *width == first),
"at {scale}x the caret took widths {:?}",
widths.iter().fold(Vec::new(), |mut seen, width| {
if !seen.contains(width) {
seen.push(*width);
}
seen
})
);
}
}
}
+2
View File
@@ -1,4 +1,5 @@
mod backdrop;
mod caret;
mod disclosure;
mod divider;
mod files;
@@ -17,6 +18,7 @@ mod tooltip;
mod tests;
pub use backdrop::{Backdrop, Place};
pub(crate) use caret::{caret_bar, caret_width};
pub use disclosure::Disclosure;
pub use divider::Divider;
pub use files::IconTheme;
+6 -4
View File
@@ -12,6 +12,7 @@ use super::{
links::Target,
view::Terminal,
};
use crate::primitives::caret_bar;
/// How a terminal paints: font and cell, ink, and the colors of what it marks.
pub(crate) struct Look {
@@ -161,10 +162,11 @@ pub(crate) fn grid(
(CursorShape::HollowBlock, _) | (_, false) => {
outline(Bounds::new(origin, cell), look.caret, BorderStyle::Solid)
}
(CursorShape::Beam, true) => fill(
Bounds::new(origin, size(look.caret_width, cell.height)),
look.caret,
),
(CursorShape::Beam, true) => {
let scale = window.scale_factor();
let bar = caret_bar(origin, look.caret_width, cell.height, scale);
fill(bar, look.caret)
}
(CursorShape::Underline, true) => fill(
Bounds::new(
origin + point(Pixels::ZERO, cell.height - look.caret_width),
+1 -1
View File
@@ -16,7 +16,7 @@
# GitHub round, 2026-10-08
- [x] G10 CI: `cargo fmt` on the files the last commits left unformatted
- [ ] G11 Issue #7: a caret is a whole number of device pixels wide at a device pixel, so it never shows thin then thick; tested
- [x] G11 Issue #7: a caret is a whole number of device pixels wide at a device pixel, so it never shows thin then thick; tested
- [ ] G12 Issue #7: an IME asks for its candidate box past the laid-out text; the box clamps to the last layout, as `bounds_for` does; tested
- [ ] G13 Issue #12: a floating option list occludes, so the wheel scrolls the list and not the page under it; tested
- [ ] G14 Issue #7: BlockEditor scrolls in its own `ScrollArea` with its scrollbar and keeps the caret in view as blocks are added; tested