sokoban-v2/src/font.zig

845 lines
26 KiB
Zig

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);
}
}
};