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 super::peers::Peer;
|
||||||
use crate::{
|
use crate::{
|
||||||
forms::TextInput,
|
forms::TextInput,
|
||||||
|
primitives::caret_bar,
|
||||||
theme::{ActiveTheme, Radius, TextSize},
|
theme::{ActiveTheme, Radius, TextSize},
|
||||||
typography::LEADING,
|
typography::LEADING,
|
||||||
};
|
};
|
||||||
@@ -61,10 +62,9 @@ impl RenderOnce for RemoteCursor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||||||
window.paint_quad(fill(
|
let scale = window.scale_factor();
|
||||||
gpui::Bounds::new(caret.origin, size(bar, caret.size.height)),
|
let bar = caret_bar(caret.origin, bar, caret.size.height, scale);
|
||||||
color,
|
window.paint_quad(fill(bar, color));
|
||||||
));
|
|
||||||
});
|
});
|
||||||
let font = gpui::Font {
|
let font = gpui::Font {
|
||||||
weight: FontWeight::MEDIUM,
|
weight: FontWeight::MEDIUM,
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{Highlight, Layout, TextInput};
|
use super::{Highlight, Layout, TextInput};
|
||||||
use crate::theme::ActiveTheme;
|
use crate::{
|
||||||
|
primitives::{caret_bar, caret_width},
|
||||||
|
theme::ActiveTheme,
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) struct TextElement {
|
pub(crate) struct TextElement {
|
||||||
input: Entity<TextInput>,
|
input: Entity<TextInput>,
|
||||||
@@ -214,7 +217,10 @@ impl Element for TextElement {
|
|||||||
) -> Prepaint {
|
) -> Prepaint {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let (selection_color, caret_color) = (theme.colors.selection, theme.colors.focus);
|
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 input = self.input.read(cx);
|
||||||
let (text, placeholder, runs) = shown_runs(input, window, cx);
|
let (text, placeholder, runs) = shown_runs(input, window, cx);
|
||||||
let font_size = window.text_style().font_size.to_pixels(window.rem_size());
|
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
|
let caret = caret_at
|
||||||
.filter(|_| focused && caret_on && selection.is_empty())
|
.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);
|
self.input.update(cx, |input, _| input.scroll = scroll);
|
||||||
Prepaint {
|
Prepaint {
|
||||||
lines,
|
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 backdrop;
|
||||||
|
mod caret;
|
||||||
mod disclosure;
|
mod disclosure;
|
||||||
mod divider;
|
mod divider;
|
||||||
mod files;
|
mod files;
|
||||||
@@ -17,6 +18,7 @@ mod tooltip;
|
|||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
pub use backdrop::{Backdrop, Place};
|
pub use backdrop::{Backdrop, Place};
|
||||||
|
pub(crate) use caret::{caret_bar, caret_width};
|
||||||
pub use disclosure::Disclosure;
|
pub use disclosure::Disclosure;
|
||||||
pub use divider::Divider;
|
pub use divider::Divider;
|
||||||
pub use files::IconTheme;
|
pub use files::IconTheme;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use super::{
|
|||||||
links::Target,
|
links::Target,
|
||||||
view::Terminal,
|
view::Terminal,
|
||||||
};
|
};
|
||||||
|
use crate::primitives::caret_bar;
|
||||||
|
|
||||||
/// How a terminal paints: font and cell, ink, and the colors of what it marks.
|
/// How a terminal paints: font and cell, ink, and the colors of what it marks.
|
||||||
pub(crate) struct Look {
|
pub(crate) struct Look {
|
||||||
@@ -161,10 +162,11 @@ pub(crate) fn grid(
|
|||||||
(CursorShape::HollowBlock, _) | (_, false) => {
|
(CursorShape::HollowBlock, _) | (_, false) => {
|
||||||
outline(Bounds::new(origin, cell), look.caret, BorderStyle::Solid)
|
outline(Bounds::new(origin, cell), look.caret, BorderStyle::Solid)
|
||||||
}
|
}
|
||||||
(CursorShape::Beam, true) => fill(
|
(CursorShape::Beam, true) => {
|
||||||
Bounds::new(origin, size(look.caret_width, cell.height)),
|
let scale = window.scale_factor();
|
||||||
look.caret,
|
let bar = caret_bar(origin, look.caret_width, cell.height, scale);
|
||||||
),
|
fill(bar, look.caret)
|
||||||
|
}
|
||||||
(CursorShape::Underline, true) => fill(
|
(CursorShape::Underline, true) => fill(
|
||||||
Bounds::new(
|
Bounds::new(
|
||||||
origin + point(Pixels::ZERO, cell.height - look.caret_width),
|
origin + point(Pixels::ZERO, cell.height - look.caret_width),
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
# GitHub round, 2026-10-08
|
# GitHub round, 2026-10-08
|
||||||
|
|
||||||
- [x] G10 CI: `cargo fmt` on the files the last commits left unformatted
|
- [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
|
- [ ] 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
|
- [ ] 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
|
- [ ] 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