fix(gui): render block drawing glyphs with no anti aliasing

This commit is contained in:
CJ van den Berg 2026-04-01 14:40:23 +02:00
parent 6954983d40
commit 4d15884402
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9

View file

@ -90,6 +90,23 @@ pub fn render(
kind: GlyphKind,
staging_buf: []u8,
) void {
// Always use 2*cell_w as the row stride so it matches the staging buffer
// width allocated by generateGlyph (which always allocates 2*cell_w wide).
const buf_w: i32 = @as(i32, @intCast(font.cell_size.x)) * 2;
const buf_h: i32 = @intCast(font.cell_size.y);
const x_offset: i32 = switch (kind) {
.single, .left => 0,
.right => @intCast(font.cell_size.x),
};
const cw: i32 = @intCast(font.cell_size.x);
const ch: i32 = @intCast(font.cell_size.y);
// Block element characters (U+2580U+259F) are rasterized geometrically
// rather than through the TrueType anti-aliasing path. Anti-aliased edges
// produce partial-alpha pixels at cell boundaries, creating visible seams
// between adjacent cells when fg bg.
if (renderBlockElement(codepoint, staging_buf, buf_w, buf_h, x_offset, cw, ch)) return;
var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit();
const alloc = arena.allocator();
@ -103,16 +120,6 @@ pub fn render(
if (dims.width == 0 or dims.height == 0) return;
// Always use 2*cell_w as the row stride so it matches the staging buffer
// width allocated by generateGlyph (which always allocates 2*cell_w wide).
const buf_w: i32 = @as(i32, @intCast(font.cell_size.x)) * 2;
const buf_h: i32 = @intCast(font.cell_size.y);
const x_offset: i32 = switch (kind) {
.single, .left => 0,
.right => @intCast(font.cell_size.x),
};
for (0..dims.height) |row| {
const dst_y: i32 = font.ascent_px + @as(i32, dims.off_y) + @as(i32, @intCast(row));
if (dst_y < 0 or dst_y >= buf_h) continue;
@ -130,3 +137,90 @@ pub fn render(
}
}
}
/// Fill a solid rectangle [x0, x1) × [y0, y1) in the staging buffer.
fn fillRect(buf: []u8, buf_w: i32, buf_h: i32, x0: i32, y0: i32, x1: i32, y1: i32) void {
const cx0 = @max(0, x0);
const cy0 = @max(0, y0);
const cx1 = @min(buf_w, x1);
const cy1 = @min(buf_h, y1);
if (cx0 >= cx1 or cy0 >= cy1) return;
var y = cy0;
while (y < cy1) : (y += 1) {
var x = cx0;
while (x < cx1) : (x += 1) {
buf[@intCast(y * buf_w + x)] = 255;
}
}
}
/// Render a block element character (U+2580U+259F) geometrically.
/// Returns true if the character was handled, false if it should fall through
/// to the normal TrueType rasterizer.
fn renderBlockElement(
cp: u21,
buf: []u8,
buf_w: i32,
buf_h: i32,
x0: i32,
cw: i32,
ch: i32,
) bool {
if (cp < 0x2580 or cp > 0x259F) return false;
const x1 = x0 + cw;
const half_w = @divTrunc(cw, 2);
const half_h = @divTrunc(ch, 2);
const mid_x = x0 + half_w;
switch (cp) {
0x2580 => fillRect(buf, buf_w, buf_h, x0, 0, x1, half_h), // upper half
0x2581 => fillRect(buf, buf_w, buf_h, x0, ch - @divTrunc(ch, 8), x1, ch), // lower 1/8
0x2582 => fillRect(buf, buf_w, buf_h, x0, ch - @divTrunc(ch, 4), x1, ch), // lower 1/4
0x2583 => fillRect(buf, buf_w, buf_h, x0, ch - @divTrunc(ch * 3, 8), x1, ch), // lower 3/8
0x2584 => fillRect(buf, buf_w, buf_h, x0, ch - half_h, x1, ch), // lower half
0x2585 => fillRect(buf, buf_w, buf_h, x0, ch - @divTrunc(ch * 5, 8), x1, ch), // lower 5/8
0x2586 => fillRect(buf, buf_w, buf_h, x0, ch - @divTrunc(ch * 3, 4), x1, ch), // lower 3/4
0x2587 => fillRect(buf, buf_w, buf_h, x0, ch - @divTrunc(ch * 7, 8), x1, ch), // lower 7/8
0x2588 => fillRect(buf, buf_w, buf_h, x0, 0, x1, ch), // full block
0x2589 => fillRect(buf, buf_w, buf_h, x0, 0, x0 + @divTrunc(cw * 7, 8), ch), // left 7/8
0x258A => fillRect(buf, buf_w, buf_h, x0, 0, x0 + @divTrunc(cw * 3, 4), ch), // left 3/4
0x258B => fillRect(buf, buf_w, buf_h, x0, 0, x0 + @divTrunc(cw * 5, 8), ch), // left 5/8
0x258C => fillRect(buf, buf_w, buf_h, x0, 0, mid_x, ch), // left half
0x258D => fillRect(buf, buf_w, buf_h, x0, 0, x0 + @divTrunc(cw * 3, 8), ch), // left 3/8
0x258E => fillRect(buf, buf_w, buf_h, x0, 0, x0 + @divTrunc(cw, 4), ch), // left 1/4
0x258F => fillRect(buf, buf_w, buf_h, x0, 0, x0 + @divTrunc(cw, 8), ch), // left 1/8
0x2590 => fillRect(buf, buf_w, buf_h, mid_x, 0, x1, ch), // right half
0x2591 => { // light shade approximate with alternating pixels
var y: i32 = 0;
while (y < ch) : (y += 1) {
var x = x0 + @mod(y, 2);
while (x < x1) : (x += 2) {
if (x >= 0 and x < buf_w and y >= 0 and y < buf_h)
buf[@intCast(y * buf_w + x)] = 255;
}
}
},
0x2592 => { // medium shade alternate rows fully/empty
var y: i32 = 0;
while (y < ch) : (y += 2) {
fillRect(buf, buf_w, buf_h, x0, y, x1, y + 1);
}
},
0x2593 => { // dark shade fill all except alternating sparse pixels
fillRect(buf, buf_w, buf_h, x0, 0, x1, ch);
var y: i32 = 0;
while (y < ch) : (y += 2) {
var x = x0 + @mod(y, 2);
while (x < x1) : (x += 2) {
if (x >= 0 and x < buf_w and y >= 0 and y < buf_h)
buf[@intCast(y * buf_w + x)] = 0;
}
}
},
0x2594 => fillRect(buf, buf_w, buf_h, x0, 0, x1, @divTrunc(ch, 8)), // upper 1/8
0x2595 => fillRect(buf, buf_w, buf_h, x1 - @divTrunc(cw, 8), 0, x1, ch), // right 1/8
else => return false,
}
return true;
}