feat(gui): add geometric rendering of more box and line drawing symbols

This commit is contained in:
CJ van den Berg 2026-04-03 12:06:09 +02:00
parent 4d15884402
commit 4dba57ebab
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9

View file

@ -101,11 +101,13 @@ pub fn render(
const cw: i32 = @intCast(font.cell_size.x); const cw: i32 = @intCast(font.cell_size.x);
const ch: i32 = @intCast(font.cell_size.y); const ch: i32 = @intCast(font.cell_size.y);
// Block element characters (U+2580U+259F) are rasterized geometrically // Block element characters, box-drawing, and related Unicode symbols are
// rather than through the TrueType anti-aliasing path. Anti-aliased edges // rasterized geometrically rather than through the TrueType anti-aliasing path.
// produce partial-alpha pixels at cell boundaries, creating visible seams // Anti-aliased edges produce partial-alpha pixels at cell boundaries, creating
// between adjacent cells when fg bg. // visible seams between adjacent cells when fg bg.
if (renderBlockElement(codepoint, staging_buf, buf_w, buf_h, x_offset, cw, ch)) return; if (renderBlockElement(codepoint, staging_buf, buf_w, buf_h, x_offset, cw, ch)) return;
if (renderBoxDrawing(codepoint, staging_buf, buf_w, buf_h, x_offset, cw, ch)) return;
if (renderExtendedBlocks(codepoint, staging_buf, buf_w, buf_h, x_offset, cw, ch)) return;
var arena = std.heap.ArenaAllocator.init(self.allocator); var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit(); defer arena.deinit();
@ -220,6 +222,312 @@ fn renderBlockElement(
}, },
0x2594 => fillRect(buf, buf_w, buf_h, x0, 0, x1, @divTrunc(ch, 8)), // upper 1/8 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 0x2595 => fillRect(buf, buf_w, buf_h, x1 - @divTrunc(cw, 8), 0, x1, ch), // right 1/8
0x2596 => fillRect(buf, buf_w, buf_h, x0, half_h, mid_x, ch), // lower-left quadrant
0x2597 => fillRect(buf, buf_w, buf_h, mid_x, half_h, x1, ch), // lower-right quadrant
0x2598 => fillRect(buf, buf_w, buf_h, x0, 0, mid_x, half_h), // upper-left quadrant
0x2599 => { // upper-left + lower-left + lower-right
fillRect(buf, buf_w, buf_h, x0, 0, mid_x, ch);
fillRect(buf, buf_w, buf_h, mid_x, half_h, x1, ch);
},
0x259A => { // upper-left + lower-right (diagonal)
fillRect(buf, buf_w, buf_h, x0, 0, mid_x, half_h);
fillRect(buf, buf_w, buf_h, mid_x, half_h, x1, ch);
},
0x259B => { // upper-left + upper-right + lower-left
fillRect(buf, buf_w, buf_h, x0, 0, x1, half_h);
fillRect(buf, buf_w, buf_h, x0, half_h, mid_x, ch);
},
0x259C => { // upper-left + upper-right + lower-right
fillRect(buf, buf_w, buf_h, x0, 0, x1, half_h);
fillRect(buf, buf_w, buf_h, mid_x, half_h, x1, ch);
},
0x259D => fillRect(buf, buf_w, buf_h, mid_x, 0, x1, half_h), // upper-right quadrant
0x259E => { // upper-right + lower-left (diagonal)
fillRect(buf, buf_w, buf_h, mid_x, 0, x1, half_h);
fillRect(buf, buf_w, buf_h, x0, half_h, mid_x, ch);
},
0x259F => { // upper-right + lower-left + lower-right
fillRect(buf, buf_w, buf_h, mid_x, 0, x1, half_h);
fillRect(buf, buf_w, buf_h, x0, half_h, x1, ch);
},
else => return false,
}
return true;
}
/// Render a box-drawing character (U+2500U+257F) geometrically.
fn renderBoxDrawing(
cp: u21,
buf: []u8,
buf_w: i32,
buf_h: i32,
x0: i32,
cw: i32,
ch: i32,
) bool {
if (cp < 0x2500 or cp > 0x257F) return false;
const x1 = x0 + cw;
// Single-line stroke thickness: ~1/8 of cell, minimum 1 pixel
const lh: i32 = @max(1, @divTrunc(ch, 8));
const lw: i32 = @max(1, @divTrunc(cw, 8));
// Single-line center positions
const hy0: i32 = @divTrunc(ch - lh, 2);
const hy1: i32 = hy0 + lh;
const vx0: i32 = x0 + @divTrunc(cw - lw, 2);
const vx1: i32 = vx0 + lw;
// Double-line: two strokes offset from center by doff each side.
// doff is chosen so the two strokes fit symmetrically without overlap.
const doff_h: i32 = @max(lh + 1, @divTrunc(ch, 6));
const doff_w: i32 = @max(lw + 1, @divTrunc(cw, 6));
// Horizontal double strokes (top = closer to top of cell):
const dhy0t: i32 = @divTrunc(ch, 2) - doff_h;
const dhy1t: i32 = dhy0t + lh;
const dhy0b: i32 = @divTrunc(ch, 2) + doff_h - lh;
const dhy1b: i32 = dhy0b + lh;
// Vertical double strokes (left = closer to left of cell):
const dvx0l: i32 = x0 + @divTrunc(cw, 2) - doff_w;
const dvx1l: i32 = dvx0l + lw;
const dvx0r: i32 = x0 + @divTrunc(cw, 2) + doff_w - lw;
const dvx1r: i32 = dvx0r + lw;
switch (cp) {
// light horizontal
0x2500 => fillRect(buf, buf_w, buf_h, x0, hy0, x1, hy1),
// light vertical
0x2502 => fillRect(buf, buf_w, buf_h, vx0, 0, vx1, ch),
// down+right (NW corner)
0x250C => {
fillRect(buf, buf_w, buf_h, vx0, hy0, x1, hy1);
fillRect(buf, buf_w, buf_h, vx0, hy0, vx1, ch);
},
// down+left (NE corner)
0x2510 => {
fillRect(buf, buf_w, buf_h, x0, hy0, vx1, hy1);
fillRect(buf, buf_w, buf_h, vx0, hy0, vx1, ch);
},
// up+right (SW corner)
0x2514 => {
fillRect(buf, buf_w, buf_h, vx0, hy0, x1, hy1);
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, hy1);
},
// up+left (SE corner)
0x2518 => {
fillRect(buf, buf_w, buf_h, x0, hy0, vx1, hy1);
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, hy1);
},
// vertical + right
0x251C => {
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, ch);
fillRect(buf, buf_w, buf_h, vx0, hy0, x1, hy1);
},
// vertical + left
0x2524 => {
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, ch);
fillRect(buf, buf_w, buf_h, x0, hy0, vx1, hy1);
},
// horizontal + down
0x252C => {
fillRect(buf, buf_w, buf_h, x0, hy0, x1, hy1);
fillRect(buf, buf_w, buf_h, vx0, hy0, vx1, ch);
},
// horizontal + up
0x2534 => {
fillRect(buf, buf_w, buf_h, x0, hy0, x1, hy1);
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, hy1);
},
// cross
0x253C => {
fillRect(buf, buf_w, buf_h, x0, hy0, x1, hy1);
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, ch);
},
// double horizontal
0x2550 => {
fillRect(buf, buf_w, buf_h, x0, dhy0t, x1, dhy1t);
fillRect(buf, buf_w, buf_h, x0, dhy0b, x1, dhy1b);
},
// double vertical
0x2551 => {
fillRect(buf, buf_w, buf_h, dvx0l, 0, dvx1l, ch);
fillRect(buf, buf_w, buf_h, dvx0r, 0, dvx1r, ch);
},
// double NW corner (down+right)
0x2554 => {
fillRect(buf, buf_w, buf_h, dvx0l, dhy0t, x1, dhy1t); // outer horiz
fillRect(buf, buf_w, buf_h, dvx0r, dhy0b, x1, dhy1b); // inner horiz
fillRect(buf, buf_w, buf_h, dvx0l, dhy0t, dvx1l, ch); // outer vert
fillRect(buf, buf_w, buf_h, dvx0r, dhy0b, dvx1r, ch); // inner vert
},
// double NE corner (down+left)
0x2557 => {
fillRect(buf, buf_w, buf_h, x0, dhy0t, dvx1r, dhy1t); // outer horiz
fillRect(buf, buf_w, buf_h, x0, dhy0b, dvx1l, dhy1b); // inner horiz
fillRect(buf, buf_w, buf_h, dvx0r, dhy0t, dvx1r, ch); // outer vert
fillRect(buf, buf_w, buf_h, dvx0l, dhy0b, dvx1l, ch); // inner vert
},
// double SW corner (up+right)
0x255A => {
fillRect(buf, buf_w, buf_h, dvx0l, dhy0t, x1, dhy1t); // outer horiz
fillRect(buf, buf_w, buf_h, dvx0r, dhy0b, x1, dhy1b); // inner horiz
fillRect(buf, buf_w, buf_h, dvx0l, 0, dvx1l, dhy1t); // outer vert
fillRect(buf, buf_w, buf_h, dvx0r, 0, dvx1r, dhy1b); // inner vert
},
// double SE corner (up+left)
0x255D => {
fillRect(buf, buf_w, buf_h, x0, dhy0t, dvx1r, dhy1t); // outer horiz
fillRect(buf, buf_w, buf_h, x0, dhy0b, dvx1l, dhy1b); // inner horiz
fillRect(buf, buf_w, buf_h, dvx0r, 0, dvx1r, dhy1t); // outer vert
fillRect(buf, buf_w, buf_h, dvx0l, 0, dvx1l, dhy1b); // inner vert
},
// single vert + double right
0x255E => {
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, ch);
fillRect(buf, buf_w, buf_h, vx0, dhy0t, x1, dhy1t);
fillRect(buf, buf_w, buf_h, vx0, dhy0b, x1, dhy1b);
},
// single vert + double left
0x2561 => {
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, ch);
fillRect(buf, buf_w, buf_h, x0, dhy0t, vx1, dhy1t);
fillRect(buf, buf_w, buf_h, x0, dhy0b, vx1, dhy1b);
},
// down single, right double
0x2552 => {
fillRect(buf, buf_w, buf_h, vx0, dhy0t, vx1, ch); // single vert
fillRect(buf, buf_w, buf_h, vx0, dhy0t, x1, dhy1t); // outer horiz
fillRect(buf, buf_w, buf_h, vx0, dhy0b, x1, dhy1b); // inner horiz
},
// down single, left double
0x2555 => {
fillRect(buf, buf_w, buf_h, vx0, dhy0t, vx1, ch); // single vert
fillRect(buf, buf_w, buf_h, x0, dhy0t, vx1, dhy1t); // outer horiz
fillRect(buf, buf_w, buf_h, x0, dhy0b, vx1, dhy1b); // inner horiz
},
// up single, right double
0x2558 => {
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, dhy1b); // single vert
fillRect(buf, buf_w, buf_h, vx0, dhy0t, x1, dhy1t); // outer horiz
fillRect(buf, buf_w, buf_h, vx0, dhy0b, x1, dhy1b); // inner horiz
},
// up single, left double
0x255B => {
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, dhy1b); // single vert
fillRect(buf, buf_w, buf_h, x0, dhy0t, vx1, dhy1t); // outer horiz
fillRect(buf, buf_w, buf_h, x0, dhy0b, vx1, dhy1b); // inner horiz
},
// down double, right single
0x2553 => {
fillRect(buf, buf_w, buf_h, dvx0l, hy0, x1, hy1); // single horiz
fillRect(buf, buf_w, buf_h, dvx0l, hy0, dvx1l, ch); // left double vert
fillRect(buf, buf_w, buf_h, dvx0r, hy0, dvx1r, ch); // right double vert
},
// down double, left single
0x2556 => {
fillRect(buf, buf_w, buf_h, x0, hy0, dvx1r, hy1); // single horiz
fillRect(buf, buf_w, buf_h, dvx0l, hy0, dvx1l, ch); // left double vert
fillRect(buf, buf_w, buf_h, dvx0r, hy0, dvx1r, ch); // right double vert
},
// up double, right single
0x2559 => {
fillRect(buf, buf_w, buf_h, dvx0l, hy0, x1, hy1); // single horiz
fillRect(buf, buf_w, buf_h, dvx0l, 0, dvx1l, hy1); // left double vert
fillRect(buf, buf_w, buf_h, dvx0r, 0, dvx1r, hy1); // right double vert
},
// up double, left single
0x255C => {
fillRect(buf, buf_w, buf_h, x0, hy0, dvx1r, hy1); // single horiz
fillRect(buf, buf_w, buf_h, dvx0l, 0, dvx1l, hy1); // left double vert
fillRect(buf, buf_w, buf_h, dvx0r, 0, dvx1r, hy1); // right double vert
},
// round down+right = , round down+left =
// round up+left = , round up+right =
0x256D => {
fillRect(buf, buf_w, buf_h, vx0, hy0, x1, hy1);
fillRect(buf, buf_w, buf_h, vx0, hy0, vx1, ch);
},
0x256E => {
fillRect(buf, buf_w, buf_h, x0, hy0, vx1, hy1);
fillRect(buf, buf_w, buf_h, vx0, hy0, vx1, ch);
},
0x256F => {
fillRect(buf, buf_w, buf_h, x0, hy0, vx1, hy1);
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, hy1);
},
0x2570 => {
fillRect(buf, buf_w, buf_h, vx0, hy0, x1, hy1);
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, hy1);
},
else => return false,
}
return true;
}
/// Render extended block characters: U+1FB82 (upper quarter block), and the
/// specific sextant/octant corner characters used by WidgetStyle thick-box borders.
/// Each character is rendered with the geometric shape that tiles correctly with
/// its adjacent border characters (, , , , 🮂, ).
fn renderExtendedBlocks(
cp: u21,
buf: []u8,
buf_w: i32,
buf_h: i32,
x0: i32,
cw: i32,
ch: i32,
) bool {
const x1 = x0 + cw;
const mid_x = x0 + @divTrunc(cw, 2);
const qh = @divTrunc(ch, 4); // quarter height (for octant thick-box)
const th = @divTrunc(ch, 3); // third height (for sextant thick-box)
switch (cp) {
// 🮂 U+1FB82 upper one-quarter block (north edge of octant thick-box)
0x1FB82 => fillRect(buf, buf_w, buf_h, x0, 0, x1, qh),
// Sextant thick-box characters (WidgetStyle "thick box (sextant)")
// .n = 🬂, .s = 🬭, .nw = 🬕, .ne = 🬨, .sw = 🬲, .se = 🬷
// Edges connect to (left half) and (right half) for left/right walls.
0x1FB02 => fillRect(buf, buf_w, buf_h, x0, 0, x1, th), // 🬂 top third (N edge)
0x1FB2D => fillRect(buf, buf_w, buf_h, x0, ch - th, x1, ch), // 🬭 bottom third (S edge)
0x1FB15 => { // 🬕 NW corner: left-half + top-third
fillRect(buf, buf_w, buf_h, x0, 0, x1, th);
fillRect(buf, buf_w, buf_h, x0, th, mid_x, ch);
},
0x1FB28 => { // 🬨 NE corner: right-half + top-third
fillRect(buf, buf_w, buf_h, x0, 0, x1, th);
fillRect(buf, buf_w, buf_h, mid_x, th, x1, ch);
},
0x1FB32 => { // 🬲 SW corner: left-half + bottom-third
fillRect(buf, buf_w, buf_h, x0, 0, mid_x, ch - th);
fillRect(buf, buf_w, buf_h, x0, ch - th, x1, ch);
},
0x1FB37 => { // 🬷 SE corner: right-half + bottom-third
fillRect(buf, buf_w, buf_h, mid_x, 0, x1, ch - th);
fillRect(buf, buf_w, buf_h, x0, ch - th, x1, ch);
},
// Octant thick-box corner characters (WidgetStyle "thick box (octant)")
// .n = 🮂 (qh), .s = (qh), .w = (half), .e = (half)
0x1CD4A => { // 𜵊 NW corner: left-half + top-quarter
fillRect(buf, buf_w, buf_h, x0, 0, x1, qh);
fillRect(buf, buf_w, buf_h, x0, qh, mid_x, ch);
},
0x1CD98 => { // 𜶘 NE corner: right-half + top-quarter
fillRect(buf, buf_w, buf_h, x0, 0, x1, qh);
fillRect(buf, buf_w, buf_h, mid_x, qh, x1, ch);
},
0x1CDD5 => { // 𜷕 SE corner: right-half + bottom-quarter
fillRect(buf, buf_w, buf_h, mid_x, 0, x1, ch - qh);
fillRect(buf, buf_w, buf_h, x0, ch - qh, x1, ch);
},
0x1CDDC => { // 𜷀 SW corner: left-half + bottom-quarter
fillRect(buf, buf_w, buf_h, x0, 0, mid_x, ch - qh);
fillRect(buf, buf_w, buf_h, x0, ch - qh, x1, ch);
},
else => return false, else => return false,
} }
return true; return true;