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,