feat(gui): add freetype rasterizer backend
This commit is contained in:
parent
1901696b7b
commit
d3aa7e17f5
8 changed files with 751 additions and 456 deletions
46
build.zig
46
build.zig
|
|
@ -559,24 +559,58 @@ pub fn build_exe(
|
|||
.optimize = optimize_deps,
|
||||
}) orelse break :blk tui_renderer_mod;
|
||||
|
||||
const geometric_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/gui/rasterizer/geometric.zig"),
|
||||
});
|
||||
|
||||
const font_finder_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/gui/rasterizer/font_finder.zig"),
|
||||
.target = target,
|
||||
});
|
||||
if (target.result.os.tag == .linux) {
|
||||
font_finder_mod.linkSystemLibrary("fontconfig", .{});
|
||||
font_finder_mod.link_libc = true;
|
||||
}
|
||||
|
||||
const truetype_rasterizer_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/gui/rasterizer/truetype.zig"),
|
||||
.target = target,
|
||||
.imports = &.{
|
||||
.{ .name = "TrueType", .module = tt_dep.module("TrueType") },
|
||||
.{ .name = "xy", .module = gui_xy_mod },
|
||||
.{ .name = "geometric", .module = geometric_mod },
|
||||
.{ .name = "font_finder", .module = font_finder_mod },
|
||||
},
|
||||
});
|
||||
|
||||
const freetype_rasterizer_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/gui/rasterizer/freetype.zig"),
|
||||
.target = target,
|
||||
.imports = &.{
|
||||
.{ .name = "xy", .module = gui_xy_mod },
|
||||
.{ .name = "geometric", .module = geometric_mod },
|
||||
.{ .name = "font_finder", .module = font_finder_mod },
|
||||
},
|
||||
});
|
||||
freetype_rasterizer_mod.linkSystemLibrary("freetype2", .{});
|
||||
freetype_rasterizer_mod.addIncludePath(.{ .cwd_relative = "/usr/include/freetype2" });
|
||||
freetype_rasterizer_mod.link_libc = true;
|
||||
|
||||
const combined_rasterizer_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/gui/rasterizer/combined.zig"),
|
||||
.imports = &.{
|
||||
.{ .name = "tt_rasterizer", .module = truetype_rasterizer_mod },
|
||||
.{ .name = "ft_rasterizer", .module = freetype_rasterizer_mod },
|
||||
.{ .name = "xy", .module = gui_xy_mod },
|
||||
.{ .name = "gui_config", .module = gui_config_mod },
|
||||
},
|
||||
});
|
||||
if (target.result.os.tag == .linux) {
|
||||
truetype_rasterizer_mod.linkSystemLibrary("fontconfig", .{});
|
||||
truetype_rasterizer_mod.link_libc = true;
|
||||
}
|
||||
|
||||
const gpu_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/gui/gpu/gpu.zig"),
|
||||
.imports = &.{
|
||||
.{ .name = "sokol", .module = sokol_mod },
|
||||
.{ .name = "rasterizer", .module = truetype_rasterizer_mod },
|
||||
.{ .name = "rasterizer", .module = combined_rasterizer_mod },
|
||||
.{ .name = "xy", .module = gui_xy_mod },
|
||||
.{ .name = "Cell", .module = gui_cell_mod },
|
||||
.{ .name = "GlyphIndexCache", .module = gui_glyph_cache_mod },
|
||||
|
|
@ -608,7 +642,7 @@ pub fn build_exe(
|
|||
.{ .name = "app", .module = app_mod },
|
||||
.{ .name = "tuirenderer", .module = tui_renderer_mod },
|
||||
.{ .name = "vaxis", .module = vaxis_mod },
|
||||
.{ .name = "rasterizer", .module = truetype_rasterizer_mod },
|
||||
.{ .name = "rasterizer", .module = combined_rasterizer_mod },
|
||||
},
|
||||
});
|
||||
break :blk mod;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const builtin_shader = @import("builtin.glsl.zig");
|
|||
|
||||
pub const Font = Rasterizer.Font;
|
||||
pub const GlyphKind = Rasterizer.GlyphKind;
|
||||
pub const RasterizerBackend = Rasterizer.Backend;
|
||||
pub const Cell = gui_cell.Cell;
|
||||
pub const Color = gui_cell.Rgba8;
|
||||
const Rgba8 = gui_cell.Rgba8;
|
||||
|
|
@ -99,6 +100,14 @@ pub fn loadFont(name: []const u8, size_px: u16) !Font {
|
|||
return global.rasterizer.loadFont(name, size_px);
|
||||
}
|
||||
|
||||
pub fn setRasterizerBackend(backend: RasterizerBackend) void {
|
||||
global.rasterizer.setBackend(backend);
|
||||
}
|
||||
|
||||
pub fn setFontWeight(font: *Font, w: u8) void {
|
||||
Rasterizer.setFontWeight(font, w);
|
||||
}
|
||||
|
||||
pub fn setBackground(color: Rgba8) void {
|
||||
global.background = color;
|
||||
}
|
||||
|
|
|
|||
86
src/gui/rasterizer/combined.zig
Normal file
86
src/gui/rasterizer/combined.zig
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/// Combined rasterizer — wraps TrueType and FreeType backends with runtime switching.
|
||||
/// Satisfies the GlyphRasterizer interface (see GlyphRasterizer.zig).
|
||||
///
|
||||
/// The active backend is changed via setBackend(). Callers must reload fonts
|
||||
/// after switching (the Font struct's .backend tag must match the active backend).
|
||||
const std = @import("std");
|
||||
const XY = @import("xy").XY;
|
||||
const TT = @import("tt_rasterizer");
|
||||
const FT = @import("ft_rasterizer");
|
||||
|
||||
pub const GlyphKind = TT.GlyphKind;
|
||||
pub const Fonts = struct {};
|
||||
pub const font_finder = TT.font_finder;
|
||||
|
||||
pub const Backend = @import("gui_config").RasterizerBackend;
|
||||
|
||||
/// Backend-specific font data.
|
||||
pub const BackendFont = union(Backend) {
|
||||
truetype: TT.Font,
|
||||
freetype: FT.Font,
|
||||
};
|
||||
|
||||
/// Combined font handle. `cell_size` is hoisted to the top level so all
|
||||
/// existing callers that do `font.cell_size.x / .y` continue to work without
|
||||
/// change. Backend-specific data lives in `backend`.
|
||||
pub const Font = struct {
|
||||
cell_size: XY(u16),
|
||||
backend: BackendFont,
|
||||
};
|
||||
|
||||
/// Set emboldening weight on a font (0 = normal).
|
||||
/// For TrueType: number of morphological dilation passes.
|
||||
/// For FreeType: outline inflation at 32 units per weight step (0.5px each).
|
||||
pub fn setFontWeight(font: *Font, w: u8) void {
|
||||
switch (font.backend) {
|
||||
.truetype => |*f| f.weight = w,
|
||||
.freetype => |*f| f.weight_strength = @as(i64, w) * 32,
|
||||
}
|
||||
}
|
||||
|
||||
const Self = @This();
|
||||
|
||||
active: Backend = .truetype,
|
||||
tt: TT,
|
||||
ft: FT,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !Self {
|
||||
const tt = try TT.init(allocator);
|
||||
const ft = try FT.init(allocator);
|
||||
return .{ .tt = tt, .ft = ft };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.tt.deinit();
|
||||
self.ft.deinit();
|
||||
}
|
||||
|
||||
pub fn setBackend(self: *Self, backend: Backend) void {
|
||||
self.active = backend;
|
||||
}
|
||||
|
||||
pub fn loadFont(self: *Self, name: []const u8, size_px: u16) !Font {
|
||||
switch (self.active) {
|
||||
.truetype => {
|
||||
const f = try self.tt.loadFont(name, size_px);
|
||||
return .{ .cell_size = f.cell_size, .backend = .{ .truetype = f } };
|
||||
},
|
||||
.freetype => {
|
||||
const f = try self.ft.loadFont(name, size_px);
|
||||
return .{ .cell_size = f.cell_size, .backend = .{ .freetype = f } };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
self: *const Self,
|
||||
font: Font,
|
||||
codepoint: u21,
|
||||
kind: GlyphKind,
|
||||
staging_buf: []u8,
|
||||
) void {
|
||||
switch (font.backend) {
|
||||
.truetype => |f| self.tt.render(f, codepoint, kind, staging_buf),
|
||||
.freetype => |f| self.ft.render(f, codepoint, @enumFromInt(@intFromEnum(kind)), staging_buf),
|
||||
}
|
||||
}
|
||||
149
src/gui/rasterizer/freetype.zig
Normal file
149
src/gui/rasterizer/freetype.zig
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/// FreeType-based glyph rasterizer.
|
||||
/// Satisfies the GlyphRasterizer interface (see GlyphRasterizer.zig).
|
||||
///
|
||||
/// Advantages over the TrueType rasterizer:
|
||||
/// - FT_Outline_EmboldenXY provides high-quality synthetic bold by inflating
|
||||
/// the glyph outline before rasterization, avoiding the ugly pixel-smearing
|
||||
/// of post-rasterization morphological dilation.
|
||||
/// - FreeType's hinting engine often produces crisper results at small sizes.
|
||||
const std = @import("std");
|
||||
const c = @cImport({
|
||||
@cInclude("ft2build.h");
|
||||
@cInclude("freetype/freetype.h");
|
||||
@cInclude("freetype/ftoutln.h");
|
||||
});
|
||||
const XY = @import("xy").XY;
|
||||
const geometric = @import("geometric");
|
||||
pub const font_finder = @import("font_finder");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub const GlyphKind = enum { single, left, right };
|
||||
|
||||
pub const Fonts = struct {};
|
||||
|
||||
pub const Font = struct {
|
||||
cell_size: XY(u16) = .{ .x = 8, .y = 16 },
|
||||
ascent_px: i32 = 0,
|
||||
face: c.FT_Face = null,
|
||||
/// Outline emboldening strength in 26.6 fixed-point pixels (64 = 1px).
|
||||
/// 0 = no emboldening. Weight 1 → 32 (0.5px), weight 2 → 64 (1px), etc.
|
||||
weight_strength: i64 = 0,
|
||||
};
|
||||
|
||||
library: c.FT_Library,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !Self {
|
||||
var library: c.FT_Library = undefined;
|
||||
if (c.FT_Init_FreeType(&library) != 0) return error.FreeTypeInitFailed;
|
||||
return .{ .library = library, .allocator = allocator };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
_ = c.FT_Done_FreeType(self.library);
|
||||
}
|
||||
|
||||
pub fn loadFont(self: *Self, name: []const u8, size_px: u16) !Font {
|
||||
const path = try font_finder.findFont(self.allocator, name);
|
||||
defer self.allocator.free(path);
|
||||
|
||||
const path_z = try self.allocator.dupeZ(u8, path);
|
||||
defer self.allocator.free(path_z);
|
||||
|
||||
var face: c.FT_Face = undefined;
|
||||
if (c.FT_New_Face(self.library, path_z.ptr, 0, &face) != 0)
|
||||
return error.FaceLoadFailed;
|
||||
errdefer _ = c.FT_Done_Face(face);
|
||||
|
||||
if (c.FT_Set_Pixel_Sizes(face, 0, size_px) != 0)
|
||||
return error.SetSizeFailed;
|
||||
|
||||
// Derive cell metrics from the full block glyph (U+2588), same strategy
|
||||
// as truetype.zig: the rendered bitmap defines exact cell dimensions.
|
||||
var ascent_px: i32 = @intCast((face.*.size.*.metrics.ascender + 32) >> 6);
|
||||
var cell_h: u16 = size_px;
|
||||
if (c.FT_Load_Char(face, 0x2588, c.FT_LOAD_RENDER) == 0) {
|
||||
const bm = face.*.glyph.*.bitmap;
|
||||
if (bm.rows > 0) {
|
||||
cell_h = @intCast(bm.rows);
|
||||
ascent_px = face.*.glyph.*.bitmap_top;
|
||||
}
|
||||
}
|
||||
|
||||
// Cell width from advance of 'M'.
|
||||
var cell_w: u16 = @max(1, size_px / 2);
|
||||
if (c.FT_Load_Char(face, 'M', c.FT_LOAD_DEFAULT) == 0) {
|
||||
const adv: i32 = @intCast((face.*.glyph.*.advance.x + 32) >> 6);
|
||||
if (adv > 0) cell_w = @intCast(adv);
|
||||
}
|
||||
|
||||
return .{
|
||||
.cell_size = .{ .x = cell_w, .y = cell_h },
|
||||
.ascent_px = ascent_px,
|
||||
.face = face,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
self: *const Self,
|
||||
font: Font,
|
||||
codepoint: u21,
|
||||
kind: GlyphKind,
|
||||
staging_buf: []u8,
|
||||
) void {
|
||||
_ = self;
|
||||
|
||||
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);
|
||||
|
||||
if (geometric.renderBlockElement(codepoint, staging_buf, buf_w, buf_h, x_offset, cw, ch)) return;
|
||||
if (geometric.renderBoxDrawing(codepoint, staging_buf, buf_w, buf_h, x_offset, cw, ch)) return;
|
||||
if (geometric.renderExtendedBlocks(codepoint, staging_buf, buf_w, buf_h, x_offset, cw, ch)) return;
|
||||
|
||||
const face = font.face orelse return;
|
||||
|
||||
// Load with FT_LOAD_NO_BITMAP so we always get an outline (needed for
|
||||
// FT_Outline_EmboldenXY; also avoids embedded bitmap strikes which may
|
||||
// not match our computed cell metrics).
|
||||
const load_flags: c.FT_Int32 = c.FT_LOAD_DEFAULT | c.FT_LOAD_NO_BITMAP;
|
||||
if (c.FT_Load_Char(face, codepoint, load_flags) != 0) return;
|
||||
|
||||
if (font.weight_strength > 0) {
|
||||
const s: c.FT_Pos = @intCast(font.weight_strength);
|
||||
_ = c.FT_Outline_EmboldenXY(&face.*.glyph.*.outline, s, s);
|
||||
}
|
||||
|
||||
if (c.FT_Render_Glyph(face.*.glyph, c.FT_RENDER_MODE_NORMAL) != 0) return;
|
||||
|
||||
const bm = face.*.glyph.*.bitmap;
|
||||
if (bm.rows == 0 or bm.width == 0) return;
|
||||
if (bm.pitch <= 0) return; // skip bottom-up bitmaps (unusual for normal mode)
|
||||
|
||||
const pitch: u32 = @intCast(bm.pitch);
|
||||
const off_x: i32 = face.*.glyph.*.bitmap_left;
|
||||
const off_y: i32 = font.ascent_px - face.*.glyph.*.bitmap_top;
|
||||
|
||||
var row: u32 = 0;
|
||||
while (row < bm.rows) : (row += 1) {
|
||||
const dst_y = off_y + @as(i32, @intCast(row));
|
||||
if (dst_y < 0 or dst_y >= buf_h) continue;
|
||||
|
||||
var col: u32 = 0;
|
||||
while (col < bm.width) : (col += 1) {
|
||||
const dst_x = x_offset + off_x + @as(i32, @intCast(col));
|
||||
if (dst_x < 0 or dst_x >= buf_w) continue;
|
||||
|
||||
const src_idx = row * pitch + col;
|
||||
const dst_idx: usize = @intCast(dst_y * buf_w + dst_x);
|
||||
if (dst_idx < staging_buf.len)
|
||||
staging_buf[dst_idx] = bm.buffer[src_idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
396
src/gui/rasterizer/geometric.zig
Normal file
396
src/gui/rasterizer/geometric.zig
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
/// Geometric rendering of Unicode block elements, box-drawing, and extended
|
||||
/// block characters. These are rasterized as solid pixel fills rather than
|
||||
/// through any font rasterizer, so they have no anti-aliased edges and tile
|
||||
/// perfectly between adjacent cells.
|
||||
///
|
||||
/// All functions take the same parameter set:
|
||||
/// cp — Unicode codepoint
|
||||
/// buf — A8 staging buffer (width = buf_w, height = buf_h)
|
||||
/// buf_w — buffer row stride in pixels (always 2*cell_w for wide glyphs)
|
||||
/// buf_h — buffer height in pixels (= cell height)
|
||||
/// x0 — left edge of this cell within buf (0 for left/single, cell_w for right)
|
||||
/// cw, ch — cell width / height in pixels
|
||||
///
|
||||
/// Returns true if the codepoint was handled, false to fall through to the
|
||||
/// font rasterizer.
|
||||
/// Fill a solid rectangle [x0, x1) × [y0, y1) in the staging buffer.
|
||||
pub 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the stroke×stroke corner area for a rounded box-drawing corner (╭╮╯╰).
|
||||
/// Fills pixels in [x_start..x_end, y_start..y_end] where distance from
|
||||
/// (corner_fx, corner_fy) is >= r_clip.
|
||||
fn drawRoundedCornerArea(
|
||||
buf: []u8,
|
||||
buf_w: i32,
|
||||
x_start: i32,
|
||||
y_start: i32,
|
||||
x_end: i32,
|
||||
y_end: i32,
|
||||
corner_fx: f32,
|
||||
corner_fy: f32,
|
||||
r_clip: f32,
|
||||
) void {
|
||||
const r2 = r_clip * r_clip;
|
||||
var cy: i32 = y_start;
|
||||
while (cy < y_end) : (cy += 1) {
|
||||
const dy: f32 = @as(f32, @floatFromInt(cy)) + 0.5 - corner_fy;
|
||||
var cx: i32 = x_start;
|
||||
while (cx < x_end) : (cx += 1) {
|
||||
const dx: f32 = @as(f32, @floatFromInt(cx)) + 0.5 - corner_fx;
|
||||
if (dx * dx + dy * dy >= r2) {
|
||||
const idx = cy * buf_w + cx;
|
||||
if (idx >= 0 and idx < @as(i32, @intCast(buf.len)))
|
||||
buf[@intCast(idx)] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a block element character (U+2580–U+259F) geometrically.
|
||||
pub fn renderBlockElement(
|
||||
cp: u21,
|
||||
buf: []u8,
|
||||
buf_w: i32,
|
||||
buf_h: i32,
|
||||
x0: i32,
|
||||
cw: i32,
|
||||
ch: i32,
|
||||
) bool {
|
||||
if (true) return false;
|
||||
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
|
||||
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
|
||||
var y: i32 = 0;
|
||||
while (y < ch) : (y += 2) {
|
||||
fillRect(buf, buf_w, buf_h, x0, y, x1, y + 1);
|
||||
}
|
||||
},
|
||||
0x2593 => { // ▓ dark shade
|
||||
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
|
||||
0x2596 => fillRect(buf, buf_w, buf_h, x0, half_h, mid_x, ch), // ▖ lower-left
|
||||
0x2597 => fillRect(buf, buf_w, buf_h, mid_x, half_h, x1, ch), // ▗ lower-right
|
||||
0x2598 => fillRect(buf, buf_w, buf_h, x0, 0, mid_x, half_h), // ▘ upper-left
|
||||
0x2599 => {
|
||||
fillRect(buf, buf_w, buf_h, x0, 0, mid_x, ch);
|
||||
fillRect(buf, buf_w, buf_h, mid_x, half_h, x1, ch);
|
||||
},
|
||||
0x259A => {
|
||||
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 => {
|
||||
fillRect(buf, buf_w, buf_h, x0, 0, x1, half_h);
|
||||
fillRect(buf, buf_w, buf_h, x0, half_h, mid_x, ch);
|
||||
},
|
||||
0x259C => {
|
||||
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
|
||||
0x259E => {
|
||||
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 => {
|
||||
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+2500–U+257F) geometrically.
|
||||
pub 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;
|
||||
|
||||
const stroke: i32 = @max(1, @divTrunc(cw, 8));
|
||||
const hy0: i32 = @divTrunc(ch - stroke, 2);
|
||||
const hy1: i32 = hy0 + stroke;
|
||||
const vx0: i32 = x0 + @divTrunc(cw - stroke, 2);
|
||||
const vx1: i32 = vx0 + stroke;
|
||||
|
||||
const doff: i32 = @max(stroke + 1, @divTrunc(cw, 4));
|
||||
const doff_h: i32 = doff;
|
||||
const doff_w: i32 = doff;
|
||||
const dhy0t: i32 = @divTrunc(ch, 2) - doff_h;
|
||||
const dhy1t: i32 = dhy0t + stroke;
|
||||
const dhy0b: i32 = @divTrunc(ch, 2) + doff_h - stroke;
|
||||
const dhy1b: i32 = dhy0b + stroke;
|
||||
const dvx0l: i32 = x0 + @divTrunc(cw, 2) - doff_w;
|
||||
const dvx1l: i32 = dvx0l + stroke;
|
||||
const dvx0r: i32 = x0 + @divTrunc(cw, 2) + doff_w - stroke;
|
||||
const dvx1r: i32 = dvx0r + stroke;
|
||||
|
||||
switch (cp) {
|
||||
0x2500 => fillRect(buf, buf_w, buf_h, x0, hy0, x1, hy1),
|
||||
0x2502 => fillRect(buf, buf_w, buf_h, vx0, 0, vx1, ch),
|
||||
0x250C => {
|
||||
fillRect(buf, buf_w, buf_h, vx0, hy0, x1, hy1);
|
||||
fillRect(buf, buf_w, buf_h, vx0, hy0, vx1, ch);
|
||||
},
|
||||
0x2510 => {
|
||||
fillRect(buf, buf_w, buf_h, x0, hy0, vx1, hy1);
|
||||
fillRect(buf, buf_w, buf_h, vx0, hy0, vx1, ch);
|
||||
},
|
||||
0x2514 => {
|
||||
fillRect(buf, buf_w, buf_h, vx0, hy0, x1, hy1);
|
||||
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, hy1);
|
||||
},
|
||||
0x2518 => {
|
||||
fillRect(buf, buf_w, buf_h, x0, hy0, vx1, hy1);
|
||||
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, hy1);
|
||||
},
|
||||
0x251C => {
|
||||
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, ch);
|
||||
fillRect(buf, buf_w, buf_h, vx0, hy0, x1, hy1);
|
||||
},
|
||||
0x2524 => {
|
||||
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, ch);
|
||||
fillRect(buf, buf_w, buf_h, x0, hy0, vx1, hy1);
|
||||
},
|
||||
0x252C => {
|
||||
fillRect(buf, buf_w, buf_h, x0, hy0, x1, hy1);
|
||||
fillRect(buf, buf_w, buf_h, vx0, hy0, vx1, ch);
|
||||
},
|
||||
0x2534 => {
|
||||
fillRect(buf, buf_w, buf_h, x0, hy0, x1, hy1);
|
||||
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, hy1);
|
||||
},
|
||||
0x253C => {
|
||||
fillRect(buf, buf_w, buf_h, x0, hy0, x1, hy1);
|
||||
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, ch);
|
||||
},
|
||||
0x2550 => {
|
||||
fillRect(buf, buf_w, buf_h, x0, dhy0t, x1, dhy1t);
|
||||
fillRect(buf, buf_w, buf_h, x0, dhy0b, x1, dhy1b);
|
||||
},
|
||||
0x2551 => {
|
||||
fillRect(buf, buf_w, buf_h, dvx0l, 0, dvx1l, ch);
|
||||
fillRect(buf, buf_w, buf_h, dvx0r, 0, dvx1r, ch);
|
||||
},
|
||||
0x2554 => {
|
||||
fillRect(buf, buf_w, buf_h, dvx0l, dhy0t, x1, dhy1t);
|
||||
fillRect(buf, buf_w, buf_h, dvx0r, dhy0b, x1, dhy1b);
|
||||
fillRect(buf, buf_w, buf_h, dvx0l, dhy0t, dvx1l, ch);
|
||||
fillRect(buf, buf_w, buf_h, dvx0r, dhy0b, dvx1r, ch);
|
||||
},
|
||||
0x2557 => {
|
||||
fillRect(buf, buf_w, buf_h, x0, dhy0t, dvx1r, dhy1t);
|
||||
fillRect(buf, buf_w, buf_h, x0, dhy0b, dvx1l, dhy1b);
|
||||
fillRect(buf, buf_w, buf_h, dvx0r, dhy0t, dvx1r, ch);
|
||||
fillRect(buf, buf_w, buf_h, dvx0l, dhy0b, dvx1l, ch);
|
||||
},
|
||||
0x255A => {
|
||||
fillRect(buf, buf_w, buf_h, dvx0l, dhy0t, x1, dhy1t);
|
||||
fillRect(buf, buf_w, buf_h, dvx0r, dhy0b, x1, dhy1b);
|
||||
fillRect(buf, buf_w, buf_h, dvx0l, 0, dvx1l, dhy1t);
|
||||
fillRect(buf, buf_w, buf_h, dvx0r, 0, dvx1r, dhy1b);
|
||||
},
|
||||
0x255D => {
|
||||
fillRect(buf, buf_w, buf_h, x0, dhy0t, dvx1r, dhy1t);
|
||||
fillRect(buf, buf_w, buf_h, x0, dhy0b, dvx1l, dhy1b);
|
||||
fillRect(buf, buf_w, buf_h, dvx0r, 0, dvx1r, dhy1t);
|
||||
fillRect(buf, buf_w, buf_h, dvx0l, 0, dvx1l, dhy1b);
|
||||
},
|
||||
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);
|
||||
},
|
||||
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);
|
||||
},
|
||||
0x2552 => {
|
||||
fillRect(buf, buf_w, buf_h, vx0, dhy0t, vx1, ch);
|
||||
fillRect(buf, buf_w, buf_h, vx0, dhy0t, x1, dhy1t);
|
||||
fillRect(buf, buf_w, buf_h, vx0, dhy0b, x1, dhy1b);
|
||||
},
|
||||
0x2555 => {
|
||||
fillRect(buf, buf_w, buf_h, vx0, dhy0t, vx1, ch);
|
||||
fillRect(buf, buf_w, buf_h, x0, dhy0t, vx1, dhy1t);
|
||||
fillRect(buf, buf_w, buf_h, x0, dhy0b, vx1, dhy1b);
|
||||
},
|
||||
0x2558 => {
|
||||
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, dhy1b);
|
||||
fillRect(buf, buf_w, buf_h, vx0, dhy0t, x1, dhy1t);
|
||||
fillRect(buf, buf_w, buf_h, vx0, dhy0b, x1, dhy1b);
|
||||
},
|
||||
0x255B => {
|
||||
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, dhy1b);
|
||||
fillRect(buf, buf_w, buf_h, x0, dhy0t, vx1, dhy1t);
|
||||
fillRect(buf, buf_w, buf_h, x0, dhy0b, vx1, dhy1b);
|
||||
},
|
||||
0x2553 => {
|
||||
fillRect(buf, buf_w, buf_h, dvx0l, hy0, x1, hy1);
|
||||
fillRect(buf, buf_w, buf_h, dvx0l, hy0, dvx1l, ch);
|
||||
fillRect(buf, buf_w, buf_h, dvx0r, hy0, dvx1r, ch);
|
||||
},
|
||||
0x2556 => {
|
||||
fillRect(buf, buf_w, buf_h, x0, hy0, dvx1r, hy1);
|
||||
fillRect(buf, buf_w, buf_h, dvx0l, hy0, dvx1l, ch);
|
||||
fillRect(buf, buf_w, buf_h, dvx0r, hy0, dvx1r, ch);
|
||||
},
|
||||
0x2559 => {
|
||||
fillRect(buf, buf_w, buf_h, dvx0l, hy0, x1, hy1);
|
||||
fillRect(buf, buf_w, buf_h, dvx0l, 0, dvx1l, hy1);
|
||||
fillRect(buf, buf_w, buf_h, dvx0r, 0, dvx1r, hy1);
|
||||
},
|
||||
0x255C => {
|
||||
fillRect(buf, buf_w, buf_h, x0, hy0, dvx1r, hy1);
|
||||
fillRect(buf, buf_w, buf_h, dvx0l, 0, dvx1l, hy1);
|
||||
fillRect(buf, buf_w, buf_h, dvx0r, 0, dvx1r, hy1);
|
||||
},
|
||||
0x256D => { // ╭ NW: down+right
|
||||
const r_clip: f32 = @max(0.0, @as(f32, @floatFromInt(stroke)) * 0.5 - 0.5);
|
||||
fillRect(buf, buf_w, buf_h, vx1, hy0, x1, hy1);
|
||||
fillRect(buf, buf_w, buf_h, vx0, hy1, vx1, ch);
|
||||
drawRoundedCornerArea(buf, buf_w, vx0, hy0, vx1, hy1, @floatFromInt(vx0), @floatFromInt(hy0), r_clip);
|
||||
},
|
||||
0x256E => { // ╮ NE: down+left
|
||||
const r_clip: f32 = @max(0.0, @as(f32, @floatFromInt(stroke)) * 0.5 - 0.5);
|
||||
fillRect(buf, buf_w, buf_h, x0, hy0, vx0, hy1);
|
||||
fillRect(buf, buf_w, buf_h, vx0, hy1, vx1, ch);
|
||||
drawRoundedCornerArea(buf, buf_w, vx0, hy0, vx1, hy1, @floatFromInt(vx1), @floatFromInt(hy0), r_clip);
|
||||
},
|
||||
0x256F => { // ╯ SE: up+left
|
||||
const r_clip: f32 = @max(0.0, @as(f32, @floatFromInt(stroke)) * 0.5 - 0.5);
|
||||
fillRect(buf, buf_w, buf_h, x0, hy0, vx0, hy1);
|
||||
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, hy0);
|
||||
drawRoundedCornerArea(buf, buf_w, vx0, hy0, vx1, hy1, @floatFromInt(vx1), @floatFromInt(hy1), r_clip);
|
||||
},
|
||||
0x2570 => { // ╰ SW: up+right
|
||||
const r_clip: f32 = @max(0.0, @as(f32, @floatFromInt(stroke)) * 0.5 - 0.5);
|
||||
fillRect(buf, buf_w, buf_h, vx1, hy0, x1, hy1);
|
||||
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, hy0);
|
||||
drawRoundedCornerArea(buf, buf_w, vx0, hy0, vx1, hy1, @floatFromInt(vx0), @floatFromInt(hy1), r_clip);
|
||||
},
|
||||
else => return false,
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Render extended block characters used by WidgetStyle borders.
|
||||
pub 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);
|
||||
const th = @divTrunc(ch, 3);
|
||||
|
||||
switch (cp) {
|
||||
0x1FB82 => fillRect(buf, buf_w, buf_h, x0, 0, x1, qh),
|
||||
0x1FB02 => fillRect(buf, buf_w, buf_h, x0, 0, x1, th),
|
||||
0x1FB2D => fillRect(buf, buf_w, buf_h, x0, ch - th, x1, ch),
|
||||
0x1FB15 => {
|
||||
fillRect(buf, buf_w, buf_h, x0, 0, x1, th);
|
||||
fillRect(buf, buf_w, buf_h, x0, th, mid_x, ch);
|
||||
},
|
||||
0x1FB28 => {
|
||||
fillRect(buf, buf_w, buf_h, x0, 0, x1, th);
|
||||
fillRect(buf, buf_w, buf_h, mid_x, th, x1, ch);
|
||||
},
|
||||
0x1FB32 => {
|
||||
fillRect(buf, buf_w, buf_h, x0, 0, mid_x, ch - th);
|
||||
fillRect(buf, buf_w, buf_h, x0, ch - th, x1, ch);
|
||||
},
|
||||
0x1FB37 => {
|
||||
fillRect(buf, buf_w, buf_h, mid_x, 0, x1, ch - th);
|
||||
fillRect(buf, buf_w, buf_h, x0, ch - th, x1, ch);
|
||||
},
|
||||
0x1CD4A => {
|
||||
fillRect(buf, buf_w, buf_h, x0, 0, x1, qh);
|
||||
fillRect(buf, buf_w, buf_h, x0, qh, mid_x, ch);
|
||||
},
|
||||
0x1CD98 => {
|
||||
fillRect(buf, buf_w, buf_h, x0, 0, x1, qh);
|
||||
fillRect(buf, buf_w, buf_h, mid_x, qh, x1, ch);
|
||||
},
|
||||
0x1CDD5 => {
|
||||
fillRect(buf, buf_w, buf_h, mid_x, 0, x1, ch - qh);
|
||||
fillRect(buf, buf_w, buf_h, x0, ch - qh, x1, ch);
|
||||
},
|
||||
0x1CDC0 => {
|
||||
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,
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
const std = @import("std");
|
||||
const TrueType = @import("TrueType");
|
||||
const XY = @import("xy").XY;
|
||||
pub const font_finder = @import("font_finder.zig");
|
||||
pub const font_finder = @import("font_finder");
|
||||
const geometric = @import("geometric");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
|
|
@ -16,6 +17,9 @@ pub const Font = struct {
|
|||
scale: f32 = 0,
|
||||
ascent_px: i32 = 0,
|
||||
tt: ?TrueType = null,
|
||||
/// Synthetic boldness: number of 1-pixel dilation passes applied after
|
||||
/// rasterization. 0 = no change, 1 = slightly bolder, 2 = bolder still.
|
||||
weight: u8 = 0,
|
||||
};
|
||||
|
||||
pub const Fonts = struct {};
|
||||
|
|
@ -105,9 +109,9 @@ pub fn render(
|
|||
// 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;
|
||||
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;
|
||||
if (geometric.renderBlockElement(codepoint, staging_buf, buf_w, buf_h, x_offset, cw, ch)) return;
|
||||
if (geometric.renderBoxDrawing(codepoint, staging_buf, buf_w, buf_h, x_offset, cw, ch)) return;
|
||||
if (geometric.renderExtendedBlocks(codepoint, staging_buf, buf_w, buf_h, x_offset, cw, ch)) return;
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
||||
defer arena.deinit();
|
||||
|
|
@ -122,6 +126,28 @@ pub fn render(
|
|||
|
||||
if (dims.width == 0 or dims.height == 0) return;
|
||||
|
||||
// Synthetic emboldening: morphological dilation. Each pass expands every
|
||||
// lit pixel one step right and one step down, thickening all strokes by
|
||||
// ~1px per pass without changing the glyph's bounding box.
|
||||
for (0..font.weight) |_| {
|
||||
// Dilate right: each pixel takes the max of itself and its left neighbour.
|
||||
// Iterate right-to-left so we don't cascade within one pass.
|
||||
for (0..dims.height) |row| {
|
||||
var col: usize = dims.width - 1;
|
||||
while (col > 0) : (col -= 1) {
|
||||
const i = row * dims.width + col;
|
||||
pixels.items[i] = @max(pixels.items[i], pixels.items[i - 1]);
|
||||
}
|
||||
}
|
||||
// Dilate down: each pixel takes the max of itself and its upper neighbour.
|
||||
for (1..dims.height) |row| {
|
||||
for (0..dims.width) |col| {
|
||||
const i = row * dims.width + col;
|
||||
pixels.items[i] = @max(pixels.items[i], pixels.items[i - dims.width]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -139,446 +165,3 @@ 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 (true) return false;
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
/// Draw the stroke×stroke corner area for a rounded box-drawing corner (╭╮╯╰).
|
||||
/// ╭╮╯╰ are identical to ┌┐└┘ in structure (L-shaped strokes meeting at center)
|
||||
/// but with the sharp outer corner vertex rounded off by a circular clip.
|
||||
///
|
||||
/// Fills pixels in [x_start..x_end, y_start..y_end] where distance from
|
||||
/// (corner_fx, corner_fy) is >= r_clip. r_clip = max(0, stroke/2 - 0.5):
|
||||
/// stroke=1 → r_clip=0 → all pixels filled (no visible rounding, ≡ sharp corner)
|
||||
/// stroke=2 → r_clip=0.5 → clips the one diagonal corner pixel
|
||||
/// stroke=3 → r_clip=1.0 → removes a 2-pixel notch, etc.
|
||||
fn drawRoundedCornerArea(
|
||||
buf: []u8,
|
||||
buf_w: i32,
|
||||
x_start: i32,
|
||||
y_start: i32,
|
||||
x_end: i32,
|
||||
y_end: i32,
|
||||
corner_fx: f32,
|
||||
corner_fy: f32,
|
||||
r_clip: f32,
|
||||
) void {
|
||||
const r2 = r_clip * r_clip;
|
||||
var cy: i32 = y_start;
|
||||
while (cy < y_end) : (cy += 1) {
|
||||
const dy: f32 = @as(f32, @floatFromInt(cy)) + 0.5 - corner_fy;
|
||||
var cx: i32 = x_start;
|
||||
while (cx < x_end) : (cx += 1) {
|
||||
const dx: f32 = @as(f32, @floatFromInt(cx)) + 0.5 - corner_fx;
|
||||
if (dx * dx + dy * dy >= r2) {
|
||||
const idx = cy * buf_w + cx;
|
||||
if (idx >= 0 and idx < @as(i32, @intCast(buf.len)))
|
||||
buf[@intCast(idx)] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a box-drawing character (U+2500–U+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: base on cell width so horizontal and vertical
|
||||
// strokes appear equally thick (cells are typically ~2× taller than wide, so
|
||||
// using ch/8 would make horizontal strokes twice as thick as vertical ones).
|
||||
const stroke: i32 = @max(1, @divTrunc(cw, 8));
|
||||
|
||||
// Single-line center positions
|
||||
const hy0: i32 = @divTrunc(ch - stroke, 2);
|
||||
const hy1: i32 = hy0 + stroke;
|
||||
const vx0: i32 = x0 + @divTrunc(cw - stroke, 2);
|
||||
const vx1: i32 = vx0 + stroke;
|
||||
|
||||
// Double-line: two strokes offset from center by doff each side.
|
||||
// Use cw-based spacing for both so horizontal and vertical double lines
|
||||
// appear with the same visual gap regardless of cell aspect ratio.
|
||||
const doff: i32 = @max(stroke + 1, @divTrunc(cw, 4));
|
||||
const doff_h: i32 = doff;
|
||||
const doff_w: i32 = doff;
|
||||
// Horizontal double strokes (top = closer to top of cell):
|
||||
const dhy0t: i32 = @divTrunc(ch, 2) - doff_h;
|
||||
const dhy1t: i32 = dhy0t + stroke;
|
||||
const dhy0b: i32 = @divTrunc(ch, 2) + doff_h - stroke;
|
||||
const dhy1b: i32 = dhy0b + stroke;
|
||||
// Vertical double strokes (left = closer to left of cell):
|
||||
const dvx0l: i32 = x0 + @divTrunc(cw, 2) - doff_w;
|
||||
const dvx1l: i32 = dvx0l + stroke;
|
||||
const dvx0r: i32 = x0 + @divTrunc(cw, 2) + doff_w - stroke;
|
||||
const dvx1r: i32 = dvx0r + stroke;
|
||||
|
||||
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 ↑
|
||||
},
|
||||
// ╭╮╯╰ rounded corners: same L-shape as ┌┐└┘ but with the outer
|
||||
// corner vertex clipped by a circle. The corner area (vx0..vx1, hy0..hy1)
|
||||
// is drawn pixel-by-pixel; everything else uses fillRect.
|
||||
// r_clip = max(0, stroke/2 - 0.5): no rounding for 1px, small notch for 2px+.
|
||||
0x256D => { // ╭ NW: down+right
|
||||
const r_clip: f32 = @max(0.0, @as(f32, @floatFromInt(stroke)) * 0.5 - 0.5);
|
||||
fillRect(buf, buf_w, buf_h, vx1, hy0, x1, hy1); // horizontal right of corner
|
||||
fillRect(buf, buf_w, buf_h, vx0, hy1, vx1, ch); // vertical below corner
|
||||
drawRoundedCornerArea(buf, buf_w, vx0, hy0, vx1, hy1, @floatFromInt(vx0), @floatFromInt(hy0), r_clip);
|
||||
},
|
||||
0x256E => { // ╮ NE: down+left
|
||||
const r_clip: f32 = @max(0.0, @as(f32, @floatFromInt(stroke)) * 0.5 - 0.5);
|
||||
fillRect(buf, buf_w, buf_h, x0, hy0, vx0, hy1); // horizontal left of corner
|
||||
fillRect(buf, buf_w, buf_h, vx0, hy1, vx1, ch); // vertical below corner
|
||||
drawRoundedCornerArea(buf, buf_w, vx0, hy0, vx1, hy1, @floatFromInt(vx1), @floatFromInt(hy0), r_clip);
|
||||
},
|
||||
0x256F => { // ╯ SE: up+left
|
||||
const r_clip: f32 = @max(0.0, @as(f32, @floatFromInt(stroke)) * 0.5 - 0.5);
|
||||
fillRect(buf, buf_w, buf_h, x0, hy0, vx0, hy1); // horizontal left of corner
|
||||
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, hy0); // vertical above corner
|
||||
drawRoundedCornerArea(buf, buf_w, vx0, hy0, vx1, hy1, @floatFromInt(vx1), @floatFromInt(hy1), r_clip);
|
||||
},
|
||||
0x2570 => { // ╰ SW: up+right
|
||||
const r_clip: f32 = @max(0.0, @as(f32, @floatFromInt(stroke)) * 0.5 - 0.5);
|
||||
fillRect(buf, buf_w, buf_h, vx1, hy0, x1, hy1); // horizontal right of corner
|
||||
fillRect(buf, buf_w, buf_h, vx0, 0, vx1, hy0); // vertical above corner
|
||||
drawRoundedCornerArea(buf, buf_w, vx0, hy0, vx1, hy1, @floatFromInt(vx0), @floatFromInt(hy1), r_clip);
|
||||
},
|
||||
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);
|
||||
},
|
||||
0x1CDC0 => { // 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,
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ var tui_pid: thespian.pid = undefined;
|
|||
var font_size_px: u16 = 16;
|
||||
var font_name_buf: [256]u8 = undefined;
|
||||
var font_name_len: usize = 0;
|
||||
var font_weight: u8 = 0;
|
||||
var font_backend: gpu.RasterizerBackend = .freetype;
|
||||
var font_dirty: std.atomic.Value(bool) = .init(true);
|
||||
var stop_requested: std.atomic.Value(bool) = .init(false);
|
||||
|
||||
|
|
@ -72,7 +74,7 @@ var cursor_dirty: std.atomic.Value(bool) = .init(false);
|
|||
var attention_pending: std.atomic.Value(bool) = .init(false);
|
||||
|
||||
// Current font — written and read only from the wio thread (after gpu.init).
|
||||
var wio_font: gpu.Font = .{ .cell_size = .{ .x = 8, .y = 16 } };
|
||||
var wio_font: gpu.Font = .{ .cell_size = .{ .x = 8, .y = 16 }, .backend = .{ .freetype = .{} } };
|
||||
|
||||
// ── Public API (called from tui thread) ───────────────────────────────────
|
||||
|
||||
|
|
@ -186,6 +188,28 @@ pub fn setFontFace(name: []const u8) void {
|
|||
requestRender();
|
||||
}
|
||||
|
||||
pub fn setFontWeight(weight: u8) void {
|
||||
font_weight = weight;
|
||||
saveConfig();
|
||||
font_dirty.store(true, .release);
|
||||
requestRender();
|
||||
}
|
||||
|
||||
pub fn getFontWeight() u8 {
|
||||
return font_weight;
|
||||
}
|
||||
|
||||
pub fn setRasterizerBackend(backend: gpu.RasterizerBackend) void {
|
||||
font_backend = backend;
|
||||
saveConfig();
|
||||
font_dirty.store(true, .release);
|
||||
requestRender();
|
||||
}
|
||||
|
||||
pub fn getRasterizerBackend() gpu.RasterizerBackend {
|
||||
return font_backend;
|
||||
}
|
||||
|
||||
pub fn setWindowTitle(title: []const u8) void {
|
||||
title_mutex.lock();
|
||||
defer title_mutex.unlock();
|
||||
|
|
@ -237,6 +261,8 @@ pub fn loadConfig() void {
|
|||
root.write_config(conf, config_arena) catch
|
||||
log.err("failed to write gui config file", .{});
|
||||
font_size_px = conf.fontsize;
|
||||
font_weight = conf.fontweight;
|
||||
font_backend = conf.fontbackend;
|
||||
const name = conf.fontface;
|
||||
const copy_len = @min(name.len, font_name_buf.len);
|
||||
@memcpy(font_name_buf[0..copy_len], name[0..copy_len]);
|
||||
|
|
@ -246,6 +272,8 @@ pub fn loadConfig() void {
|
|||
fn saveConfig() void {
|
||||
var conf, _ = root.read_config(gui_config, config_arena);
|
||||
conf.fontsize = @truncate(font_size_px);
|
||||
conf.fontweight = font_weight;
|
||||
conf.fontbackend = font_backend;
|
||||
conf.fontface = getFontName();
|
||||
root.write_config(conf, config_arena) catch
|
||||
log.err("failed to write gui config file", .{});
|
||||
|
|
@ -285,7 +313,10 @@ fn pixelToCellPos(pos: wio.Position) CellPos {
|
|||
fn reloadFont() void {
|
||||
const name = if (font_name_len > 0) font_name_buf[0..font_name_len] else "monospace";
|
||||
const size_physical: u16 = @intFromFloat(@round(@as(f32, @floatFromInt(font_size_px)) * dpi_scale));
|
||||
wio_font = gpu.loadFont(name, @max(size_physical, 4)) catch return;
|
||||
gpu.setRasterizerBackend(font_backend);
|
||||
var f = gpu.loadFont(name, @max(size_physical, 4)) catch return;
|
||||
gpu.setFontWeight(&f, font_weight);
|
||||
wio_font = f;
|
||||
}
|
||||
|
||||
// Check dirty flag and reload if needed.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
fontface: []const u8 = "Cascadia Code",
|
||||
fontface: []const u8 = "IosevkaTerm Nerd Font Mono",
|
||||
fontsize: u8 = 14,
|
||||
fontweight: u8 = 0,
|
||||
fontbackend: RasterizerBackend = .freetype,
|
||||
|
||||
initial_window_x: u16 = 1087,
|
||||
initial_window_y: u16 = 1014,
|
||||
|
||||
include_files: []const u8 = "",
|
||||
|
||||
pub const RasterizerBackend = enum {
|
||||
truetype,
|
||||
freetype,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue