diff --git a/src/terminal/element.rs b/src/terminal/element.rs index 38318813..6655976a 100644 --- a/src/terminal/element.rs +++ b/src/terminal/element.rs @@ -659,6 +659,21 @@ impl PowerlineShape { _ => return None, }) } + + fn closing_edge_x(self, bounds: Bounds) -> 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, shape: PowerlineShape) -> gpui::Path { @@ -694,6 +709,40 @@ fn powerline_path(bounds: Bounds, shape: PowerlineShape) -> gpui::Path

, + shape: PowerlineShape, + scale_factor: f32, + fg_alpha: f32, +) -> Option> { + // 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 { // 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.);