a73x

c6a25e27

Add Atlas.reset and Face.reinit for scale changes

a73x   2026-04-09 10:19

Atlas.reset() zeroes pixel memory, restores the cursor-white pixel at
(0,0), rewinds packing cursors, clears the glyph cache, and marks the
atlas dirty. Face.reinit() replaces the underlying FT_Face at a new
px_size without re-creating the FT_Library.

These are the primitives Task 5/6 need to rebuild rasterization when
the wl_surface buffer scale changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

diff --git a/src/font.zig b/src/font.zig
index fe1cde6..89f3001 100644
--- a/src/font.zig
+++ b/src/font.zig
@@ -85,6 +85,25 @@ pub const Face = struct {
        _ = c.FT_Done_FreeType(self.library);
    }

    pub fn reinit(
        self: *Face,
        path: [:0]const u8,
        index: c_int,
        px_size: u32,
    ) !void {
        _ = c.FT_Done_Face(self.face);
        self.face = null;

        var new_face: c.FT_Face = null;
        if (c.FT_New_Face(self.library, path.ptr, index, &new_face) != 0) return error.FtNewFaceFailed;
        errdefer _ = c.FT_Done_Face(new_face);

        if (c.FT_Set_Pixel_Sizes(new_face, 0, px_size) != 0) return error.FtSetPixelSizesFailed;

        self.face = new_face;
        self.px_size = px_size;
    }

    pub fn rasterize(self: *Face, codepoint: u21) !Glyph {
        const glyph_index = c.FT_Get_Char_Index(self.face, codepoint);
        if (c.FT_Load_Glyph(self.face, glyph_index, c.FT_LOAD_RENDER) != 0) {
@@ -191,6 +210,16 @@ pub const Atlas = struct {
        self.cache.deinit();
    }

    pub fn reset(self: *Atlas) void {
        @memset(self.pixels, 0);
        self.pixels[0] = 255;
        self.cursor_x = 1;
        self.cursor_y = 0;
        self.row_height = 1;
        self.cache.clearRetainingCapacity();
        self.dirty = true;
    }

    pub fn cursorUV(self: *const Atlas) GlyphUV {
        return .{
            .u0 = 0,
@@ -301,3 +330,39 @@ test "Atlas reserves a white pixel for cursor rendering" {
    try std.testing.expectEqual(@as(f32, 0), uv.u0);
    try std.testing.expectEqual(@as(f32, 0), uv.v0);
}

test "Atlas.reset clears cache and starts fresh" {
    var lookup = try lookupConfiguredFont(std.testing.allocator);
    defer lookup.deinit(std.testing.allocator);

    var face = try Face.init(std.testing.allocator, lookup.path, lookup.index, 14);
    defer face.deinit();

    var atlas = try Atlas.init(std.testing.allocator, 256, 256);
    defer atlas.deinit();

    _ = try atlas.getOrInsert(&face, 'A');
    try std.testing.expect(atlas.cache.count() > 0);

    atlas.reset();
    try std.testing.expectEqual(@as(u32, 0), atlas.cache.count());
    try std.testing.expectEqual(@as(u8, 255), atlas.pixels[0]);
    try std.testing.expect(atlas.dirty);

    // Re-inserting the same glyph should succeed after reset.
    _ = try atlas.getOrInsert(&face, 'A');
}

test "Face.reinit switches px_size and produces different cell metrics" {
    var lookup = try lookupConfiguredFont(std.testing.allocator);
    defer lookup.deinit(std.testing.allocator);

    var face = try Face.init(std.testing.allocator, lookup.path, lookup.index, 14);
    defer face.deinit();
    const small_cell = face.cellWidth();

    try face.reinit(lookup.path, lookup.index, 28);
    const large_cell = face.cellWidth();

    try std.testing.expect(large_cell > small_cell);
}