feat(gui): M3 - TrueType rasterizer with fontconfig font discovery
Replace the M2 stub rasterizer with a real font rasterizer: - build.zig.zon: add TrueType as a lazy path dependency - build.zig: swap stub_rasterizer_mod for truetype_rasterizer_mod; link fontconfig + libc on Linux; TrueType dep is lazy (falls back gracefully) - src/gui/rasterizer/truetype.zig: andrewrk/TrueType rasterizer; loadFont uses fontconfig to locate the font file, derives cell dimensions from vertical metrics (ascent−descent) and 'M' advance width; render blits the A8 glyph bitmap into the caller-provided staging buffer with correct baseline placement (ascent_px + off_y) and double-wide support (+cell_w x-offset for kind=.right); arena allocator per render call - src/gui/rasterizer/font_finder.zig: OS dispatcher (Linux only for now) - src/gui/rasterizer/font_finder/linux.zig: fontconfig C API - FcFontMatch resolves a font name pattern to an absolute file path Requires: libfontconfig-dev (already present alongside libgl-dev etc.) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
39b482b2e0
commit
ae0f62c3bf
5 changed files with 173 additions and 3 deletions
17
build.zig
17
build.zig
|
|
@ -554,18 +554,29 @@ pub fn build_exe(
|
|||
const gui_glyph_cache_mod = b.createModule(.{ .root_source_file = b.path("src/gui/GlyphIndexCache.zig") });
|
||||
const gui_xterm_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xterm.zig") });
|
||||
|
||||
const stub_rasterizer_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/gui/rasterizer/stub.zig"),
|
||||
const tt_dep = b.lazyDependency("TrueType", .{
|
||||
.target = target,
|
||||
.optimize = optimize_deps,
|
||||
}) orelse break :blk tui_renderer_mod;
|
||||
|
||||
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 },
|
||||
},
|
||||
});
|
||||
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 = stub_rasterizer_mod },
|
||||
.{ .name = "rasterizer", .module = truetype_rasterizer_mod },
|
||||
.{ .name = "xy", .module = gui_xy_mod },
|
||||
.{ .name = "Cell", .module = gui_cell_mod },
|
||||
.{ .name = "GlyphIndexCache", .module = gui_glyph_cache_mod },
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@
|
|||
.path = "../flow-deps/sokol-zig",
|
||||
.lazy = true,
|
||||
},
|
||||
.TrueType = .{
|
||||
.path = "../flow-deps/TrueType",
|
||||
.lazy = true,
|
||||
},
|
||||
.diffz = .{
|
||||
.url = "git+https://github.com/ziglibs/diffz.git#fbdf690b87db6b1142bbce6d4906f90b09ce60bb",
|
||||
.hash = "diffz-0.0.1-G2tlIezMAQBwGNGDs7Hn_N25dWSjEzgR_FAx9GFAvCuZ",
|
||||
|
|
|
|||
9
src/gui/rasterizer/font_finder.zig
Normal file
9
src/gui/rasterizer/font_finder.zig
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
pub fn findFont(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
|
||||
return switch (builtin.os.tag) {
|
||||
.linux => @import("font_finder/linux.zig").find(allocator, name),
|
||||
else => error.FontFinderNotSupported,
|
||||
};
|
||||
}
|
||||
28
src/gui/rasterizer/font_finder/linux.zig
Normal file
28
src/gui/rasterizer/font_finder/linux.zig
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
const std = @import("std");
|
||||
const fc = @cImport({
|
||||
@cInclude("fontconfig/fontconfig.h");
|
||||
});
|
||||
|
||||
pub fn find(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
|
||||
const config = fc.FcInitLoadConfigAndFonts() orelse return error.FontconfigInit;
|
||||
defer fc.FcConfigDestroy(config);
|
||||
|
||||
const name_z = try allocator.dupeZ(u8, name);
|
||||
defer allocator.free(name_z);
|
||||
|
||||
const pat = fc.FcNameParse(name_z.ptr) orelse return error.FontPatternParse;
|
||||
defer fc.FcPatternDestroy(pat);
|
||||
|
||||
_ = fc.FcConfigSubstitute(config, pat, fc.FcMatchPattern);
|
||||
fc.FcDefaultSubstitute(pat);
|
||||
|
||||
var result: fc.FcResult = undefined;
|
||||
const font = fc.FcFontMatch(config, pat, &result) orelse return error.FontNotFound;
|
||||
defer fc.FcPatternDestroy(font);
|
||||
|
||||
var file: [*c]fc.FcChar8 = undefined;
|
||||
if (fc.FcPatternGetString(font, fc.FC_FILE, 0, &file) != fc.FcResultMatch)
|
||||
return error.FontPathNotFound;
|
||||
|
||||
return allocator.dupe(u8, std.mem.sliceTo(file, 0));
|
||||
}
|
||||
118
src/gui/rasterizer/truetype.zig
Normal file
118
src/gui/rasterizer/truetype.zig
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
const std = @import("std");
|
||||
const TrueType = @import("TrueType");
|
||||
const XY = @import("xy").XY;
|
||||
const font_finder = @import("font_finder.zig");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub const GlyphKind = enum {
|
||||
single,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
|
||||
pub const Font = struct {
|
||||
cell_size: XY(u16),
|
||||
scale: f32 = 0,
|
||||
ascent_px: i32 = 0,
|
||||
tt: ?TrueType = null,
|
||||
};
|
||||
|
||||
pub const Fonts = struct {};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
font_data: std.ArrayListUnmanaged([]u8),
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.font_data = .empty,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
for (self.font_data.items) |data| {
|
||||
self.allocator.free(data);
|
||||
}
|
||||
self.font_data.deinit(self.allocator);
|
||||
}
|
||||
|
||||
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 data = try std.fs.cwd().readFileAlloc(self.allocator, path, 64 * 1024 * 1024);
|
||||
errdefer self.allocator.free(data);
|
||||
|
||||
const tt = try TrueType.load(data);
|
||||
|
||||
const scale = tt.scaleForPixelHeight(@floatFromInt(size_px));
|
||||
const vm = tt.verticalMetrics();
|
||||
|
||||
const ascent_px: i32 = @intFromFloat(@round(@as(f32, @floatFromInt(vm.ascent)) * scale));
|
||||
const descent_px: i32 = @intFromFloat(@round(@as(f32, @floatFromInt(vm.descent)) * scale));
|
||||
const cell_h: u16 = @intCast(@max(ascent_px - descent_px, 1));
|
||||
|
||||
const m_glyph = tt.codepointGlyphIndex('M');
|
||||
const m_hmetrics = tt.glyphHMetrics(m_glyph);
|
||||
const cell_w_f: f32 = @as(f32, @floatFromInt(m_hmetrics.advance_width)) * scale;
|
||||
const cell_w: u16 = @intFromFloat(@ceil(cell_w_f));
|
||||
|
||||
try self.font_data.append(self.allocator, data);
|
||||
|
||||
return Font{
|
||||
.tt = tt, // TrueType holds a slice into `data` which is now owned by self.font_data
|
||||
.cell_size = .{ .x = cell_w, .y = cell_h },
|
||||
.scale = scale,
|
||||
.ascent_px = ascent_px,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
self: *const Self,
|
||||
font: Font,
|
||||
codepoint: u21,
|
||||
kind: GlyphKind,
|
||||
staging_buf: []u8,
|
||||
) void {
|
||||
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
const tt = font.tt orelse return;
|
||||
|
||||
var pixels = std.ArrayListUnmanaged(u8){};
|
||||
|
||||
const glyph = tt.codepointGlyphIndex(codepoint);
|
||||
const dims = tt.glyphBitmap(alloc, &pixels, glyph, font.scale, font.scale) catch return;
|
||||
|
||||
if (dims.width == 0 or dims.height == 0) return;
|
||||
|
||||
const buf_w: i32 = switch (kind) {
|
||||
.single => @intCast(font.cell_size.x),
|
||||
.left, .right => @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;
|
||||
|
||||
for (0..dims.width) |col| {
|
||||
const dst_x: i32 = x_offset + @as(i32, dims.off_x) + @as(i32, @intCast(col));
|
||||
if (dst_x < 0 or dst_x >= buf_w) continue;
|
||||
|
||||
const src_idx = row * dims.width + col;
|
||||
const dst_idx: usize = @intCast(dst_y * buf_w + dst_x);
|
||||
|
||||
if (src_idx < pixels.items.len and dst_idx < staging_buf.len) {
|
||||
staging_buf[dst_idx] = pixels.items[src_idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue