ghostty/src/font/Family.zig
2022-08-19 09:56:01 -07:00

175 lines
5.3 KiB
Zig

//! Family represents a multiple styles of a single font: regular, bold,
//! italic, etc. It is able to cache the glyphs into a single atlas.
const Family = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const Atlas = @import("../Atlas.zig");
const ftc = @import("freetype").c;
const ftok = ftc.FT_Err_Ok;
const Face = @import("main.zig").Face;
const Glyph = @import("main.zig").Glyph;
const Style = @import("main.zig").Style;
const testFont = @import("test.zig").fontRegular;
const log = std.log.scoped(.font_family);
// NOTE(mitchellh): I think eventually atlas and the freetype lib fields
// move even higher up into another struct that manages sets of font families
// in order to support fallback and so on.
/// The texture atlas where all the font glyphs are rendered.
/// This is NOT owned by the Family, deinitialization must
/// be manually done.
atlas: Atlas,
/// The FreeType library, initialized by this init func.
ft_library: ftc.FT_Library,
/// The glyphs that are loaded into the atlas, keyed by codepoint.
glyphs: std.AutoHashMapUnmanaged(GlyphKey, Glyph) = .{},
/// The font faces representing all the styles in this family.
/// These should be set directly or via various loader functions.
regular: ?Face = null,
bold: ?Face = null,
/// This struct is used for the hash key for glyphs.
const GlyphKey = struct {
style: Style,
codepoint: u32,
};
pub fn init(atlas: Atlas) !Family {
var res = Family{
.atlas = atlas,
.ft_library = undefined,
};
if (ftc.FT_Init_FreeType(&res.ft_library) != ftok)
return error.FreeTypeInitFailed;
return res;
}
pub fn deinit(self: *Family, alloc: Allocator) void {
self.glyphs.deinit(alloc);
if (self.regular) |*face| face.deinit();
if (self.bold) |*face| face.deinit();
if (ftc.FT_Done_FreeType(self.ft_library) != ftok)
log.err("failed to clean up FreeType", .{});
self.* = undefined;
}
/// Loads a font to use from memory.
///
/// This can only be called if a font is not already loaded for the given style.
pub fn loadFaceFromMemory(
self: *Family,
comptime style: Style,
source: [:0]const u8,
size: u32,
) !void {
var face = try Face.init(self.ft_library);
errdefer face.deinit();
try face.loadFaceFromMemory(source, size);
@field(self, switch (style) {
.regular => "regular",
.bold => "bold",
.italic => unreachable,
.bold_italic => unreachable,
}) = face;
}
/// Get the glyph for the given codepoint and style. If the glyph hasn't
/// been loaded yet this will return null.
pub fn getGlyph(self: Family, cp: anytype, style: Style) ?*Glyph {
const utf32 = codepoint(cp);
const entry = self.glyphs.getEntry(.{
.style = style,
.codepoint = utf32,
}) orelse return null;
return entry.value_ptr;
}
/// Add a glyph. If the glyph has already been loaded this will return
/// the existing loaded glyph. If a glyph style can't be found, this will
/// fall back to the "regular" style. If a glyph can't be found in the
/// "regular" style, this will fall back to the unknown glyph character.
///
/// The codepoint can be either a u8 or []const u8 depending on if you know
/// it is ASCII or must be UTF-8 decoded.
pub fn addGlyph(self: *Family, alloc: Allocator, v: anytype, style: Style) !*Glyph {
const face = face: {
// Real is the face we SHOULD use for this style.
var real = switch (style) {
.regular => self.regular,
.bold => self.bold,
.italic => unreachable,
.bold_italic => unreachable,
};
// Fall back to regular if it is null
if (real == null) real = self.regular;
// Return our face if we have it.
if (real) |ptr| break :face ptr;
// If we reached this point, we have no font in the style we
// want OR the fallback.
return error.NoFontFallback;
};
// We need a UTF32 codepoint
const utf32 = codepoint(v);
// If we have this glyph loaded already then we're done.
const glyphKey = .{
.style = style,
.codepoint = utf32,
};
const gop = try self.glyphs.getOrPut(alloc, glyphKey);
if (gop.found_existing) return gop.value_ptr;
errdefer _ = self.glyphs.remove(glyphKey);
// Get the glyph and add it to the atlas.
// TODO: handle glyph not found
gop.value_ptr.* = try face.loadGlyph(alloc, &self.atlas, utf32);
return gop.value_ptr;
}
/// Returns the UTF-32 codepoint for the given value.
fn codepoint(v: anytype) u32 {
// We need a UTF32 codepoint for freetype
return switch (@TypeOf(v)) {
u32 => v,
comptime_int, u8 => @intCast(u32, v),
[]const u8 => @intCast(u32, try std.unicode.utfDecode(v)),
else => @compileError("invalid codepoint type"),
};
}
test {
const testing = std.testing;
const alloc = testing.allocator;
var fam = try init(try Atlas.init(alloc, 512));
defer fam.deinit(alloc);
defer fam.atlas.deinit(alloc);
try fam.loadFaceFromMemory(.regular, testFont, 48);
// Generate all visible ASCII
var i: u8 = 32;
while (i < 127) : (i += 1) {
_ = try fam.addGlyph(alloc, i, .regular);
}
i = 32;
while (i < 127) : (i += 1) {
try testing.expect(fam.getGlyph(i, .regular) != null);
}
}