const std = @import("std"); const log = std.log.scoped(.font); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Io = std.Io; const Platform = @import("platform"); const Gfx = Platform.Gfx; const SlotMapType = Platform.SlotMapType; const Vec2 = Platform.Vec2; const Rect = Platform.Rect; const Color = Platform.Color; const STBTrueType = @import("stb_truetype"); const Font = @This(); spritesheet: Gfx.Spritesheet.Id, stb: STBTrueType.Font, cache: GlyphCache, const Options = struct { spritesheet: Gfx.Spritesheet.Id, ttf_data: [:0]const u8, glyph_slots: u32 = 256 }; pub fn init(gpa: Allocator, opts: Options) !Font { const offset = STBTrueType.Font.getFontOffsetForIndex(opts.ttf_data, 0) orelse return error.MissingFont0; const stb_font = try STBTrueType.Font.init(opts.ttf_data, offset); var cache = try GlyphCache.init(gpa, opts.glyph_slots); errdefer cache.deinit(gpa); return Font{ .spritesheet = opts.spritesheet, .stb = stb_font, .cache = cache, }; } pub fn deinit(self: *Font, gpa: Allocator) void { self.cache.deinit(gpa); } pub fn getGlyphIndex(font: *Font, codepoint: u21) ?Glyph.Index { if (font.stb.findGlyphIndex(codepoint)) |index| { return index; } else { // Fallback return font.getGlyphIndex('?'); } } pub fn getGlyph(font: *Font, index: Glyph.Index, scale_x: f32, scale_y: f32) ?*Font.Glyph { const glyph_key = Font.Glyph.Key{ .index = index, .scale_x = .fromF32(scale_x), .scale_y = .fromF32(scale_y) }; var iog_result = font.cache.insertOrGet(glyph_key); if (iog_result == null and font.cache.lru_tail != GlyphSlot.nil_id) { const id = font.cache.lru_tail; if (font.cache.get(id).last_used_frame != Gfx.getFrameIndex()) { font.cache.remove(id); iog_result = font.cache.insertOrGet(glyph_key); log.debug("Evicted least recently used glyph", .{}); } } if (iog_result) |result| { const glyph_id = result.id; const glyph = font.cache.get(glyph_id); if (!result.found_existing) { glyph.box = font.stb.getGlyphBitmapBox( glyph_key.index, glyph_key.scale_x.toF32(), glyph_key.scale_y.toF32() ); const box_width: u32 = @intCast(glyph.box.x1 - glyph.box.x0); const box_height: u32 = @intCast(glyph.box.y1 - glyph.box.y0); if (box_width > 0 and box_height > 0) { const sprite = Gfx.initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 }); assert(box_width < Font.Glyph.max_width); assert(box_height < Font.Glyph.max_height); var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined; const bitmap = Gfx.Image{ .pixels = .{ .r8 = (&bitmap_buffer).ptr }, .width = box_width, .height = box_height }; font.stb.makeGlyphBitmap( bitmap.pixels.?.r8, box_width, box_height, box_width, glyph_key.scale_x.toF32(), glyph_key.scale_y.toF32(), glyph_key.index ); Gfx.setSprite(sprite, .{ .image = bitmap }); glyph.sprite = sprite; } } glyph.last_used_frame = Gfx.getFrameIndex(); return glyph; } else { log.warn("Failed to render glyph, because cache is full", .{}); return null; } } pub fn getAscent(font: *Font, height: f32) f32 { const vmetrics = font.stb.getFontVMetrics(); const scale = font.stb.scaleForPixelHeight(height); return @as(f32, @floatFromInt(vmetrics.ascent)) * scale; } pub fn measureText(font: *Font, height: f32, text: []const u8) Rect { var bounds: Rect = .init(0, 0, 0, 0); var first_glyph = false; var layout = TextRunLayout.init(font, height); var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 }; while (iter.nextCodepoint()) |codepoint| { const glyph_index = font.getGlyphIndex(codepoint) orelse continue; const glyph_layout = layout.next(glyph_index) orelse continue; if (first_glyph) { bounds = glyph_layout.rect; first_glyph = false; } else { bounds = bounds.expand(glyph_layout.rect); } } return bounds; } pub const DrawTextOptions = struct { text: []const u8, pos: Vec2, height: f32, color: Gfx.Color = .white, alignment: Vec2 = .init(0, 0) }; pub fn drawText(font: *Font, opts: DrawTextOptions) void { var layout = Font.TextRunLayout.init(font, opts.height); var pos = opts.pos; pos.y += font.getAscent(opts.height); if (!opts.alignment.eql(.zero)) { const bounds = font.measureText(opts.height, opts.text); pos = pos.sub(bounds.size.multiply(opts.alignment)); } var iter = std.unicode.Utf8Iterator{ .bytes = opts.text, .i = 0 }; while (iter.nextCodepoint()) |codepoint| { const glyph_index = font.getGlyphIndex(codepoint) orelse continue; const glyph_layout = layout.next(glyph_index) orelse continue; if (glyph_layout.sprite == Gfx.Sprite.Id.nil) { continue; } var glyph_pos = glyph_layout.rect.pos.add(pos); // Round to nearest pixel to avoid artifacts glyph_pos.x = @round(glyph_pos.x); glyph_pos.y = @round(glyph_pos.y); Gfx.drawSprite( glyph_layout.sprite, glyph_pos, glyph_layout.rect.size, opts.color ); } } const GlyphCache = struct { glyphs: []GlyphSlot, glyphs_hashmap: []GlyphSlot.Id, glyphs_used: std.bit_set.DynamicBitSetUnmanaged, lru_head: GlyphSlot.Id, lru_tail: GlyphSlot.Id, free_head: GlyphSlot.Id, fn init(gpa: Allocator, capacity: u32) !GlyphCache { assert(capacity > 0); const glyphs = try gpa.alloc(GlyphSlot, capacity); errdefer gpa.free(glyphs); const glyphs_hashmap = try gpa.alloc(GlyphSlot.Id, 2*capacity); errdefer gpa.free(glyphs); var glyphs_used = try std.bit_set.DynamicBitSetUnmanaged.initEmpty(gpa, capacity); errdefer glyphs_used.deinit(gpa); var self: GlyphCache = .{ .glyphs = glyphs, .glyphs_hashmap = glyphs_hashmap, .glyphs_used = glyphs_used, .lru_head = GlyphSlot.nil_id, .lru_tail = GlyphSlot.nil_id, .free_head = undefined }; @memset(self.glyphs_hashmap, GlyphSlot.nil_id); @memset(self.glyphs, .{ .glyph = undefined, .lru_prev = GlyphSlot.nil_id, .lru_next = GlyphSlot.nil_id, .next_free = GlyphSlot.nil_id, }); assert(self.glyphs_hashmap.len > self.glyphs.len); self.free_head = 1; for (1.., self.glyphs[1..(self.glyphs.len-1)]) |i, *glyph| { glyph.next_free = @intCast(i + 1); } return self; } fn deinit(self: *GlyphCache, gpa: Allocator) void { for (self.glyphs_hashmap) |glyph_id| { if (glyph_id == GlyphSlot.nil_id) { continue; } const slot = self.glyphs[glyph_id]; slot.glyph.deinit(); } gpa.free(self.glyphs); gpa.free(self.glyphs_hashmap); self.glyphs_used.deinit(gpa); self.* = undefined; } fn get(self: *GlyphCache, id: GlyphSlot.Id) *Glyph { return &self.glyphs[id].glyph; } fn appendToLRUHead(self: *GlyphCache, id: GlyphSlot.Id) void { if (self.lru_head != GlyphSlot.nil_id) { self.glyphs[self.lru_head].lru_prev = id; self.glyphs[id].lru_next = self.lru_head; } else { assert(self.lru_tail == GlyphSlot.nil_id); self.lru_tail = id; } self.glyphs[id].lru_prev = GlyphSlot.nil_id; self.lru_head = id; } fn removeFromLRU(self: *GlyphCache, id: GlyphSlot.Id) void { assert(id != GlyphSlot.nil_id); const lru_prev = self.glyphs[id].lru_prev; const lru_next = self.glyphs[id].lru_next; if (lru_prev != GlyphSlot.nil_id) { self.glyphs[lru_prev].lru_next = lru_next; } if (lru_next != GlyphSlot.nil_id) { self.glyphs[lru_next].lru_prev = lru_prev; } if (id == self.lru_head) { self.lru_head = lru_next; } if (id == self.lru_tail) { self.lru_tail = lru_prev; } } fn popFreeId(self: *GlyphCache) ?GlyphSlot.Id { if (self.free_head != GlyphSlot.nil_id) { const result = self.free_head; self.free_head = self.glyphs[result].next_free; return result; } return null; } fn pushFreeId(self: *GlyphCache, id: GlyphSlot.Id) void { assert(id != GlyphSlot.nil_id); self.glyphs[id].next_free = self.free_head; self.free_head = id; } pub const InsertOrGetResult = struct { id: GlyphSlot.Id, found_existing: bool, }; fn insertOrGet(self: *GlyphCache, key: Glyph.Key) ?InsertOrGetResult { for (0..self.glyphs_hashmap.len) |offset| { const i = (key.toInt() + offset) % self.glyphs_hashmap.len; const hashmap_id = self.glyphs_hashmap[i]; if (hashmap_id == GlyphSlot.nil_id) { const id = self.popFreeId() orelse return null; self.glyphs_hashmap[i] = id; self.glyphs[id].glyph = .init(key); self.glyphs[id].lru_next = GlyphSlot.nil_id; self.glyphs[id].lru_prev = GlyphSlot.nil_id; self.appendToLRUHead(id); return InsertOrGetResult{ .id = id, .found_existing = false }; } else { const glyph = &self.glyphs[hashmap_id].glyph; if (glyph.key == key) { // An entry for this key already exists self.removeFromLRU(hashmap_id); self.appendToLRUHead(hashmap_id); return InsertOrGetResult{ .id = hashmap_id, .found_existing = true }; } } } return null; } fn removeFromHashmap(self: *GlyphCache, id: GlyphSlot.Id) void { var bucket = self.glyphs[id].glyph.key.toInt() % self.glyphs_hashmap.len; for (0..self.glyphs_hashmap.len) |_| { assert(self.glyphs_hashmap[bucket] != GlyphSlot.nil_id); if (self.glyphs_hashmap[bucket] == id) { break; } bucket = (bucket + 1) % self.glyphs_hashmap.len; } assert(self.glyphs_hashmap[bucket] == id); var empty = bucket; var current = (bucket + 1) % self.glyphs_hashmap.len; while (self.glyphs_hashmap[current] != GlyphSlot.nil_id) { const current_glyph_id = self.glyphs_hashmap[current]; const home = self.glyphs[current_glyph_id].glyph.key.toInt() % self.glyphs_hashmap.len; if (self.hashmapDistance(home, current) > self.hashmapDistance(home, empty)) { self.glyphs_hashmap[empty] = self.glyphs_hashmap[current]; empty = current; } current = (current + 1) % self.glyphs_hashmap.len; } assert(self.glyphs_hashmap[empty] != GlyphSlot.nil_id); self.glyphs_hashmap[empty] = GlyphSlot.nil_id; } fn hashmapDistance(self: GlyphCache, from: usize, to: usize) usize { if (to >= from) { return to - from; } else { return self.glyphs_hashmap.len - from + to; } } fn remove(self: *GlyphCache, id: GlyphSlot.Id) void { assert(id != GlyphSlot.nil_id); self.pushFreeId(id); self.removeFromHashmap(id); self.removeFromLRU(id); const glyph = self.glyphs[id].glyph; glyph.deinit(); } fn getLRUItems(self: *GlyphCache, gpa: Allocator) ![]GlyphSlot.Id { var lru: std.ArrayList(GlyphSlot.Id) = .empty; defer lru.deinit(gpa); var lru_iter = self.lru_head; while (lru_iter != GlyphSlot.nil_id) { try lru.append(gpa, lru_iter); lru_iter = self.glyphs[lru_iter].lru_next; } return try lru.toOwnedSlice(gpa); } test { const gpa = std.testing.allocator; var cache = try GlyphCache.init(gpa, 256); defer cache.deinit(gpa); const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const id1 = cache.insertOrGet(key).?.id; const id2 = cache.insertOrGet(key).?.id; try std.testing.expectEqual(id1, id2); } test { const gpa = std.testing.allocator; var cache = try GlyphCache.init(gpa, 256); defer cache.deinit(gpa); const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const id1 = cache.insertOrGet(key).?.id; cache.remove(id1); const id2 = cache.insertOrGet(key).?.id; try std.testing.expectEqual(id1, id2); } test { const gpa = std.testing.allocator; var cache = try GlyphCache.init(gpa, 256); defer cache.deinit(gpa); const id1 = cache.insertOrGet(Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id2 = cache.insertOrGet(Glyph.Key{ .index = 11, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; try std.testing.expect(id1 != id2); } test { const gpa = std.testing.allocator; const limit = 256; var cache = try GlyphCache.init(gpa, limit); defer cache.deinit(gpa); for (0..limit-1) |i| { const id = cache.insertOrGet(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) }); try std.testing.expect(id != null); } const id = cache.insertOrGet(Glyph.Key{ .index = limit, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }); try std.testing.expect(id == null); } test { const gpa = std.testing.allocator; const limit = 256; var cache = try GlyphCache.init(gpa, limit); defer cache.deinit(gpa); for (0..10) |_| { var ids: std.ArrayList(GlyphSlot.Id) = .empty; defer ids.deinit(gpa); for (0..(limit-1)) |i| { const id = cache.insertOrGet(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) }); try std.testing.expect(id != null); try ids.append(gpa, id.?.id); } assert(cache.free_head == GlyphSlot.nil_id); const lru = try cache.getLRUItems(gpa); defer gpa.free(lru); const expected_lru = try gpa.dupe(GlyphSlot.Id, ids.items); defer gpa.free(expected_lru); std.mem.reverse(GlyphSlot.Id, expected_lru); try std.testing.expectEqualSlices(GlyphSlot.Id, expected_lru, lru); for (ids.items) |id| { cache.remove(id); } assert(cache.lru_head == GlyphSlot.nil_id); assert(cache.lru_tail == GlyphSlot.nil_id); } } test { const gpa = std.testing.allocator; var cache = try GlyphCache.init(gpa, 256); defer cache.deinit(gpa); const id1 = cache.insertOrGet(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id2 = cache.insertOrGet(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id3 = cache.insertOrGet(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const lru = try cache.getLRUItems(gpa); defer gpa.free(lru); try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); } test { const gpa = std.testing.allocator; var cache = try GlyphCache.init(gpa, 256); defer cache.deinit(gpa); const id1 = cache.insertOrGet(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id2 = cache.insertOrGet(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id3 = cache.insertOrGet(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const lru = try cache.getLRUItems(gpa); defer gpa.free(lru); try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); } test { const gpa = std.testing.allocator; var cache = try GlyphCache.init(gpa, 256); defer cache.deinit(gpa); const id = cache.insertOrGet(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; try std.testing.expect(cache.lru_head != GlyphSlot.nil_id); try std.testing.expect(cache.lru_tail != GlyphSlot.nil_id); cache.remove(id); try std.testing.expect(cache.lru_head == GlyphSlot.nil_id); try std.testing.expect(cache.lru_tail == GlyphSlot.nil_id); } test { const gpa = std.testing.allocator; var cache = try GlyphCache.init(gpa, 256); defer cache.deinit(gpa); const key1 = Glyph.Key{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const id1 = cache.insertOrGet(key1).?.id; const key2 = Glyph.Key{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const id2 = cache.insertOrGet(key2).?.id; const key3 = Glyph.Key{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const id3 = cache.insertOrGet(key3).?.id; _ = cache.insertOrGet(key2); { const lru = try cache.getLRUItems(gpa); defer gpa.free(lru); try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id2, id3, id1 }, lru); } _ = cache.insertOrGet(key1); { const lru = try cache.getLRUItems(gpa); defer gpa.free(lru); try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id1, id2, id3 }, lru); } } test { const gpa = std.testing.allocator; var cache = try GlyphCache.init(gpa, 256); defer cache.deinit(gpa); const id1 = cache.insertOrGet(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id2 = cache.insertOrGet(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id3 = cache.insertOrGet(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; { const lru = try cache.getLRUItems(gpa); defer gpa.free(lru); try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); } _ = cache.remove(id2); { const lru = try cache.getLRUItems(gpa); defer gpa.free(lru); try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id1 }, lru); } _ = cache.remove(id3); { const lru = try cache.getLRUItems(gpa); defer gpa.free(lru); try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id1 }, lru); } _ = cache.remove(id1); { const lru = try cache.getLRUItems(gpa); defer gpa.free(lru); try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ }, lru); } } }; const GlyphSlot = struct { glyph: Glyph, lru_next: GlyphSlot.Id, lru_prev: GlyphSlot.Id, next_free: GlyphSlot.Id, const Id = u16; const nil_id: GlyphSlot.Id = 0; comptime { assert(@bitSizeOf(Id) % 8 == 0); } }; const Glyph = struct { key: Key, box: STBTrueType.Box, sprite: Gfx.Sprite.Id, last_used_frame: u32, const max_width: u32 = 512; const max_height: u32 = 512; const Index = u32; const Key = packed struct(u64) { index: Index, scale_x: q4d12, scale_y: q4d12, pub fn toInt(self: Key) u64 { return @bitCast(self); } // 4 integer bits, 12 fractional bits const q4d12 = enum(u16) { _, pub fn toF32(integer: q4d12) f32 { return @as(f32, @floatFromInt(@intFromEnum(integer))) / (1 << 12); } pub fn fromF32(float: f32) q4d12 { return @enumFromInt(@as(u16, @intFromFloat(float * (1 << 12)))); } pub fn format(self: q4d12, writer: *Io.Writer) Io.Writer.Error!void { try writer.print("q12d4{{ {} }}", .{self.toF32()}); } }; test { try std.testing.expectEqual(q4d12.fromF32(10.0625).toF32(), 10.0625); } }; fn init(key: Key) Glyph { return Glyph{ .key = key, .box = .{ .x0 = 0, .y0 = 0, .x1 = 0, .y1 = 0, }, .sprite = .nil, .last_used_frame = 0 }; } fn deinit(self: Glyph) void { Gfx.deinitSprite(self.sprite); } }; pub const TextRunLayout = struct { font: *Font, scale_x: f32, scale_y: f32, prev_glyph: ?Font.Glyph.Index, pen_pos: Vec2, const GlyphRect = struct { rect: Rect, sprite: Gfx.Sprite.Id, }; pub fn init(font: *Font, height: f32) TextRunLayout { const scale = font.stb.scaleForPixelHeight(height); return TextRunLayout{ .font = font, .scale_x = scale, .scale_y = scale, .prev_glyph = null, .pen_pos = .init(0, 0) }; } pub fn next(self: *TextRunLayout, glyph_index: Font.Glyph.Index) ?GlyphRect { const glyph = self.font.getGlyph(glyph_index, self.scale_x, self.scale_y) orelse return null; if (self.prev_glyph) |prev_glyph| { const kerning = self.font.stb.getGlyphKernAdvance(prev_glyph, glyph_index); self.pen_pos.x += @as(f32, @floatFromInt(kerning)) * self.scale_x; } const hmetrics = self.font.stb.getGlyphHMetrics(glyph_index); const glyph_size = Vec2.initFromInt(i32, glyph.box.x1 - glyph.box.x0, glyph.box.y1 - glyph.box.y0); var draw_pos = self.pen_pos; draw_pos.x -= @floatFromInt(glyph.box.x0); draw_pos.x += @as(f32, @floatFromInt(hmetrics.left_side_bearing)) * self.scale_x; draw_pos.y += @floatFromInt(glyph.box.y0); const draw_rect = Rect{ .pos = draw_pos, .size = glyph_size }; self.pen_pos.x += @as(f32, @floatFromInt(hmetrics.advance_width)) * self.scale_x; self.prev_glyph = glyph_index; return GlyphRect{ .rect = draw_rect, .sprite = glyph.sprite }; } }; pub const TextBuilder = struct { gpa: std.mem.Allocator, glyphs: std.ArrayList(TextBuilder.Glyph), color: Color, baseline: f32, layout: ?Font.TextRunLayout, bounds: Rect, const Glyph = struct { sprite: Gfx.Sprite.Id, rect: Rect, color: Color }; pub fn init(gpa: std.mem.Allocator) TextBuilder { return TextBuilder{ .gpa = gpa, .glyphs = .empty, .color = .white, .baseline = 0, .layout = null, .bounds = .init(0, 0, 0, 0) }; } pub fn font(self: *TextBuilder, new_font: *Font, height: f32) void { var new_layout = Font.TextRunLayout.init(new_font, height); if (self.layout) |layout| { if (new_layout.font == layout.font and new_layout.scale_x == layout.scale_x and new_layout.scale_y == layout.scale_y ) { return; } } const font_baseline = new_font.getAscent(height); if (font_baseline > self.baseline) { // Fixup previously drawn glyph to match the new larger baseline const offset_y = font_baseline - self.baseline; for (self.glyphs.items) |*glyph| { glyph.rect.pos.y += offset_y; } self.bounds.pos.y += offset_y; self.baseline = font_baseline; } if (self.layout) |layout| { new_layout.pen_pos = layout.pen_pos; if (layout.font == new_layout.font) { new_layout.prev_glyph = layout.prev_glyph; } } self.layout = new_layout; } pub fn putCodepoint(self: *TextBuilder, codepoint: u21) !void { if (self.layout == null) { return; } const layout = &self.layout.?; const glyph_index = layout.font.getGlyphIndex(codepoint) orelse return; const glyph_layout = layout.next(glyph_index) orelse return; var rect = glyph_layout.rect; rect.pos.y += self.baseline; try self.glyphs.append(self.gpa, TextBuilder.Glyph{ .sprite = glyph_layout.sprite, .rect = rect, .color = self.color }); if (self.bounds.size.x > 0 and self.bounds.size.y > 0) { self.bounds = self.bounds.expand(rect); } else { self.bounds = rect; } } pub fn put(self: *TextBuilder, text: []const u8) !void { var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 }; while (iter.nextCodepoint()) |codepoint| { try self.putCodepoint(codepoint); } } pub fn draw(self: *TextBuilder, pos: Vec2) void { for (self.glyphs.items) |glyph| { Gfx.drawSprite(glyph.sprite, glyph.rect.pos.add(pos), glyph.rect.size, glyph.color); } } pub fn offset(self: *TextBuilder, vec2: Vec2) void { if (self.layout) |*layout| { layout.pen_pos = layout.pen_pos.add(vec2); } } };