move font handling to separate file

This commit is contained in:
Rokas Puzonas 2025-12-30 15:42:01 +02:00
parent 4df4f42022
commit 9f3c41f991
4 changed files with 661 additions and 593 deletions

View File

@ -1,8 +1,28 @@
const std = @import("std");
const Font = @import("./engine/font.zig");
const Gfx = @import("./engine/graphics.zig");
const Assets = @This(); const Assets = @This();
pub const FontVariant = enum {
regular,
italic,
bold,
const Map = std.EnumArray(FontVariant, Font.Id);
};
font_map: FontVariant.Map = .initFill(.invalid),
pub fn init() !Assets { pub fn init() !Assets {
const font_map: FontVariant.Map = .init(.{
.regular = try Gfx.fonts.add("regular", @embedFile("./assets/roboto-font/Roboto-Regular.ttf")),
.italic = try Gfx.fonts.add("italic", @embedFile("./assets/roboto-font/Roboto-Italic.ttf")),
.bold = try Gfx.fonts.add("bold", @embedFile("./assets/roboto-font/Roboto-Bold.ttf")),
});
return Assets{ return Assets{
.font_map = font_map
}; };
} }

614
src/engine/font.zig Normal file
View File

@ -0,0 +1,614 @@
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,

View File

@ -38,47 +38,11 @@ const hex = Math.rgb_hex;
const rgba = Math.rgba; const rgba = Math.rgba;
const log = std.log.scoped(.graphics); const log = std.log.scoped(.graphics);
pub const Font = @import("./font.zig");
pub const white = rgb(255, 255, 255); pub const white = rgb(255, 255, 255);
pub const black = rgb(0, 0, 0); pub const black = rgb(0, 0, 0);
// 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 Vertex = extern struct { const Vertex = extern struct {
position: Vec2, position: Vec2,
color: Vec4, color: Vec4,
@ -100,364 +64,6 @@ const DrawFrame = struct {
} }
}; };
pub const FontVariant = enum {
regular,
italic,
bold
};
pub const Font = struct {
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 {
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(font: Font) QuadIterator {
const stash = g_fons_context;
font.setAsCurrent();
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{
.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 = g_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(font: Font, text: []const u8) Ascii {
return Ascii{
.iter = .init(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(font: Font, text: []const u21) Unicode {
return Unicode{
.iter = .init(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;
}
};
};
// TODO: Maybe this should also have a `color: Vec4` field?
variant: FontVariant,
size: f32,
spacing: f32 = 0,
line_spacing: f32 = 1,
pub const default = Font{
.variant = .regular,
.size = 64,
};
pub const bold = Font{
.variant = .bold,
.size = 64,
};
pub fn setAsCurrent(self: Font) void {
const fs = g_fons_context;
c.fonsSetFont(fs, font_id_map.get(self.variant));
c.fonsSetSize(fs, self.size * dpi_scale);
c.fonsSetAlign(fs, Font.AlignX.left.toFONSAlignment() | Font.AlignY.top.toFONSAlignment());
c.fonsSetSpacing(fs, self.spacing);
}
pub fn drawText(self: Font, pos: Vec2, color: Vec4, text: []const u8) void {
const fs = g_fons_context;
// TODO: Test if calling `.setAsCurrent()` isn't expensive
self.setAsCurrent();
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);
pushTransform(.init(0, 0), 1.0/64.0);
defer popTransform();
_ = c.fonsDrawText(
fs,
pos.x*64,
pos.y*64,
text.ptr,
text.ptr + text.len
);
}
pub fn getBounds(self: Font, text: []const u8) Rect {
const fs = g_fons_context;
self.setAsCurrent();
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);
}
// 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 fn getBoundsUtf8(self: Font, text: []const u21) Rect {
const fs = g_fons_context;
self.setAsCurrent();
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: Font, text: []const u8) Vec2 {
const bounds = self.getBounds(text);
return bounds.size;
}
pub fn quadIter(self: Font, text: []const u8) QuadIterator.Ascii {
return QuadIterator.Ascii.init(self, text);
}
pub fn quadUnicodeIter(self: Font, text: []const u21) QuadIterator.Unicode {
return QuadIterator.Unicode.init(self, text);
}
};
pub const ImageId = enum { pub const ImageId = enum {
tilemap tilemap
}; };
@ -502,174 +108,12 @@ pub const Corners = struct {
} }
}; };
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
font: Font,
line_height: f32 = 0,
pos: Vec2 = Vec2.zero,
lines: std.ArrayListUnmanaged(Line) = .empty,
bounds: Rect = Rect.zero,
pub fn init(font: Font) DrawTextContext {
var self = DrawTextContext{
.font = font
};
const fs = g_fons_context;
self.font.setAsCurrent();
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 = g_fons_context;
self.font.setAsCurrent();
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 = g_fons_context;
self.font.setAsCurrent();
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.font.drawText(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;
}
};
// The lower the better. Best quality with 1 // The lower the better. Best quality with 1
pub var circle_quality: f32 = 6; pub var circle_quality: f32 = 6;
pub var draw_frame: DrawFrame = undefined; pub var draw_frame: DrawFrame = undefined;
var dpi_scale: f32 = 1;
var g_fons_context: ?*c.struct_FONScontext = null;
var main_pipeline: sgl.Pipeline = .{}; var main_pipeline: sgl.Pipeline = .{};
var font_id_map: std.EnumArray(FontVariant, c_int) = .initFill(c.FONS_INVALID);
var image_map: std.EnumArray(ImageId, sg.Image) = .initFill(.{}); var image_map: std.EnumArray(ImageId, sg.Image) = .initFill(.{});
var image_view_map: std.EnumArray(ImageId, sg.View) = .initFill(.{}); var image_view_map: std.EnumArray(ImageId, sg.View) = .initFill(.{});
@ -680,6 +124,8 @@ var tile_coords: std.EnumArray(TileId, Vec2) = .initUndefined();
const tile_size: Vec2 = .init(8, 8); const tile_size: Vec2 = .init(8, 8);
var tilemap_size: Vec2 = .init(0, 0); var tilemap_size: Vec2 = .init(0, 0);
pub var fonts: Font.Context = undefined;
const Options = struct { const Options = struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
logger: sg.Logger = .{}, logger: sg.Logger = .{},
@ -689,15 +135,6 @@ inline fn structCast(T: type, value: anytype) T {
return @as(*T, @ptrFromInt(@intFromPtr(&value))).*; return @as(*T, @ptrFromInt(@intFromPtr(&value))).*;
} }
fn loadEmbededFont(fs: ?*c.FONScontext, name: [*c]const u8, comptime path: []const u8) c_int {
const data = @embedFile(path);
const font_id = c.fonsAddFontMem(fs, name, @constCast(data.ptr), data.len, 0);
assert(font_id != c.FONS_INVALID);
return font_id;
}
const ImageData = struct { const ImageData = struct {
rgba8_pixels: [*c]u8, rgba8_pixels: [*c]u8,
width: u32, width: u32,
@ -785,8 +222,6 @@ fn makeImageFromMemory(image_datas: []const []const u8) !sg.Image {
} }
pub fn init(options: Options) !void { pub fn init(options: Options) !void {
dpi_scale = sapp.dpiScale();
draw_frame.init(); draw_frame.init();
sg.setup(.{ sg.setup(.{
@ -827,19 +262,7 @@ pub fn init(options: Options) !void {
imgui.addFont(@embedFile("../assets/roboto-font/Roboto-Regular.ttf"), 16); imgui.addFont(@embedFile("../assets/roboto-font/Roboto-Regular.ttf"), 16);
// make sure the fontstash atlas width/height is pow-2 fonts = try Font.Context.init(512);
const atlas_dim = std.math.ceilPowerOfTwoAssert(u32, @intFromFloat(512 * dpi_scale));
g_fons_context = c.sfons_create(&c.sfons_desc_t{
.width = @intCast(atlas_dim),
.height = @intCast(atlas_dim),
});
assert(g_fons_context != null);
font_id_map = .init(.{
.regular = loadEmbededFont(g_fons_context, "regular", "../assets/roboto-font/Roboto-Regular.ttf"),
.italic = loadEmbededFont(g_fons_context, "italic", "../assets/roboto-font/Roboto-Italic.ttf"),
.bold = loadEmbededFont(g_fons_context, "bold", "../assets/roboto-font/Roboto-Bold.ttf"),
});
const tilemap = try ImageData.load(@embedFile("../assets/kenney-micro-roguelike/colored_tilemap_packed.png")); const tilemap = try ImageData.load(@embedFile("../assets/kenney-micro-roguelike/colored_tilemap_packed.png"));
defer tilemap.deinit(); defer tilemap.deinit();
@ -885,7 +308,7 @@ pub fn init(options: Options) !void {
pub fn deinit() void { pub fn deinit() void {
imgui.shutdown(); imgui.shutdown();
c.sfons_destroy(g_fons_context); fonts.deinit();
sgl.shutdown(); sgl.shutdown();
sg.shutdown(); sg.shutdown();
} }
@ -1022,7 +445,7 @@ pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
} }
pub fn drawText(allocator: Allocator, pos: Vec2, font: Font, color: Vec4, text: []const u8) !void { pub fn drawText(allocator: Allocator, pos: Vec2, font: Font, color: Vec4, text: []const u8) !void {
var ctx = DrawTextContext.init(font); var ctx = Font.DrawTextContext.init(fonts, font);
defer ctx.deinit(allocator); defer ctx.deinit(allocator);
try ctx.queueText(allocator, text); try ctx.queueText(allocator, text);
@ -1030,7 +453,7 @@ pub fn drawText(allocator: Allocator, pos: Vec2, font: Font, color: Vec4, text:
} }
pub fn measureText(font: Font, text: []const u8) Rect { pub fn measureText(font: Font, text: []const u8) Rect {
var ctx = DrawTextContext.init(font); var ctx = Font.DrawTextContext.init(fonts, font);
return ctx.measureText(text); return ctx.measureText(text);
} }
@ -1089,7 +512,7 @@ pub fn beginFrame() void {
.dpi_scale = sapp.dpiScale() .dpi_scale = sapp.dpiScale()
}); });
c.fonsClearState(g_fons_context); fonts.clearState();
sgl.defaults(); sgl.defaults();
sgl.matrixModeProjection(); sgl.matrixModeProjection();
sgl.ortho(0, draw_frame.screen_size.x, draw_frame.screen_size.y, 0, -1, 1); sgl.ortho(0, draw_frame.screen_size.x, draw_frame.screen_size.y, 0, -1, 1);
@ -1114,7 +537,7 @@ pub fn endFrame() void {
} }
}; };
c.sfons_flush(g_fons_context); fonts.flush();
{ {
sg.beginPass(.{ sg.beginPass(.{

View File

@ -63,6 +63,7 @@ gpa: Allocator,
canvas_size: Vec2, canvas_size: Vec2,
level: Level, level: Level,
assets: *Assets,
current_level: u32, current_level: u32,
levels: std.ArrayList(Level), levels: std.ArrayList(Level),
@ -83,13 +84,13 @@ finale_timer: ?Timer.Id = null,
finale_counter: u32 = 0, finale_counter: u32 = 0,
pub fn init(gpa: Allocator, assets: *Assets) !Game { pub fn init(gpa: Allocator, assets: *Assets) !Game {
_ = assets; // autofix
var self = Game{ var self = Game{
.gpa = gpa, .gpa = gpa,
.canvas_size = (Vec2.init(20, 15)), .canvas_size = (Vec2.init(20, 15)),
.level = .empty, .level = .empty,
.levels = .empty, .levels = .empty,
.current_level = 0, .current_level = 0,
.assets = assets
}; };
errdefer self.deinit(); errdefer self.deinit();
@ -484,30 +485,40 @@ pub fn tickFinale(self: *Game) !void {
text: []const u8, text: []const u8,
}; };
const regular_font = Gfx.Font{
.id = self.assets.font_map.get(.regular),
.size = 64
};
const bold_font = Gfx.Font{
.id = self.assets.font_map.get(.bold),
.size = 64
};
const lines = [5]Line{ const lines = [5]Line{
.{ .{
.pos = Vec2.init(1, 1), .pos = Vec2.init(1, 1),
.font = Gfx.Font.default, .font = regular_font,
.text = "Congratulations scientist" .text = "Congratulations scientist"
}, },
.{ .{
.pos = Vec2.init(1, 2), .pos = Vec2.init(1, 2),
.font = Gfx.Font.default, .font = regular_font,
.text = "You have passed the entrance exam" .text = "You have passed the entrance exam"
}, },
.{ .{
.pos = Vec2.init(1, 3), .pos = Vec2.init(1, 3),
.font = Gfx.Font.default, .font = regular_font,
.text = "Here is your entry code" .text = "Here is your entry code"
}, },
.{ .{
.pos = Vec2.init(1, 5), .pos = Vec2.init(1, 5),
.font = Gfx.Font.bold, .font = bold_font,
.text = key .text = key
}, },
.{ .{
.pos = Vec2.init(1, 7), .pos = Vec2.init(1, 7),
.font = Gfx.Font.default, .font = regular_font,
.text = "I'll meet you at the lab" .text = "I'll meet you at the lab"
} }
}; };