zzz/src/client/font.zig
2026-01-27 21:16:46 +01:00

377 lines
14 KiB
Zig

const std = @import("std");
const rl = @import("raylib");
const kb = @import("kb");
const ft = @import("mach-freetype");
const hb = @import("mach-harfbuzz");
const rp = @import("stb_rect_pack");
const ztracy = @import("ztracy");
pub var ft_lib : ft.Library = undefined;
pub var shader : rl.Shader = undefined;
const ATLAS_SIZE = 4096;
const Glyph = struct {
index : u32,
x : i16,
y : i16,
width : i32,
height : i32,
dpi_width : f32,
dpi_height : f32,
bearing_x : i32,
bearing_y : i32,
dpi_bearing_x : f32,
dpi_bearing_y : f32,
descent : i32,
ascent : i32,
dpi_descent : f32,
dpi_ascent : f32,
st0 : rl.Vector2,
st1 : rl.Vector2,
};
pub const Font = struct {
face : ft.Face,
glyphs : std.AutoHashMap(u32, Glyph),
kbts_font : kb.kbts_font,
// texture : c.Texture2D,
texture : rl.Texture2D,
kb_file_data : []u8,
pub fn init(filename : []const u8, size : i32, allocator: std.mem.Allocator) !Font {
const file_data = try std.fs.cwd().readFileAlloc(
allocator,
filename,
10 * 1024 * 1024
);
defer allocator.free(file_data);
const kb_file_data = try allocator.dupe(u8, file_data);
errdefer allocator.free(kb_file_data);
var face = try ft_lib.createFaceMemory(file_data, 0);
var glyphs = std.AutoHashMap(u32, Glyph).init(allocator);
try face.setCharSize(0, size * 64, 0, 96 * @as(u16, @intFromFloat(rl.getWindowScaleDPI().y)));
try face.selectCharmap(.unicode);
var rects = try std.ArrayList(rp.stbrp_rect).initCapacity(allocator, 1024);
defer rects.deinit(allocator);
var nodes : [ATLAS_SIZE]rp.stbrp_node = std.mem.zeroes([ATLAS_SIZE]rp.stbrp_node);
var stbrpcontext : rp.stbrp_context = .{};
rp.stbrp_init_target(&stbrpcontext, ATLAS_SIZE, ATLAS_SIZE, &nodes, nodes.len);
const _atlas : []u8 = try allocator.alloc(u8, ATLAS_SIZE * ATLAS_SIZE);
defer allocator.free(_atlas);
@memset(_atlas, 0);
const atlas : rl.Image = .{
.data = _atlas.ptr,
.width = ATLAS_SIZE,
.height = ATLAS_SIZE,
.mipmaps = 1,
.format = rl.PixelFormat.uncompressed_grayscale,
};
for (0..face.numGlyphs()) |index| {
try face.loadGlyph(@intCast(index), .{});
const bmp = face.glyph().bitmap();
try rects.append(allocator, .{
.id = @intCast(index),//@intCast(face.glyph().glyphIndex()),
.w = @max(1, @as(c_int, @intCast(bmp.width()))),
.h = @max(1, @as(c_int, @intCast(bmp.rows()))),
.x = 0,
.y = 0,
.was_packed = 0,
});
}
_ = rp.stbrp_pack_rects(&stbrpcontext, rects.items.ptr, @intCast(rects.items.len));
for (rects.items) |rect| {
const index : u32 = @intCast(rect.id);
if (rect.was_packed == 0) continue;
try face.loadGlyph(index, .{ .render = true });
const bmp = face.glyph().bitmap();
const pitch: usize = @intCast(bmp.pitch());
const w: usize = @intCast(bmp.width());
const h: usize = @intCast(bmp.rows());
const dst_x: usize = @intCast(rect.x);
const dst_y: usize = @intCast(rect.y);
if (bmp.buffer()) |buf| {
for (0..h) |y| {
const src = buf[y * pitch .. y * pitch + w];
const dst_index = (dst_y + y) * ATLAS_SIZE + dst_x;
const dst = _atlas[dst_index .. dst_index + w];
@memcpy(dst, src);
}
}
const width : i32 = @intCast(bmp.width());
const height : i32 = @intCast(bmp.rows());
const dpi_width : f32 = @as(f32, @floatFromInt(width)) / rl.getWindowScaleDPI().x;
const dpi_height : f32 = @as(f32, @floatFromInt(height)) / rl.getWindowScaleDPI().y;
const bearing_x : i32 = face.glyph().bitmapLeft();
const bearing_y : i32 = face.glyph().bitmapTop();
const dpi_bearing_x : f32 = @as(f32, @floatFromInt(bearing_x)) / rl.getWindowScaleDPI().x;
const dpi_bearing_y : f32 = @as(f32, @floatFromInt(bearing_y)) / rl.getWindowScaleDPI().y;
const descent : i32 = height - bearing_y;
const ascent : i32 = height - descent;
const dpi_descent : f32 = dpi_height - dpi_bearing_y;
const dpi_ascent : f32 = dpi_height - dpi_descent;
const x : i16 = @intCast(rect.x);
const y : i16 = @as(i16, @intCast(rect.y)) + @as(i16, @intCast(height));
const fx = @as(f32, @floatFromInt(x));
const fy = @as(f32, @floatFromInt(y));
const fw = @as(f32, @floatFromInt(width));
const fh = @as(f32, @floatFromInt(height));
const fs = @as(f32, ATLAS_SIZE);
const glyph = Glyph{
.x = x,
.y = y,
.index = index,
.bearing_x = bearing_x,
.bearing_y = bearing_y,
.dpi_bearing_x = dpi_bearing_x,
.dpi_bearing_y = dpi_bearing_y,
.width = width,
.height = height,
.dpi_width = dpi_width,
.dpi_height = dpi_height,
.descent = descent,
.ascent = ascent,
.dpi_descent = dpi_descent,
.dpi_ascent = dpi_ascent,
.st0 = .{.x = (fx + 0.5) / fs, .y = (fy - fh + 0.5) / fs},
.st1 = .{.x = (fx + fw - 0.5) / fs, .y = (fy - 0.5) / fs},
};
try glyphs.put(glyph.index, glyph);
}
const texture = try rl.loadTextureFromImage(atlas);
var kbts_font = kb.kbts_FontFromMemory(kb_file_data.ptr, @intCast(kb_file_data.len), 0, null, null);
if (kb.kbts_FontIsValid(&kbts_font) == 0) {
std.log.info("[Error] [kb_text_shape] Failed to load the font.", .{});
}
const hb_blob = hb.Blob.initFromFile(filename);
_ = hb_blob;
return .{
.face = face,
.glyphs = glyphs,
.kbts_font = kbts_font,
.texture = texture,
.kb_file_data = kb_file_data,
};
}
pub fn deinit(self: *Font, allocator : std.mem.Allocator) void {
self.glyphs.deinit();
self.face.deinit();
kb.kbts_FreeFont(&self.kb);
self.texture.unload();
allocator.free(self.kb_file_data);
}
pub fn render_text(
self: *Font,
text: []const u8,
pos: rl.Vector2,
window_space: bool,
colour: rl.Color,
background: rl.Color,
nice_background: bool,
count_descent: bool
) void {
if (text.len == 0)
return;
const zone = ztracy.ZoneNC(@src(), "render_text", 0x00_ff_00_00);
defer zone.End();
rl.beginShaderMode(shader);
rl.gl.rlSetTexture(self.texture.id);
rl.gl.rlBegin(rl.gl.rl_quads);
if (nice_background) {
self.render_text(
text,
pos.add(.{.x = 3.0, .y = -3.0}),
window_space,
rl.Color{ .r = 0, .g = 0, .b = 0, .a = 255 },
rl.Color{ .r = 0, .g = 0, .b = 0, .a = 0 },
false,
false
);
}
var render_pos = pos;
_ = count_descent;
_ = background;
const font_ascent : f32 = @floatFromInt(self.face.size().metrics().ascender >> 6);
const dpi_font_ascent = font_ascent / rl.getWindowScaleDPI().y;
render_pos.y += dpi_font_ascent;
const Context = kb.kbts_CreateShapeContext(null, null);
defer kb.kbts_DestroyShapeContext(Context);
const kb_font = kb.kbts_ShapePushFont(Context, &self.kb);
if (kb_font == null) {
std.log.info("Could not open font!", .{});
return;
}
const zone2 = ztracy.ZoneNC(@src(), "ShapeUtf8", 0x00_ff_00_00);
kb.kbts_ShapeBegin(Context, kb.KBTS_DIRECTION_DONT_KNOW, kb.KBTS_LANGUAGE_DONT_KNOW);
kb.kbts_ShapeUtf8(Context, text.ptr, @intCast(text.len), kb.KBTS_USER_ID_GENERATION_MODE_CODEPOINT_INDEX);
kb.kbts_ShapeEnd(Context);
zone2.End();
var cursor_x : f32 = 0;
var cursor_y : f32 = 0;
var Run = kb.kbts_run{};
while (kb.kbts_ShapeRun(Context, &Run) != 0) {
const zone3 = ztracy.ZoneNC(@src(), "shape run", 0x00_ff_00_00);
defer zone3.End();
if ((Run.Flags & kb.KBTS_BREAK_FLAG_LINE_HARD) != 0) {
const font_linegap : f32 = @floatFromInt(self.face.size().metrics().height >> 6);
const dpi_font_linegap = font_linegap / rl.getWindowScaleDPI().y;
cursor_y += dpi_font_linegap;
cursor_x = 0;
}
var g : [*c]kb.kbts_glyph = null;
while (kb.kbts_GlyphIteratorNext(&Run.Glyphs, &g) != 0) {
const zone4 = ztracy.ZoneNC(@src(), "glyph iterator", 0x00_ff_00_00);
defer zone4.End();
const RunGlyph : *kb.kbts_glyph = g.?;
const CodepointIndex : i32 = RunGlyph.UserIdOrCodepointIndex;
var ShapeCodepoint : kb.kbts_shape_codepoint = undefined;
if (kb.kbts_ShapeGetShapeCodepoint(Context, CodepointIndex, &ShapeCodepoint) == 0) {
std.log.info("aah it's wrong!! idk how to handle the error rn!!", .{});
}
const advance_x : f32 = @floatFromInt(ft.mulFix(RunGlyph.AdvanceX, @intCast(self.face.size().metrics().x_scale)) >> 6);
const advance_y : f32 = @floatFromInt(ft.mulFix(RunGlyph.AdvanceY, @intCast(self.face.size().metrics().y_scale)) >> 6);
const dpi_advance_x = advance_x / rl.getWindowScaleDPI().x;
const dpi_advance_y = advance_y / rl.getWindowScaleDPI().y;
const offset_x : f32 = @floatFromInt(ft.mulFix(RunGlyph.OffsetX, @intCast(self.face.size().metrics().x_scale)) >> 6);
const offset_y : f32 = @floatFromInt(ft.mulFix(RunGlyph.OffsetY, @intCast(self.face.size().metrics().y_scale)) >> 6);
const dpi_offset_x = offset_x / rl.getWindowScaleDPI().x;
const dpi_offset_y = offset_y / rl.getWindowScaleDPI().y;
if (self.glyphs.getPtr(RunGlyph.Id)) |glyph| {
if (RunGlyph.OffsetX != 0) {
std.log.info("font bearing ({}, {}), kb offset ({}, {})", .{glyph.bearing_x, glyph.bearing_y, RunGlyph.OffsetX, RunGlyph.OffsetX});
}
var v0 = rl.Vector2.zero();
var v1 = rl.Vector2.zero();
v0 = render_pos.add(.{ .x = cursor_x + glyph.dpi_bearing_x + dpi_offset_x,
.y = cursor_y - glyph.dpi_bearing_y - dpi_offset_y });
v1 = v0.add(rl.Vector2{ .x = glyph.dpi_width, .y = glyph.dpi_height });
const p0 : rl.Vector4 = .{ .x = v0.x, .y = v0.y, .z = 0.0, .w = 1.0 };
const p1 : rl.Vector4 = .{ .x = v1.x, .y = v1.y, .z = 0.0, .w = 1.0 };
cursor_x += dpi_advance_x;
cursor_y += dpi_advance_y;
const st0 = glyph.st0;
const st1 = glyph.st1;
rl.gl.rlColor4ub(colour.r, colour.g, colour.b, colour.a);
rl.gl.rlNormal3f(0.0, 0.0, 1.0);
rl.gl.rlTexCoord2f(st0.x, st0.y); rl.gl.rlVertex2f(p0.x, p0.y);
rl.gl.rlTexCoord2f(st0.x, st1.y); rl.gl.rlVertex2f(p0.x, p1.y);
rl.gl.rlTexCoord2f(st1.x, st1.y); rl.gl.rlVertex2f(p1.x, p1.y);
rl.gl.rlTexCoord2f(st1.x, st0.y); rl.gl.rlVertex2f(p1.x, p0.y);
} else {
std.log.warn("kb_text_shape found the glyph, but we didn't load it from the font. index {}", .{RunGlyph.Id});
std.log.warn("advance_x {}", .{advance_x});
cursor_x += dpi_advance_x;
cursor_y += dpi_advance_y;
}
}
}
rl.gl.rlEnd();
rl.gl.rlSetTexture(0);
rl.endShaderMode();
}
pub fn size_row(self: *Font, str: []const u8, n: i32, max_width: f32) struct {rl.Vector2, f32, i32} {
_ = max_width;
_ = n;
const Context = kb.kbts_CreateShapeContext(null, null);
const kb_font = kb.kbts_ShapePushFont(Context, &self.kb);
if (kb_font == null) {
std.log.info("Could not open font!", .{});
return .{ rl.Vector2.zero(), 0, 0 };
}
kb.kbts_ShapeBegin(Context, kb.KBTS_DIRECTION_DONT_KNOW, kb.KBTS_LANGUAGE_DONT_KNOW);
kb.kbts_ShapeUtf8(Context, str.ptr, @intCast(str.len), kb.KBTS_USER_ID_GENERATION_MODE_CODEPOINT_INDEX);
kb.kbts_ShapeEnd(Context);
var size = rl.Vector2.zero();
var max_descent : f32 = 0;
var Run = kb.kbts_run{};
while(kb.kbts_ShapeRun(Context, &Run) != 0) {
if ((Run.Flags & kb.KBTS_BREAK_FLAG_LINE_HARD) != 0) {
}
var g : [*c]kb.kbts_glyph = null;
while (kb.kbts_GlyphIteratorNext(&Run.Glyphs, &g) != 0) {
const RunGlyph : *kb.kbts_glyph = g.?;
const CodepointIndex : i32 = RunGlyph.UserIdOrCodepointIndex;
var ShapeCodepoint : kb.kbts_shape_codepoint = undefined;
if (kb.kbts_ShapeGetShapeCodepoint(Context, CodepointIndex, &ShapeCodepoint) == 0) {
std.log.info("aah it's wrong!! idk how to handle the error rn!!", .{});
}
if (self.glyphs.getPtr(@intCast(RunGlyph.Id))) |glyph| {
const advance_x = @as(f32, @floatFromInt(ft.mulFix(RunGlyph.AdvanceX, @intCast(self.face.size().metrics().x_scale)) >> 6));
const dpi_advance_x = advance_x / rl.getWindowScaleDPI().x;
size.y = @max(size.y, glyph.dpi_height);
size.x += dpi_advance_x;
max_descent = @max(max_descent, glyph.dpi_descent);
}
}
}
return .{ size, max_descent, 0};
}
};