1
0
Fork 0
zenolith-sdl2/src/Sdl2Font.zig
mcmrcs@proton.me 563e01c2f2 feat: implemented create and update texture (#2)
Reviewed-on: https://git.mzte.de/zenolith/zenolith-sdl2/pulls/2
Co-authored-by: mcmrcs@proton.me <marciusdasilva51@gmail.com>
Co-committed-by: mcmrcs@proton.me <marciusdasilva51@gmail.com>
2024-03-18 23:16:48 +01:00

191 lines
6.5 KiB
Zig

//! A font backed by a texture atlas (SDL2 Texture) using a custom lazy bin-packing algorithm
//! and freetype. Create this using Sdl2Platform.createFont.
const std = @import("std");
const zenolith = @import("zenolith");
const util = @import("util.zig");
const ffi = @import("ffi.zig");
const c = ffi.c;
const Sdl2Texture = @import("Sdl2Texture.zig");
face: c.FT_Face,
atlas: Sdl2Texture,
renderer: *c.SDL_Renderer,
/// This is an ArrayHashMap to speed up iteration which is requried for collision checking.
glyphs: std.AutoArrayHashMap(GlyphProperties, AtlasGlyph),
/// A buffer for creating glyph pixel data in for SDL2 textures.
pixel_buf: std.ArrayList(u8),
const Sdl2Font = @This();
pub const AtlasGlyph = struct {
glyph: zenolith.text.Glyph,
sprite: zenolith.layout.Rectangle,
};
pub const GlyphProperties = struct {
codepoint: u21,
size: u31,
};
pub fn deinit(self: *Sdl2Font) void {
self.atlas.deinit();
_ = c.FT_Done_Face(self.face);
self.glyphs.deinit();
self.pixel_buf.deinit();
}
pub fn getGlyph(self: *Sdl2Font, codepoint: u21, style: zenolith.text.Style) !zenolith.text.Glyph {
const props = GlyphProperties{ .codepoint = codepoint, .size = style.size };
if (self.glyphs.get(props)) |g| return g.glyph;
try ffi.handleFTError(c.FT_Set_Pixel_Sizes(self.face, 0, @intCast(style.size)));
try ffi.handleFTError(c.FT_Load_Char(self.face, codepoint, c.FT_LOAD_RENDER));
const bmp = self.face.*.glyph.*.bitmap;
const rect: zenolith.layout.Rectangle = if (bmp.rows * bmp.width == 0) .{
.pos = .{ .x = 0, .y = 0 },
.size = .{ .width = 0, .height = 0 },
} else try self.addAtlastSprite(bmp.buffer[0 .. bmp.rows * bmp.width], @intCast(bmp.width));
const glyph = zenolith.text.Glyph{
.codepoint = codepoint,
.size = rect.size,
.bearing = .{
.x = self.face.*.glyph.*.bitmap_left,
.y = -self.face.*.glyph.*.bitmap_top,
},
// I see no point in supporting negative glyph advance.
.advance = @intCast(@max(0, self.face.*.glyph.*.advance.x) >> 6),
};
try self.glyphs.put(props, .{ .glyph = glyph, .sprite = rect });
return glyph;
}
pub fn heightMetrics(self: *Sdl2Font, size: u31) zenolith.text.HeightMetrics {
if (c.FT_Set_Pixel_Sizes(self.face, 0, @intCast(size)) != 0)
// TODO: WONK
@panic("Unable to FT_Set_Pixel_Sizes for determining height metrics");
// All these produce equally nonsensical results and there seems to be no consensus which one is actually correct:
//return @intCast(self.face.*.size.*.metrics.height >> 6);
//return @intCast((self.face.*.size.*.metrics.ascender - self.face.*.size.*.metrics.descender) >> 6);
//return @intCast((c.FT_MulFix(self.face.*.bbox.yMax, self.face.*.size.*.metrics.y_scale) >> 6) -
// (c.FT_MulFix(self.face.*.bbox.yMin, self.face.*.size.*.metrics.y_scale) >> 6));
const bpad: u31 = @intCast(@max(0, -(self.face.*.size.*.metrics.descender >> 6)));
return .{
.y_offset = size,
.bottom_padding = bpad,
};
}
fn getSize(self: *Sdl2Font) zenolith.layout.Size {
var w: c_int = 0;
var h: c_int = 0;
// This will only error if I messed up somewhere.
if (c.SDL_QueryTexture(self.atlas.tex, null, null, &w, &h) != 0) unreachable;
return .{ .width = @intCast(w), .height = @intCast(h) };
}
pub fn addAtlastSprite(self: *Sdl2Font, data: []const u8, width: u31) !zenolith.layout.Rectangle {
std.debug.assert(data.len % width == 0);
// Amount of pixels to leave empty between glyphs to avoid scaling artifacts.
const padding = 2;
const size = self.getSize();
var collision = zenolith.layout.Rectangle{
// start in bottom right
.pos = .{
.x = @intCast(size.width - width),
.y = @intCast(size.height),
},
// size of glyph + padding
.size = .{
.width = width + padding,
.height = @intCast(@divExact(data.len, width) + padding),
},
};
// This is the state of the state machine. We continually keep moving up/left
// until we hit a wall that prevents us from moving.
//
// This is probably not the fastest bin packing algorithm, but it doesn't require knowing
// all glyphs before adding them.
var state: enum { up, left } = .up;
while (true) switch (state) {
.up => {
if (self.isTouchingAnyOnTop(collision)) {
state = .left;
} else {
collision.pos.y -= 1;
}
},
.left => {
if (!self.isTouchingAnyOnTop(collision)) {
state = .up;
} else if (!self.isTouchingAnyOnLeft(collision)) {
collision.pos.x -= 1;
} else {
break;
}
},
};
if (@as(u31, @intCast(collision.pos.y)) + collision.size.height - padding > size.height)
return error.AtlastTooSmall;
try self.pixel_buf.resize(data.len * 4);
for (data, 0..) |pix, i| {
@memset(self.pixel_buf.items[i * 4 .. i * 4 + 4], pix);
}
const rect = zenolith.layout.Rectangle{
.pos = collision.pos,
.size = .{
.width = width,
.height = @intCast(@divExact(data.len, width)),
},
};
try self.atlas.setPixels(self.pixel_buf.items, @intCast(width * 4), rect);
return rect;
}
pub fn getSprite(self: *Sdl2Font, codepoint: u21, size: u31) ?zenolith.layout.Rectangle {
return (self.glyphs.get(.{ .codepoint = codepoint, .size = size }) orelse return null).sprite;
}
fn isTouchingAnyOnTop(self: *Sdl2Font, rect: zenolith.layout.Rectangle) bool {
if (rect.pos.y == 0) return true;
for (self.glyphs.unmanaged.entries.items(.value)) |other| {
if (other.sprite.pos.y + @as(isize, @intCast(other.sprite.size.height)) == rect.pos.y and
other.sprite.pos.x < rect.pos.x + @as(isize, @intCast(rect.size.width)) and
other.sprite.pos.x + @as(isize, @intCast(other.sprite.size.width)) > rect.pos.x) return true;
}
return false;
}
fn isTouchingAnyOnLeft(self: *Sdl2Font, rect: zenolith.layout.Rectangle) bool {
if (rect.pos.x == 0) return true;
for (self.glyphs.unmanaged.entries.items(.value)) |other| {
if (other.sprite.pos.x + @as(isize, @intCast(other.sprite.size.width)) == rect.pos.x and
other.sprite.pos.y < rect.pos.y + @as(isize, @intCast(rect.size.height)) and
other.sprite.pos.y + @as(isize, @intCast(other.sprite.size.height)) > rect.pos.y) return true;
}
return false;
}