mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
fix(terminal): eliminate seams between Powerline separators (#336)
* fix(terminal): eliminate seams between Powerline separators * fix(terminal): skip the separator cover quad when the glyph is dim The cover quad and the anti-aliased path overlap on the closing edge's device pixel. With an opaque foreground that is a no-op, but a DIM cell carries fg.a = 0.66, so the two compositing passes push that one column to 1 - 0.34^2 = 0.884 alpha and tint the neighboring cell's background. Emit the quad only for opaque separators. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
l0ng-ai
parent
a1d89a3752
commit
47aa6532f5
+160
-1
@@ -659,6 +659,21 @@ impl PowerlineShape {
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn closing_edge_x(self, bounds: Bounds<Pixels>) -> Pixels {
|
||||
// Keep this match exhaustive so adding a shape cannot silently assign
|
||||
// its solid closing edge to the wrong side of the cell.
|
||||
match self {
|
||||
Self::TriangleRight
|
||||
| Self::HalfCircleRight
|
||||
| Self::SlantLowerLeft
|
||||
| Self::SlantUpperLeft => bounds.left(),
|
||||
Self::TriangleLeft
|
||||
| Self::HalfCircleLeft
|
||||
| Self::SlantLowerRight
|
||||
| Self::SlantUpperRight => bounds.right(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn powerline_path(bounds: Bounds<Pixels>, shape: PowerlineShape) -> gpui::Path<Pixels> {
|
||||
@@ -694,6 +709,40 @@ fn powerline_path(bounds: Bounds<Pixels>, shape: PowerlineShape) -> gpui::Path<P
|
||||
}
|
||||
}
|
||||
|
||||
fn powerline_solid_edge(
|
||||
bounds: Bounds<Pixels>,
|
||||
shape: PowerlineShape,
|
||||
scale_factor: f32,
|
||||
fg_alpha: f32,
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
// A filled path anti-aliases a closing edge that falls between device
|
||||
// pixels. Cover exactly that partially occupied pixel with opaque
|
||||
// foreground color; an already aligned edge needs no extra primitive.
|
||||
//
|
||||
// Only opaque separators qualify. A translucent one (DIM) would composite
|
||||
// the cover quad and the path on top of each other, pushing that single
|
||||
// column past the glyph's own alpha and tinting the neighboring cell.
|
||||
if !fg_alpha.is_finite() || fg_alpha < 1. {
|
||||
return None;
|
||||
}
|
||||
let scale_factor = if scale_factor.is_finite() && scale_factor > 0. {
|
||||
scale_factor
|
||||
} else {
|
||||
1.
|
||||
};
|
||||
let physical_edge = shape.closing_edge_x(bounds).as_f32() * scale_factor;
|
||||
let physical_left = physical_edge.floor();
|
||||
let physical_right = physical_edge.ceil();
|
||||
if physical_left == physical_right {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Bounds::from_corners(
|
||||
point(px(physical_left / scale_factor), bounds.top()),
|
||||
point(px(physical_right / scale_factor), bounds.bottom()),
|
||||
))
|
||||
}
|
||||
|
||||
fn native_cell_residue(style: &GlyphStyle) -> Option<char> {
|
||||
// Special underlines are painted directly from the cell buffer, so a
|
||||
// shaped blank is only needed for decorations GPUI owns.
|
||||
@@ -764,8 +813,14 @@ fn paint_glyphs(
|
||||
size(geom.cell_width, geom.line_height),
|
||||
);
|
||||
let native = if let Some(shape) = PowerlineShape::of(cell.c) {
|
||||
let fg = GlyphStyle::of(cell).fg;
|
||||
if let Some(edge) =
|
||||
powerline_solid_edge(cell_bounds, shape, window.scale_factor(), fg.a)
|
||||
{
|
||||
window.paint_quad(fill(edge, fg));
|
||||
}
|
||||
let path = powerline_path(cell_bounds, shape);
|
||||
window.paint_path(path, GlyphStyle::of(cell).fg);
|
||||
window.paint_path(path, fg);
|
||||
true
|
||||
} else if let Some(ink) =
|
||||
super::boxdraw::glyph(cell.c, cell_bounds, window.scale_factor())
|
||||
@@ -1952,6 +2007,110 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn powerline_solid_edge_covers_only_the_fractional_device_pixel() {
|
||||
let bounds = Bounds::new(point(px(10.2), px(20.)), size(px(9.2), px(21.)));
|
||||
let scale = 1.25;
|
||||
|
||||
for shape in [
|
||||
PowerlineShape::TriangleRight,
|
||||
PowerlineShape::HalfCircleRight,
|
||||
PowerlineShape::SlantLowerLeft,
|
||||
PowerlineShape::SlantUpperLeft,
|
||||
] {
|
||||
assert_eq!(
|
||||
powerline_solid_edge(bounds, shape, scale, 1.),
|
||||
Some(Bounds::from_corners(
|
||||
point(px(12. / scale), bounds.top()),
|
||||
point(px(13. / scale), bounds.bottom()),
|
||||
)),
|
||||
"{shape:?} must cover only the device pixel containing its left edge"
|
||||
);
|
||||
}
|
||||
|
||||
for shape in [
|
||||
PowerlineShape::TriangleLeft,
|
||||
PowerlineShape::HalfCircleLeft,
|
||||
PowerlineShape::SlantLowerRight,
|
||||
PowerlineShape::SlantUpperRight,
|
||||
] {
|
||||
assert_eq!(
|
||||
powerline_solid_edge(bounds, shape, scale, 1.),
|
||||
Some(Bounds::from_corners(
|
||||
point(px(24. / scale), bounds.top()),
|
||||
point(px(25. / scale), bounds.bottom()),
|
||||
)),
|
||||
"{shape:?} must cover only the device pixel containing its right edge"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn powerline_solid_edge_skips_device_aligned_edges() {
|
||||
let bounds = Bounds::new(point(px(10.), px(20.)), size(px(9.), px(21.)));
|
||||
|
||||
for shape in [
|
||||
PowerlineShape::TriangleRight,
|
||||
PowerlineShape::TriangleLeft,
|
||||
PowerlineShape::HalfCircleRight,
|
||||
PowerlineShape::HalfCircleLeft,
|
||||
PowerlineShape::SlantLowerLeft,
|
||||
PowerlineShape::SlantLowerRight,
|
||||
PowerlineShape::SlantUpperLeft,
|
||||
PowerlineShape::SlantUpperRight,
|
||||
] {
|
||||
assert_eq!(
|
||||
powerline_solid_edge(bounds, shape, 2., 1.),
|
||||
None,
|
||||
"{shape:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn powerline_solid_edge_sanitizes_invalid_scale() {
|
||||
let bounds = Bounds::new(point(px(10.25), px(20.)), size(px(9.), px(21.)));
|
||||
let expected = Some(Bounds::from_corners(
|
||||
point(px(10.), bounds.top()),
|
||||
point(px(11.), bounds.bottom()),
|
||||
));
|
||||
|
||||
for scale in [0., -1., f32::NAN] {
|
||||
assert_eq!(
|
||||
powerline_solid_edge(bounds, PowerlineShape::TriangleRight, scale, 1.),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn powerline_solid_edge_skips_translucent_separators() {
|
||||
let bounds = Bounds::new(point(px(10.2), px(20.)), size(px(9.2), px(21.)));
|
||||
|
||||
for shape in [
|
||||
PowerlineShape::TriangleRight,
|
||||
PowerlineShape::TriangleLeft,
|
||||
PowerlineShape::HalfCircleRight,
|
||||
PowerlineShape::HalfCircleLeft,
|
||||
PowerlineShape::SlantLowerLeft,
|
||||
PowerlineShape::SlantLowerRight,
|
||||
PowerlineShape::SlantUpperLeft,
|
||||
PowerlineShape::SlantUpperRight,
|
||||
] {
|
||||
assert!(
|
||||
powerline_solid_edge(bounds, shape, 1.25, 1.).is_some(),
|
||||
"{shape:?} still needs the cover quad when opaque"
|
||||
);
|
||||
for alpha in [DIM_OPACITY, 0., 0.99, f32::NAN] {
|
||||
assert_eq!(
|
||||
powerline_solid_edge(bounds, shape, 1.25, alpha),
|
||||
None,
|
||||
"{shape:?} must not stack a cover quad under a translucent path"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seg_clip_width_frees_solo_symbols_but_pins_batched_runs() {
|
||||
let cell = px(10.);
|
||||
|
||||
Reference in New Issue
Block a user