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:
1 parent
cbf63c6945
commit
33df23c02a
6 files changed
+90
-12
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user