From 4d158844028e1f7413ead2a38efbd789cd0a4bd2 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 1 Apr 2026 14:40:23 +0200 Subject: [PATCH] fix(gui): render block drawing glyphs with no anti aliasing --- src/gui/rasterizer/truetype.zig | 114 +++++++++++++++++++++++++++++--- 1 file changed, 104 insertions(+), 10 deletions(-) diff --git a/src/gui/rasterizer/truetype.zig b/src/gui/rasterizer/truetype.zig index aa0c2114..a2e9e28c 100644 --- a/src/gui/rasterizer/truetype.zig +++ b/src/gui/rasterizer/truetype.zig @@ -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+2580–U+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+2580–U+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; +}