game-2025-12-13/src/engine/font.zig

615 lines
18 KiB
Zig

const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const sokol = @import("sokol");
const sapp = sokol.app;
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const Rect = Math.Rect;
const Vec4 = Math.Vec4;
const Gfx = @import("./graphics.zig");
const c = @cImport({
@cInclude("sokol/sokol_gfx.h");
@cInclude("fontstash.h");
@cInclude("sokol_fontstash.h");
});
// Struct definition copies from fonstash.h
// TODO: Create a getter, to avoid copying this struct by hand
const FONSstate = extern struct {
font: c_int,
@"align": c_int,
size: f32,
color: c_uint,
blur: f32,
spacing: f32
};
const FONSfont = opaque{};
const FONSglyph = opaque{};
extern fn zig_isTopLeft(stash: ?*c.FONScontext) bool;
extern fn zig_getGlyphIndex(glyph: ?*FONSglyph) c_int;
extern fn zig_fons__getState(stash: ?*c.struct_FONScontext) ?*FONSstate;
extern fn zig_getFont(stash: ?*c.FONScontext, index: c_int) ?*FONSfont;
extern fn zig_fons__tt_getPixelHeightScale(font: ?*FONSfont, size: f32) f32;
extern fn zig_fons__getVertAlign(stash: ?*c.FONScontext, font: ?*FONSfont, @"align": c_int, isize: i16) f32;
extern fn zig_fons__getGlyph(
stash: ?*c.FONScontext,
font: ?*FONSfont,
codepoint: c_uint,
@"isize": i16,
iblur: i16
) ?*FONSglyph;
extern fn zig_fons__getQuad(
stash: ?*c.FONScontext,
font: ?*FONSfont,
prevGlyphIndex: c_int, glyph: ?*FONSglyph,
scale: f32,
spacing: f32,
x: ?*f32, y: ?*f32,
q: ?*c.FONSquad
) void;
const Font = @This();
pub const Id = enum(c_int) {
_,
pub const invalid: Id = @enumFromInt(c.FONS_INVALID);
};
pub const AlignX = enum {
left,
right,
center,
fn toFONSAlignment(self: AlignX) c_int {
return switch (self) {
.left => c.FONS_ALIGN_LEFT,
.center => c.FONS_ALIGN_CENTER,
.right => c.FONS_ALIGN_RIGHT,
};
}
};
pub const AlignY = enum {
top,
middle,
bottom,
baseline,
fn toFONSAlignment(self: AlignY) c_int {
return switch (self) {
.top => c.FONS_ALIGN_TOP,
.middle => c.FONS_ALIGN_MIDDLE,
.bottom => c.FONS_ALIGN_BOTTOM,
.baseline => c.FONS_ALIGN_BASELINE,
};
}
};
pub const QuadIterator = struct {
ctx: Context,
x: f32,
y: f32,
next_x: f32,
next_y: f32,
spacing: f32,
scale: f32,
isize: c_short,
iblur: c_short,
prev_glyph_index: c_int,
font: *FONSfont,
pub fn init(ctx: Context, font: Font) QuadIterator {
const stash = ctx.fons_context;
ctx.setAsCurrent(font);
const x: f32 = 0;
var y: f32 = 0;
const state = zig_fons__getState(stash).?;
const fons_font = zig_getFont(stash, state.font) orelse @panic("Invalid font");
const font_isize: c_short = @intFromFloat(@trunc(state.size * 10.0));
const font_iblur: c_short = @intFromFloat(@trunc(state.blur));
const scale = zig_fons__tt_getPixelHeightScale(fons_font, @as(f32, @floatFromInt(font_isize)) / 10.0);
assert(state.@"align" & c.FONS_ALIGN_LEFT != 0);
assert(state.@"align" & c.FONS_ALIGN_RIGHT == 0);
assert(state.@"align" & c.FONS_ALIGN_CENTER == 0);
// Align vertically.
y += zig_fons__getVertAlign(stash, fons_font, @truncate(state.@"align"), font_isize);
return QuadIterator{
.ctx = ctx,
.x = x,
.y = y,
.next_x = x,
.next_y = y,
.isize = font_isize,
.iblur = font_iblur,
.scale = scale,
.spacing = state.spacing,
.font = fons_font,
.prev_glyph_index = -1
};
}
pub fn next(self: *QuadIterator, codepoint: u21) ?Rect {
const stash = self.ctx.fons_context;
self.x = self.next_x;
self.y = self.next_y;
const glyph = zig_fons__getGlyph(stash, self.font, codepoint, self.isize, self.iblur);
if (glyph != null) {
var q: c.FONSquad = .{};
zig_fons__getQuad(stash, self.font, self.prev_glyph_index, glyph, self.scale, self.spacing, &self.x, &self.y, &q);
self.prev_glyph_index = zig_getGlyphIndex(glyph);
return Rect.init(q.x0, q.y0, q.x1 - q.x0, q.y1 - q.y0);
} else {
self.prev_glyph_index = -1;
return null;
}
}
pub const Ascii = struct {
iter: QuadIterator,
text: []const u8,
index: usize,
pub fn init(ctx: Context, font: Font, text: []const u8) Ascii {
return Ascii{
.iter = .init(ctx, font),
.text = text,
.index = 0
};
}
pub fn next(self: *Ascii) ?Rect {
while (self.index < self.text.len) {
const codepoint_len = std.unicode.utf8ByteSequenceLength(self.text[self.index]) catch {
self.index += 1;
continue;
};
if (self.index + codepoint_len > self.text.len) {
self.index = self.text.len;
return null;
}
const codepoint = std.unicode.utf8Decode(self.text[self.index..][0..codepoint_len]) catch {
self.index += 1;
continue;
};
self.index += codepoint_len;
if (self.iter.next(codepoint)) |rect| {
return rect;
}
}
return null;
}
pub fn last(self: *Ascii) ?Rect {
var last_rect: ?Rect = null;
while (self.next()) |rect| {
last_rect = rect;
}
return last_rect;
}
};
pub const Unicode = struct {
iter: QuadIterator,
text: []const u21,
index: usize,
pub fn init(ctx: Context, font: Font, text: []const u21) Unicode {
return Unicode{
.iter = .init(ctx, font),
.text = text,
.index = 0
};
}
pub fn next(self: *Unicode) ?Rect {
while (self.index < self.text.len) {
const next_rect = self.iter.next(self.text[self.index]);
self.index += 1;
if (next_rect) |rect| {
return rect;
}
}
return null;
}
pub fn last(self: *Unicode) ?Rect {
var last_rect: ?Rect = null;
while (self.next()) |rect| {
last_rect = rect;
}
return last_rect;
}
};
};
pub const Context = struct {
fons_context: *c.struct_FONScontext,
pub fn init(atlas_size: f32) !Context {
const dpi_scale = sapp.dpiScale();
const atlas_dim = std.math.ceilPowerOfTwoAssert(u32, @intFromFloat(atlas_size * dpi_scale));
const fons_context = c.sfons_create(&c.sfons_desc_t{
.width = @intCast(atlas_dim),
.height = @intCast(atlas_dim),
});
if (fons_context == null) {
return error.sfons_create;
}
return Context{
.fons_context = fons_context.?
};
}
pub fn deinit(self: Context) void {
c.sfons_destroy(self.fons_context);
}
pub fn add(self: Context, name: [*c]const u8, data: []const u8) !Id {
const font_id = c.fonsAddFontMem(
self.fons_context,
name,
@constCast(data.ptr),
@intCast(data.len),
0
);
if (font_id == c.FONS_INVALID) {
return error.fonsAddFontMem;
}
return @enumFromInt(font_id);
}
pub fn clearState(self: Context) void {
c.fonsClearState(self.fons_context);
}
pub fn flush(self: Context) void {
c.sfons_flush(self.fons_context);
}
fn setAsCurrent(self: Context, font: Font) void {
const fs = self.fons_context;
const dpi_scale = sapp.dpiScale();
c.fonsSetFont(fs, @intFromEnum(font.id));
c.fonsSetSize(fs, font.size * dpi_scale);
c.fonsSetAlign(fs, Font.AlignX.left.toFONSAlignment() | Font.AlignY.top.toFONSAlignment());
c.fonsSetSpacing(fs, font.spacing);
}
pub fn drawText(self: Context, font: Font, pos: Vec2, color: Vec4, text: []const u8) void {
const fs = self.fons_context;
// TODO: Test if calling `.setAsCurrent()` isn't expensive
self.setAsCurrent(font);
const fons_color = c.sfons_rgba(
@intFromFloat(color.x * 255),
@intFromFloat(color.y * 255),
@intFromFloat(color.z * 255),
@intFromFloat(color.w * 255)
);
c.fonsSetColor(fs, fons_color);
Gfx.pushTransform(.init(0, 0), 1.0/64.0);
defer Gfx.popTransform();
_ = c.fonsDrawText(
fs,
pos.x*64,
pos.y*64,
text.ptr,
text.ptr + text.len
);
}
pub fn getBounds(self: Context, font: Font, text: []const u8) Rect {
const fs = self.fons_context;
self.setAsCurrent(font);
var line_bounds: [4]f32 = undefined;
_ = c.fonsTextBounds(fs, 0, 0, text.ptr, text.ptr + text.len, &line_bounds);
const min_x = line_bounds[0];
const min_y = line_bounds[1];
const max_x = line_bounds[2];
const max_y = line_bounds[3];
const width = max_x - min_x;
const height = max_y - min_y;
return Rect.init(min_x, min_y, width, height);
}
pub fn getBoundsUtf8(self: Context, font: Font, text: []const u21) Rect {
const fs = self.fons_context;
self.setAsCurrent(font);
var line_bounds: [4]f32 = undefined;
_ = fonsTextBoundsUtf8(fs, 0, 0, text, &line_bounds);
const min_x = line_bounds[0];
const min_y = line_bounds[1];
const max_x = line_bounds[2];
const max_y = line_bounds[3];
const width = max_x - min_x;
const height = max_y - min_y;
return Rect.init(min_x, min_y, width, height);
}
pub fn measure(self: Context, font: Font, text: []const u8) Vec2 {
const bounds = self.getBounds(font, text);
return bounds.size;
}
pub fn quadIter(self: Context, font: Font, text: []const u8) QuadIterator.Ascii {
return QuadIterator.Ascii.init(self, font, text);
}
pub fn quadUnicodeIter(self: Context, font: Font, text: []const u21) QuadIterator.Unicode {
return QuadIterator.Unicode.init(self, font, text);
}
// A reimplementation of `fonsTextBounds` which uses an array of already decoded codepoints
fn fonsTextBoundsUtf8(stash: ?*c.struct_FONScontext, initial_x: f32, initial_y: f32, str: []const u21, bounds: [*c]f32) f32 {
if (stash == null) {
return 0;
}
var x = initial_x;
var y = initial_y;
const state = zig_fons__getState(stash).?;
const font = zig_getFont(stash, state.font) orelse return 0;
const font_isize: i16 = @intFromFloat(@trunc(state.size * 10.0));
const font_iblur: i16 = @intFromFloat(@trunc(state.blur));
const scale = zig_fons__tt_getPixelHeightScale(font, @as(f32, @floatFromInt(font_isize)) / 10.0);
// Align vertically.
y += zig_fons__getVertAlign(stash, font, @truncate(state.@"align"), font_isize);
var minx = x;
var maxx = x;
var miny = y;
var maxy = y;
const startx = x;
var prevGlyphIndex: c_int = -1;
for (str) |codepoint| {
const glyph = zig_fons__getGlyph(stash, font, codepoint, font_isize, font_iblur);
if (glyph != null) {
var q: c.FONSquad = .{};
zig_fons__getQuad(stash, font, prevGlyphIndex, glyph, scale, state.spacing, &x, &y, &q);
if (q.x0 < minx) minx = q.x0;
if (q.x1 > maxx) maxx = q.x1;
if (zig_isTopLeft(stash)) {
if (q.y0 < miny) miny = q.y0;
if (q.y1 > maxy) maxy = q.y1;
} else {
if (q.y1 < miny) miny = q.y1;
if (q.y0 > maxy) maxy = q.y0;
}
prevGlyphIndex = zig_getGlyphIndex(glyph);
} else {
prevGlyphIndex = -1;
}
}
const advance = x - startx;
// Align horizontally
if ((state.@"align" & c.FONS_ALIGN_LEFT) != 0) {
// empty
} else if ((state.@"align" & c.FONS_ALIGN_RIGHT) != 0) {
minx -= advance;
maxx -= advance;
} else if ((state.@"align" & c.FONS_ALIGN_CENTER) != 0) {
minx -= advance * 0.5;
maxx -= advance * 0.5;
}
if (bounds != null) {
bounds[0] = minx;
bounds[1] = miny;
bounds[2] = maxx;
bounds[3] = maxy;
}
return advance;
}
};
pub const DrawTextContext = struct {
pub const Line = struct {
// Assumes that the text won't be invalidted between `queue*` and `draw` functions
text: []const u8,
pos: Vec2,
bounds: Rect,
};
// TODO: Add support for multiple font in a single context.
// I.e. each command should be able to have a separate font
ctx: Context,
font: Font,
line_height: f32 = 0,
pos: Vec2 = Vec2.zero,
lines: std.ArrayListUnmanaged(Line) = .empty,
bounds: Rect = Rect.zero,
pub fn init(ctx: Context, font: Font) DrawTextContext {
var self = DrawTextContext{
.ctx = ctx,
.font = font
};
const fs = ctx.fons_context;
self.ctx.setAsCurrent(font);
c.fonsVertMetrics(fs, null, null, &self.line_height);
return self;
}
pub fn deinit(self: *DrawTextContext, allocator: Allocator) void {
self.lines.deinit(allocator);
}
pub fn queueText(self: *DrawTextContext, allocator: Allocator, text: []const u8) !void {
var line_iter = std.mem.splitScalar(u8, text, '\n');
while (line_iter.next()) |line| {
try self.queueLine(allocator, line);
}
}
pub fn measureText(self: *const DrawTextContext, text: []const u8) Rect {
const fs = self.ctx.fons_context;
self.ctx.setAsCurrent(self.font);
var min_x: f32 = 0;
var max_x: f32 = 0;
var min_y: f32 = 0;
var max_y: f32 = 0;
var pos = Vec2.zero;
var line_iter = std.mem.splitScalar(u8, text, '\n');
while (line_iter.next()) |line| {
var line_bounds: [4]f32 = undefined;
_ = c.fonsTextBounds(fs, pos.x, pos.y, line.ptr, line.ptr + line.len, &line_bounds);
min_x = @min(min_x, line_bounds[0]);
min_y = @min(min_y, line_bounds[1]);
max_x = @max(max_x, line_bounds[2]);
max_y = @max(max_y, line_bounds[3]);
pos.y += self.line_height * self.font.line_spacing;
}
return Rect.init(min_x, min_y, max_x - min_x, max_y - min_y);
}
pub fn queueLine(self: *DrawTextContext, allocator: Allocator, line: []const u8) !void {
const fs = self.ctx.fons_context;
self.ctx.setAsCurrent(self.font);
var line_bounds: [4]f32 = undefined;
_ = c.fonsTextBounds(fs, self.pos.x, self.pos.y, line.ptr, line.ptr + line.len, &line_bounds);
try self.lines.append(allocator, .{
.bounds = Rect.init(
line_bounds[0],
line_bounds[1],
line_bounds[2] - line_bounds[0],
line_bounds[3] - line_bounds[1],
),
.pos = self.pos,
.text = line
});
self.pos.y += self.line_height * self.font.line_spacing;
var min_x = self.bounds.pos.x;
var max_x = self.bounds.pos.x + self.bounds.size.x;
var min_y = self.bounds.pos.y;
var max_y = self.bounds.pos.y + self.bounds.size.y;
min_x = @min(min_x, line_bounds[0]);
min_y = @min(min_y, line_bounds[1]);
max_x = @max(max_x, line_bounds[2]);
max_y = @max(max_y, line_bounds[3]);
self.bounds = Rect.init(min_x, min_y, max_x - min_x, max_y - min_y);
}
pub fn draw(self: *DrawTextContext, pos: Vec2, color: Vec4) void {
for (self.lines.items) |cmd| {
self.ctx.drawText(self.font, pos.add(cmd.pos), color, cmd.text);
}
}
pub fn isEmpty(self: *DrawTextContext) bool {
return self.lines.items.len == 0;
}
// pub fn drawLine(self: *DrawTextContext, line: []const u8) void {
// const fs = fons_context;
//
// _ = c.fonsDrawText(fs, self.pos.x, self.pos.y, line.ptr, line.ptr + line.len);
// self.pos.y += self.line_height * self.font.line_spacing;
// }
//
// pub fn drawLines(self: *DrawTextContext, lines: []const []const u8) void {
// for (lines) |line| {
// self.drawLine(line);
// }
// }
//
// pub fn drawText(self: *DrawTextContext, text: []const u8) void {
// var line_iter = std.mem.splitScalar(u8, text, '\n');
// while (line_iter.next()) |line| {
// self.drawLine(line);
// }
// }
//
// pub fn measureLine(self: *DrawTextContext, line: []const u8) void {
// const fs = fons_context;
//
// var min_x: f32 = 0;
// var min_y: f32 = 0;
// var max_x: f32 = 0;
// var max_y: f32 = 0;
//
// var line_iter = std.mem.splitScalar(u8, text, '\n');
// var y: f32 = 0;
// while (line_iter.next()) |line| {
// var line_bounds: [4]f32 = undefined;
//
// _ = c.fonsTextBounds(fs, 0, y, line.ptr, line.ptr + line.len, &line_bounds);
// y += line_height * font.line_spacing;
//
// min_x = @min(min_x, line_bounds[0]);
// min_y = @min(min_y, line_bounds[1]);
// max_x = @max(max_x, line_bounds[2]);
// max_y = @max(max_y, line_bounds[3]);
// }
//
// return Rect.init(min_x, min_y, max_x - min_x, max_y - min_y);
// }
pub fn end(self: *DrawTextContext) void {
_ = self;
}
};
id: Id,
size: f32,
spacing: f32 = 0,
line_spacing: f32 = 1,