563e01c2f2
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>
191 lines
6.5 KiB
Zig
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;
|
|
}
|