mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
175 lines
5.3 KiB
Zig
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);
|
|
}
|
|
}
|