mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Powerline: Add half-circle rendering
This adds in-terminal rendering for the powerline glyphs E0B4 and E0B6, similar to how we are rendering the triangle shapes currently. The circle glyphs use a much more complex rendering due to the nuances of drawing them: we use a midpoint algorithm for drawing on a 4x supersampled matrix, fill, and then downsample. We use the same downsampling approach as is done in the arc box drawing code. The midpoint variant we're using here is described by Dennis Yurichev: https://yurichev.com/news/20220322_circle/, although there are similar variants elsewhere (some cited at the bottom of his article).
This commit is contained in:
@ -148,6 +148,8 @@ const Kind = enum {
|
||||
|
||||
// Powerline fonts
|
||||
0xE0B0,
|
||||
0xE0B4,
|
||||
0xE0B6,
|
||||
0xE0B2,
|
||||
0xE0B8,
|
||||
0xE0BA,
|
||||
|
@ -56,7 +56,7 @@ pub fn renderGlyph(
|
||||
defer canvas.deinit(alloc);
|
||||
|
||||
// Perform the actual drawing
|
||||
try self.draw(&canvas, cp);
|
||||
try self.draw(alloc, &canvas, cp);
|
||||
|
||||
// Write the drawing to the atlas
|
||||
const region = try canvas.writeAtlas(alloc, atlas);
|
||||
@ -77,7 +77,7 @@ pub fn renderGlyph(
|
||||
};
|
||||
}
|
||||
|
||||
fn draw(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void {
|
||||
fn draw(self: Powerline, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void {
|
||||
switch (cp) {
|
||||
// Hard dividers and triangles
|
||||
0xE0B0,
|
||||
@ -88,6 +88,11 @@ fn draw(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void {
|
||||
0xE0BE,
|
||||
=> try self.draw_wedge_triangle(canvas, cp),
|
||||
|
||||
// Half-circles
|
||||
0xE0B4,
|
||||
0xE0B6,
|
||||
=> try self.draw_half_circle(alloc, canvas, cp),
|
||||
|
||||
else => return error.InvalidCodepoint,
|
||||
}
|
||||
}
|
||||
@ -168,6 +173,156 @@ fn draw_wedge_triangle(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !v
|
||||
}, .on);
|
||||
}
|
||||
|
||||
fn draw_half_circle(self: Powerline, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void {
|
||||
const supersample = 4;
|
||||
|
||||
// We make a canvas big enough for the whole circle, with the supersample
|
||||
// applied.
|
||||
const width = self.width * 2 * supersample;
|
||||
const height = self.height * supersample;
|
||||
|
||||
// We set a minimum super-sampled canvas to assert on. The minimum cell
|
||||
// size is 1x3px, and this looked safe in empirical testing.
|
||||
std.debug.assert(width >= 8); // 1 * 2 * 4
|
||||
std.debug.assert(height >= 12); // 3 * 4
|
||||
|
||||
const center_x = width / 2 - 1;
|
||||
const center_y = height / 2 - 1;
|
||||
|
||||
// Our radius.
|
||||
const radius = @min(width, height) / 2;
|
||||
|
||||
// Pre-allocate a matrix to plot the points on.
|
||||
const cap = height * width;
|
||||
var points = try alloc.alloc(u8, cap);
|
||||
defer alloc.free(points);
|
||||
@memset(points, 0);
|
||||
{
|
||||
// Using a midpoint algorithm.
|
||||
// As explained on https://yurichev.com/news/20220322_circle/ and
|
||||
// other sites
|
||||
var x: i32 = @intCast(radius);
|
||||
var y: i32 = 0;
|
||||
var radius_err: i32 = 0;
|
||||
|
||||
const cx: i32 = @intCast(center_x);
|
||||
const cy: i32 = @intCast(center_y);
|
||||
|
||||
while (x >= y) {
|
||||
// Right side
|
||||
const x1 = @max(0, cx + x);
|
||||
const y1 = @max(0, cy + y);
|
||||
const x2 = @max(0, cx + x);
|
||||
const y2 = @max(0, cy - y);
|
||||
const x3 = @max(0, cx + y);
|
||||
const y3 = @max(0, cy + x);
|
||||
const x4 = @max(0, cx + y);
|
||||
const y4 = @max(0, cy - x);
|
||||
|
||||
// Left side
|
||||
const x5 = @max(0, cx - x);
|
||||
const y5 = @max(0, cy + y);
|
||||
const x6 = @max(0, cx - x);
|
||||
const y6 = @max(0, cy - y);
|
||||
const x7 = @max(0, cx - y);
|
||||
const y7 = @max(0, cy + x);
|
||||
const x8 = @max(0, cx - y);
|
||||
const y8 = @max(0, cy - x);
|
||||
|
||||
// Points
|
||||
const p1 = y1 * width + x1;
|
||||
const p2 = y2 * width + x2;
|
||||
const p3 = y3 * width + x3;
|
||||
const p4 = y4 * width + x4;
|
||||
const p5 = y5 * width + x5;
|
||||
const p6 = y6 * width + x6;
|
||||
const p7 = y7 * width + x7;
|
||||
const p8 = y8 * width + x8;
|
||||
|
||||
// Set the points in the matrix, ignore any out of bounds
|
||||
if (p1 < cap) points[p1] = 0xFF;
|
||||
if (p2 < cap) points[p2] = 0xFF;
|
||||
if (p3 < cap) points[p3] = 0xFF;
|
||||
if (p4 < cap) points[p4] = 0xFF;
|
||||
if (p5 < cap) points[p5] = 0xFF;
|
||||
if (p6 < cap) points[p6] = 0xFF;
|
||||
if (p7 < cap) points[p7] = 0xFF;
|
||||
if (p8 < cap) points[p8] = 0xFF;
|
||||
|
||||
// Calculate next pixels based on midpoint bounds
|
||||
y += 1;
|
||||
radius_err += 2 * y + 1;
|
||||
if (radius_err > 0) {
|
||||
x -= 1;
|
||||
radius_err -= 2 * x + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fill
|
||||
{
|
||||
const u_height: u32 = @intCast(height);
|
||||
const u_width: u32 = @intCast(width);
|
||||
|
||||
for (0..u_height) |yf| {
|
||||
for (0..u_width) |left| {
|
||||
// Count forward from the left to the first filled pixel
|
||||
if (points[yf * u_width + left] != 0) {
|
||||
// Count back to our left point from the right to the first
|
||||
// filled pixel on the other side.
|
||||
var right: usize = u_width - 1;
|
||||
while (right > left) : (right -= 1) {
|
||||
if (points[yf * u_width + right] != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Start filling 1 index after the left and go until we hit
|
||||
// the right; this will be a no-op if the line length is <
|
||||
// 3 as both left and right will have already been filled.
|
||||
const start = yf * u_width + left;
|
||||
const end = yf * u_width + right;
|
||||
if (end - start >= 3) {
|
||||
for (start + 1..end) |idx| {
|
||||
points[idx] = 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we have our points, we need to "split" our matrix on the x
|
||||
// axis for the downsample.
|
||||
{
|
||||
// The side of the circle we're drawing
|
||||
const offset_j: u32 = if (cp == 0xE0B4) center_x + 1 else 0;
|
||||
|
||||
for (0..self.height) |r| {
|
||||
for (0..self.width) |c| {
|
||||
var total: u32 = 0;
|
||||
for (0..supersample) |i| {
|
||||
for (0..supersample) |j| {
|
||||
const idx = (r * supersample + i) * width + (c * supersample + j + offset_j);
|
||||
total += points[idx];
|
||||
}
|
||||
}
|
||||
|
||||
const average = @as(u8, @intCast(@min(total / (supersample * supersample), 0xFF)));
|
||||
canvas.rect(
|
||||
.{
|
||||
.x = @intCast(c),
|
||||
.y = @intCast(r),
|
||||
.width = 1,
|
||||
.height = 1,
|
||||
},
|
||||
@as(font.sprite.Color, @enumFromInt(average)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "all" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
@ -179,6 +334,8 @@ test "all" {
|
||||
0xE0BA,
|
||||
0xE0BC,
|
||||
0xE0BE,
|
||||
0xE0B4,
|
||||
0xE0B6,
|
||||
};
|
||||
for (cps) |cp| {
|
||||
var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale);
|
||||
|
Reference in New Issue
Block a user