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