From 9f3c41f99112dd6c07928febbd2bf0194a4d2a16 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Tue, 30 Dec 2025 15:42:01 +0200 Subject: [PATCH] move font handling to separate file --- src/assets.zig | 20 ++ src/engine/font.zig | 614 ++++++++++++++++++++++++++++++++++++++++ src/engine/graphics.zig | 597 +------------------------------------- src/game.zig | 23 +- 4 files changed, 661 insertions(+), 593 deletions(-) create mode 100644 src/engine/font.zig diff --git a/src/assets.zig b/src/assets.zig index 1c1a033..274a6cb 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -1,8 +1,28 @@ +const std = @import("std"); +const Font = @import("./engine/font.zig"); +const Gfx = @import("./engine/graphics.zig"); 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 { + 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{ + .font_map = font_map }; } diff --git a/src/engine/font.zig b/src/engine/font.zig new file mode 100644 index 0000000..55b288d --- /dev/null +++ b/src/engine/font.zig @@ -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, diff --git a/src/engine/graphics.zig b/src/engine/graphics.zig index 7e5a961..afe8c16 100644 --- a/src/engine/graphics.zig +++ b/src/engine/graphics.zig @@ -38,47 +38,11 @@ const hex = Math.rgb_hex; const rgba = Math.rgba; const log = std.log.scoped(.graphics); +pub const Font = @import("./font.zig"); + pub const white = rgb(255, 255, 255); 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 { position: Vec2, 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 { 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 pub var circle_quality: f32 = 6; 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 font_id_map: std.EnumArray(FontVariant, c_int) = .initFill(c.FONS_INVALID); - var image_map: std.EnumArray(ImageId, sg.Image) = .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); var tilemap_size: Vec2 = .init(0, 0); +pub var fonts: Font.Context = undefined; + const Options = struct { allocator: std.mem.Allocator, logger: sg.Logger = .{}, @@ -689,15 +135,6 @@ inline fn structCast(T: type, value: anytype) T { 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 { rgba8_pixels: [*c]u8, width: u32, @@ -785,8 +222,6 @@ fn makeImageFromMemory(image_datas: []const []const u8) !sg.Image { } pub fn init(options: Options) !void { - dpi_scale = sapp.dpiScale(); - draw_frame.init(); sg.setup(.{ @@ -827,19 +262,7 @@ pub fn init(options: Options) !void { imgui.addFont(@embedFile("../assets/roboto-font/Roboto-Regular.ttf"), 16); - // make sure the fontstash atlas width/height is pow-2 - 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"), - }); + fonts = try Font.Context.init(512); const tilemap = try ImageData.load(@embedFile("../assets/kenney-micro-roguelike/colored_tilemap_packed.png")); defer tilemap.deinit(); @@ -885,7 +308,7 @@ pub fn init(options: Options) !void { pub fn deinit() void { imgui.shutdown(); - c.sfons_destroy(g_fons_context); + fonts.deinit(); sgl.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 { - var ctx = DrawTextContext.init(font); + var ctx = Font.DrawTextContext.init(fonts, font); defer ctx.deinit(allocator); 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 { - var ctx = DrawTextContext.init(font); + var ctx = Font.DrawTextContext.init(fonts, font); return ctx.measureText(text); } @@ -1089,7 +512,7 @@ pub fn beginFrame() void { .dpi_scale = sapp.dpiScale() }); - c.fonsClearState(g_fons_context); + fonts.clearState(); sgl.defaults(); sgl.matrixModeProjection(); 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(.{ diff --git a/src/game.zig b/src/game.zig index c5c18e0..b569ddb 100644 --- a/src/game.zig +++ b/src/game.zig @@ -63,6 +63,7 @@ gpa: Allocator, canvas_size: Vec2, level: Level, +assets: *Assets, current_level: u32, levels: std.ArrayList(Level), @@ -83,13 +84,13 @@ finale_timer: ?Timer.Id = null, finale_counter: u32 = 0, pub fn init(gpa: Allocator, assets: *Assets) !Game { - _ = assets; // autofix var self = Game{ .gpa = gpa, .canvas_size = (Vec2.init(20, 15)), .level = .empty, .levels = .empty, .current_level = 0, + .assets = assets }; errdefer self.deinit(); @@ -484,30 +485,40 @@ pub fn tickFinale(self: *Game) !void { 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{ .{ .pos = Vec2.init(1, 1), - .font = Gfx.Font.default, + .font = regular_font, .text = "Congratulations scientist" }, .{ .pos = Vec2.init(1, 2), - .font = Gfx.Font.default, + .font = regular_font, .text = "You have passed the entrance exam" }, .{ .pos = Vec2.init(1, 3), - .font = Gfx.Font.default, + .font = regular_font, .text = "Here is your entry code" }, .{ .pos = Vec2.init(1, 5), - .font = Gfx.Font.bold, + .font = bold_font, .text = key }, .{ .pos = Vec2.init(1, 7), - .font = Gfx.Font.default, + .font = regular_font, .text = "I'll meet you at the lab" } };