a73x

src/gui/atlas.zig

Ref:   Size: 3.4 KiB   History

//! Grow-only shelf-packed R8 glyph texture. Coordinates never move.
const std = @import("std");

pub const Variant = enum(u2) { regular, bold, italic, bold_italic };
pub const Key = struct { face_id: u32, variant: Variant, glyph_id: u32 };
pub const Entry = struct { x: u16, y: u16, w: u16, h: u16, left: i16, top: i16 };

pub const Atlas = struct {
    width: u16,
    height: u16,
    pixels: []u8,
    dirty: bool = false,
    entries: std.AutoHashMapUnmanaged(Key, Entry) = .empty,
    shelf_x: u16 = 0,
    shelf_y: u16 = 0,
    shelf_h: u16 = 0,
    pub fn init(alloc: std.mem.Allocator, width: u16, height: u16) !Atlas {
        if (width == 0 or height == 0) return error.AtlasFull;
        const pixels = try alloc.alloc(u8, @as(usize, width) * height);
        @memset(pixels, 0);
        return .{ .width = width, .height = height, .pixels = pixels };
    }
    pub fn deinit(self: *Atlas, alloc: std.mem.Allocator) void {
        alloc.free(self.pixels);
        self.entries.deinit(alloc);
    }
    pub fn get(self: *const Atlas, key: Key) ?Entry {
        return self.entries.get(key);
    }
    pub fn put(self: *Atlas, alloc: std.mem.Allocator, key: Key, w: u16, h: u16, left: i16, top: i16, bitmap: []const u8) !Entry {
        if (self.get(key)) |entry| return entry;
        if (w > self.width) return error.GlyphTooWide;
        if (bitmap.len < @as(usize, w) * h) return error.BadBitmap;
        if (@as(u32, self.shelf_x) + w > self.width) {
            self.shelf_y = std.math.add(u16, self.shelf_y, self.shelf_h) catch return error.AtlasFull;
            self.shelf_x = 0;
            self.shelf_h = 0;
        }
        while (@as(u32, self.shelf_y) + @max(self.shelf_h, h) > self.height) try self.grow(alloc);
        const e: Entry = .{ .x = self.shelf_x, .y = self.shelf_y, .w = w, .h = h, .left = left, .top = top };
        for (0..h) |row| {
            const dst = (@as(usize, e.y) + row) * self.width + e.x;
            @memcpy(self.pixels[dst .. dst + w], bitmap[row * w .. row * w + w]);
        }
        self.shelf_x = std.math.add(u16, self.shelf_x, w) catch return error.AtlasFull;
        self.shelf_h = @max(self.shelf_h, h);
        self.dirty = true;
        try self.entries.put(alloc, key, e);
        return e;
    }
    fn grow(self: *Atlas, alloc: std.mem.Allocator) !void {
        const h = std.math.mul(u16, self.height, 2) catch return error.AtlasFull;
        const p = try alloc.alloc(u8, @as(usize, self.width) * h);
        @memset(p, 0);
        @memcpy(p[0..self.pixels.len], self.pixels);
        alloc.free(self.pixels);
        self.pixels = p;
        self.height = h;
        self.dirty = true;
    }
};

test "face and variant key separation preserve coordinates through growth" {
    const a = std.testing.allocator;
    var at = try Atlas.init(a, 4, 2);
    defer at.deinit(a);
    const px = [_]u8{ 1, 2, 3, 4 };
    const r = try at.put(a, .{ .face_id = 0, .variant = .regular, .glyph_id = 7 }, 2, 2, 0, 0, &px);
    const fallback = try at.put(a, .{ .face_id = 1, .variant = .regular, .glyph_id = 7 }, 2, 2, 0, 0, &px);
    _ = try at.put(a, .{ .face_id = 0, .variant = .bold, .glyph_id = 7 }, 2, 2, 0, 0, &px);
    try std.testing.expect(at.height > 2);
    try std.testing.expect(r.x != fallback.x or r.y != fallback.y);
    try std.testing.expectEqual(r, at.get(.{ .face_id = 0, .variant = .regular, .glyph_id = 7 }).?);
    try std.testing.expectEqual(fallback, at.get(.{ .face_id = 1, .variant = .regular, .glyph_id = 7 }).?);
}