mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
move towards font family management to prep for bold/italic
This commit is contained in:
@ -1,231 +0,0 @@
|
||||
//! Implements font loading and rendering into a texture atlas, using
|
||||
//! Atlas as the backing implementation. The FontAtlas represents a single
|
||||
//! face with a single size.
|
||||
const FontAtlas = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ftc = @import("freetype/c.zig");
|
||||
const Atlas = @import("Atlas.zig");
|
||||
|
||||
const ftok = ftc.FT_Err_Ok;
|
||||
const log = std.log.scoped(.font_atlas);
|
||||
|
||||
/// The texture atlas where all the font glyphs are rendered.
|
||||
/// This is NOT owned by the FontAtlas, deinitialization must
|
||||
/// be manually done.
|
||||
atlas: Atlas,
|
||||
|
||||
/// The glyphs that are loaded into the atlas, keyed by codepoint.
|
||||
glyphs: std.AutoHashMapUnmanaged(u32, Glyph),
|
||||
|
||||
/// The FreeType library
|
||||
ft_library: ftc.FT_Library,
|
||||
|
||||
/// Our font face.
|
||||
ft_face: ftc.FT_Face = null,
|
||||
|
||||
/// Information about a single glyph.
|
||||
pub const Glyph = struct {
|
||||
/// width of glyph in pixels
|
||||
width: u32,
|
||||
|
||||
/// height of glyph in pixels
|
||||
height: u32,
|
||||
|
||||
/// left bearing
|
||||
offset_x: i32,
|
||||
|
||||
/// top bearing
|
||||
offset_y: i32,
|
||||
|
||||
/// coordinates in the atlas of the top-left corner. These have to
|
||||
/// be normalized to be between 0 and 1 prior to use in shaders.
|
||||
atlas_x: u32,
|
||||
atlas_y: u32,
|
||||
|
||||
/// horizontal position to increase drawing position for strings
|
||||
advance_x: f32,
|
||||
};
|
||||
|
||||
pub fn init(atlas: Atlas) !FontAtlas {
|
||||
var res = FontAtlas{
|
||||
.atlas = atlas,
|
||||
.ft_library = undefined,
|
||||
.glyphs = .{},
|
||||
};
|
||||
|
||||
if (ftc.FT_Init_FreeType(&res.ft_library) != ftok)
|
||||
return error.FreeTypeInitFailed;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *FontAtlas, alloc: Allocator) void {
|
||||
self.glyphs.deinit(alloc);
|
||||
|
||||
if (self.ft_face != null) {
|
||||
if (ftc.FT_Done_Face(self.ft_face) != ftok)
|
||||
log.err("failed to clean up font face", .{});
|
||||
}
|
||||
|
||||
if (ftc.FT_Done_FreeType(self.ft_library) != ftok)
|
||||
log.err("failed to clean up FreeType", .{});
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Loads a font to use for the atlas.
|
||||
///
|
||||
/// This can only be called if a font is not already loaded.
|
||||
pub fn loadFaceFromMemory(self: *FontAtlas, source: [:0]const u8, size: u32) !void {
|
||||
assert(self.ft_face == null);
|
||||
|
||||
if (ftc.FT_New_Memory_Face(
|
||||
self.ft_library,
|
||||
source.ptr,
|
||||
@intCast(c_long, source.len),
|
||||
0,
|
||||
&self.ft_face,
|
||||
) != ftok) return error.FaceLoadFailed;
|
||||
errdefer {
|
||||
_ = ftc.FT_Done_Face(self.ft_face);
|
||||
self.ft_face = null;
|
||||
}
|
||||
|
||||
if (ftc.FT_Select_Charmap(self.ft_face, ftc.FT_ENCODING_UNICODE) != ftok)
|
||||
return error.FaceLoadFailed;
|
||||
|
||||
if (ftc.FT_Set_Pixel_Sizes(self.ft_face, size, size) != ftok)
|
||||
return error.FaceLoadFailed;
|
||||
}
|
||||
|
||||
/// Get the glyph for the given codepoint.
|
||||
pub fn getGlyph(self: FontAtlas, v: anytype) ?*Glyph {
|
||||
const utf32 = codepoint(v);
|
||||
const entry = self.glyphs.getEntry(utf32) orelse return null;
|
||||
return entry.value_ptr;
|
||||
}
|
||||
|
||||
/// Add a glyph to the font atlas. 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: *FontAtlas, alloc: Allocator, v: anytype) !*Glyph {
|
||||
assert(self.ft_face != null);
|
||||
|
||||
// We need a UTF32 codepoint for freetype
|
||||
const utf32 = codepoint(v);
|
||||
|
||||
// If we have this glyph loaded already then we're done.
|
||||
const gop = try self.glyphs.getOrPut(alloc, utf32);
|
||||
if (gop.found_existing) return gop.value_ptr;
|
||||
errdefer _ = self.glyphs.remove(utf32);
|
||||
|
||||
const glyph_index = glyph_index: {
|
||||
// log.warn("glyph load: {x}", .{utf32});
|
||||
const idx = ftc.FT_Get_Char_Index(self.ft_face, utf32);
|
||||
if (idx > 0) break :glyph_index idx;
|
||||
|
||||
// Unknown glyph.
|
||||
log.warn("glyph not found: {x}", .{utf32});
|
||||
|
||||
// TODO: render something more identifiable than a space
|
||||
break :glyph_index ftc.FT_Get_Char_Index(self.ft_face, ' ');
|
||||
};
|
||||
|
||||
if (ftc.FT_Load_Glyph(
|
||||
self.ft_face,
|
||||
glyph_index,
|
||||
ftc.FT_LOAD_RENDER,
|
||||
) != ftok) return error.LoadGlyphFailed;
|
||||
|
||||
const glyph = self.ft_face.*.glyph;
|
||||
const bitmap = glyph.*.bitmap;
|
||||
|
||||
const src_w = bitmap.width;
|
||||
const src_h = bitmap.rows;
|
||||
const tgt_w = src_w;
|
||||
const tgt_h = src_h;
|
||||
|
||||
const region = try self.atlas.reserve(alloc, tgt_w, tgt_h);
|
||||
|
||||
// Build our buffer
|
||||
//
|
||||
// TODO(perf): we can avoid a buffer copy here in some cases where
|
||||
// tgt_w == bitmap.width and bitmap.width == bitmap.pitch
|
||||
const buffer = try alloc.alloc(u8, tgt_w * tgt_h);
|
||||
defer alloc.free(buffer);
|
||||
var dst_ptr = buffer;
|
||||
var src_ptr = bitmap.buffer;
|
||||
var i: usize = 0;
|
||||
while (i < src_h) : (i += 1) {
|
||||
std.mem.copy(u8, dst_ptr, src_ptr[0..bitmap.width]);
|
||||
dst_ptr = dst_ptr[tgt_w..];
|
||||
src_ptr += @intCast(usize, bitmap.pitch);
|
||||
}
|
||||
|
||||
// Write the glyph information into the atlas
|
||||
assert(region.width == tgt_w);
|
||||
assert(region.height == tgt_h);
|
||||
self.atlas.set(region, buffer);
|
||||
|
||||
// Store glyph metadata
|
||||
gop.value_ptr.* = .{
|
||||
.width = tgt_w,
|
||||
.height = tgt_h,
|
||||
.offset_x = glyph.*.bitmap_left,
|
||||
.offset_y = glyph.*.bitmap_top,
|
||||
.atlas_x = region.x,
|
||||
.atlas_y = region.y,
|
||||
.advance_x = f26dot6ToFloat(glyph.*.advance.x),
|
||||
};
|
||||
|
||||
//log.debug("loaded glyph codepoint=U+{x} glyph={}", .{ utf32, gop.value_ptr.* });
|
||||
|
||||
return gop.value_ptr;
|
||||
}
|
||||
|
||||
/// Convert 16.6 pixel format to pixels based on the scale factor of the
|
||||
/// current font size.
|
||||
pub fn unitsToPxY(self: FontAtlas, units: i32) i32 {
|
||||
return @intCast(i32, ftc.FT_MulFix(units, self.ft_face.*.size.*.metrics.y_scale) >> 6);
|
||||
}
|
||||
|
||||
/// Convert 26.6 pixel format to f32
|
||||
fn f26dot6ToFloat(v: ftc.FT_F26Dot6) f32 {
|
||||
return @intToFloat(f32, v >> 6);
|
||||
}
|
||||
|
||||
/// 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 alloc = testing.allocator;
|
||||
var font = try init(try Atlas.init(alloc, 512));
|
||||
defer font.deinit(alloc);
|
||||
defer font.atlas.deinit(alloc);
|
||||
|
||||
try font.loadFaceFromMemory(testFont, 48);
|
||||
|
||||
// Generate all visible ASCII
|
||||
var i: u8 = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
_ = try font.addGlyph(alloc, i);
|
||||
}
|
||||
|
||||
i = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
try testing.expect(font.getGlyph(i) != null);
|
||||
}
|
||||
}
|
||||
|
||||
const testFont = @embedFile("../fonts/Inconsolata-Regular.ttf");
|
30
src/Grid.zig
30
src/Grid.zig
@ -6,7 +6,7 @@ const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Atlas = @import("Atlas.zig");
|
||||
const FontAtlas = @import("FontAtlas.zig");
|
||||
const font = @import("font/font.zig");
|
||||
const terminal = @import("terminal/main.zig");
|
||||
const Terminal = terminal.Terminal;
|
||||
const gl = @import("opengl.zig");
|
||||
@ -34,7 +34,7 @@ vbo: gl.Buffer,
|
||||
texture: gl.Texture,
|
||||
|
||||
/// The font atlas.
|
||||
font_atlas: FontAtlas,
|
||||
font_atlas: font.Family,
|
||||
atlas_dirty: bool,
|
||||
|
||||
/// Whether the cursor is visible or not. This is used to control cursor
|
||||
@ -103,9 +103,9 @@ pub fn init(alloc: Allocator) !Grid {
|
||||
// font atlas with all the visible ASCII characters since they are common.
|
||||
var atlas = try Atlas.init(alloc, 512);
|
||||
errdefer atlas.deinit(alloc);
|
||||
var font = try FontAtlas.init(atlas);
|
||||
errdefer font.deinit(alloc);
|
||||
try font.loadFaceFromMemory(face_ttf, 32);
|
||||
var fam = try font.Family.init(atlas);
|
||||
errdefer fam.deinit(alloc);
|
||||
try fam.loadFaceFromMemory(.regular, face_ttf, 32);
|
||||
|
||||
// Load all visible ASCII characters and build our cell width based on
|
||||
// the widest character that we see.
|
||||
@ -113,7 +113,7 @@ pub fn init(alloc: Allocator) !Grid {
|
||||
var cell_width: f32 = 0;
|
||||
var i: u8 = 32;
|
||||
while (i <= 126) : (i += 1) {
|
||||
const glyph = try font.addGlyph(alloc, i);
|
||||
const glyph = try fam.addGlyph(alloc, i, .regular);
|
||||
if (glyph.advance_x > cell_width) {
|
||||
cell_width = @ceil(glyph.advance_x);
|
||||
}
|
||||
@ -126,12 +126,11 @@ pub fn init(alloc: Allocator) !Grid {
|
||||
// '_' which should live at the bottom of a cell.
|
||||
const cell_height: f32 = cell_height: {
|
||||
// This is the height reported by the font face
|
||||
const face_height: i32 = font.unitsToPxY(font.ft_face.*.height);
|
||||
const face_height: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.height);
|
||||
|
||||
// Determine the height of the underscore char
|
||||
assert(font.ft_face != null);
|
||||
const glyph = font.getGlyph('_').?;
|
||||
var res: i32 = font.unitsToPxY(font.ft_face.*.ascender);
|
||||
const glyph = fam.getGlyph('_', .regular).?;
|
||||
var res: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender);
|
||||
res -= glyph.offset_y;
|
||||
res += @intCast(i32, glyph.height);
|
||||
|
||||
@ -141,7 +140,10 @@ pub fn init(alloc: Allocator) !Grid {
|
||||
|
||||
break :cell_height @intToFloat(f32, res);
|
||||
};
|
||||
const cell_baseline = cell_height - @intToFloat(f32, font.unitsToPxY(font.ft_face.*.ascender));
|
||||
const cell_baseline = cell_height - @intToFloat(
|
||||
f32,
|
||||
fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender),
|
||||
);
|
||||
log.debug("cell dimensions w={d} h={d} baseline={d}", .{ cell_width, cell_height, cell_baseline });
|
||||
|
||||
// Create our shader
|
||||
@ -235,7 +237,7 @@ pub fn init(alloc: Allocator) !Grid {
|
||||
.ebo = ebo,
|
||||
.vbo = vbo,
|
||||
.texture = tex,
|
||||
.font_atlas = font,
|
||||
.font_atlas = fam,
|
||||
.atlas_dirty = false,
|
||||
.cursor_visible = true,
|
||||
.cursor_style = .box,
|
||||
@ -310,11 +312,11 @@ pub fn updateCells(self: *Grid, term: Terminal) !void {
|
||||
|
||||
// Get our glyph
|
||||
// TODO: if we add a glyph, I think we need to rerender the texture.
|
||||
const glyph = if (self.font_atlas.getGlyph(cell.char)) |glyph|
|
||||
const glyph = if (self.font_atlas.getGlyph(cell.char, .regular)) |glyph|
|
||||
glyph
|
||||
else glyph: {
|
||||
self.atlas_dirty = true;
|
||||
break :glyph try self.font_atlas.addGlyph(self.alloc, cell.char);
|
||||
break :glyph try self.font_atlas.addGlyph(self.alloc, cell.char, .regular);
|
||||
};
|
||||
|
||||
const fg = cell.fg orelse self.foreground;
|
||||
|
164
src/font/Face.zig
Normal file
164
src/font/Face.zig
Normal file
@ -0,0 +1,164 @@
|
||||
//! Face represents a single font face. A single font face has a single set
|
||||
//! of properties associated with it such as style, weight, etc.
|
||||
//!
|
||||
//! A Face isn't typically meant to be used directly. It is usually used
|
||||
//! via a Family in order to store it in an Atlas.
|
||||
const Face = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ftc = @import("../freetype/c.zig");
|
||||
const Atlas = @import("../Atlas.zig");
|
||||
const Glyph = @import("font.zig").Glyph;
|
||||
|
||||
const ftok = ftc.FT_Err_Ok;
|
||||
const log = std.log.scoped(.font_face);
|
||||
|
||||
/// The FreeType library
|
||||
ft_library: ftc.FT_Library,
|
||||
|
||||
/// Our font face.
|
||||
ft_face: ftc.FT_Face = null,
|
||||
|
||||
pub fn init(lib: ftc.FT_Library) !Face {
|
||||
return Face{
|
||||
.ft_library = lib,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Face) void {
|
||||
if (self.ft_face != null) {
|
||||
if (ftc.FT_Done_Face(self.ft_face) != ftok)
|
||||
log.err("failed to clean up font face", .{});
|
||||
}
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Loads a font to use.
|
||||
///
|
||||
/// This can only be called if a font is not already loaded.
|
||||
pub fn loadFaceFromMemory(self: *Face, source: [:0]const u8, size: u32) !void {
|
||||
assert(self.ft_face == null);
|
||||
|
||||
if (ftc.FT_New_Memory_Face(
|
||||
self.ft_library,
|
||||
source.ptr,
|
||||
@intCast(c_long, source.len),
|
||||
0,
|
||||
&self.ft_face,
|
||||
) != ftok) return error.FaceLoadFailed;
|
||||
errdefer {
|
||||
_ = ftc.FT_Done_Face(self.ft_face);
|
||||
self.ft_face = null;
|
||||
}
|
||||
|
||||
if (ftc.FT_Select_Charmap(self.ft_face, ftc.FT_ENCODING_UNICODE) != ftok)
|
||||
return error.FaceLoadFailed;
|
||||
|
||||
if (ftc.FT_Set_Pixel_Sizes(self.ft_face, size, size) != ftok)
|
||||
return error.FaceLoadFailed;
|
||||
}
|
||||
|
||||
/// Load a glyph for this face. 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 loadGlyph(self: Face, alloc: Allocator, atlas: *Atlas, cp: u32) !Glyph {
|
||||
assert(self.ft_face != null);
|
||||
|
||||
// We need a UTF32 codepoint for freetype
|
||||
const glyph_index = glyph_index: {
|
||||
// log.warn("glyph load: {x}", .{cp});
|
||||
const idx = ftc.FT_Get_Char_Index(self.ft_face, cp);
|
||||
if (idx > 0) break :glyph_index idx;
|
||||
|
||||
// Unknown glyph.
|
||||
log.warn("glyph not found: {x}", .{cp});
|
||||
|
||||
// TODO: render something more identifiable than a space
|
||||
break :glyph_index ftc.FT_Get_Char_Index(self.ft_face, ' ');
|
||||
};
|
||||
|
||||
if (ftc.FT_Load_Glyph(
|
||||
self.ft_face,
|
||||
glyph_index,
|
||||
ftc.FT_LOAD_RENDER,
|
||||
) != ftok) return error.LoadGlyphFailed;
|
||||
|
||||
const glyph = self.ft_face.*.glyph;
|
||||
const bitmap = glyph.*.bitmap;
|
||||
|
||||
const src_w = bitmap.width;
|
||||
const src_h = bitmap.rows;
|
||||
const tgt_w = src_w;
|
||||
const tgt_h = src_h;
|
||||
|
||||
const region = try atlas.reserve(alloc, tgt_w, tgt_h);
|
||||
|
||||
// Build our buffer
|
||||
//
|
||||
// TODO(perf): we can avoid a buffer copy here in some cases where
|
||||
// tgt_w == bitmap.width and bitmap.width == bitmap.pitch
|
||||
const buffer = try alloc.alloc(u8, tgt_w * tgt_h);
|
||||
defer alloc.free(buffer);
|
||||
var dst_ptr = buffer;
|
||||
var src_ptr = bitmap.buffer;
|
||||
var i: usize = 0;
|
||||
while (i < src_h) : (i += 1) {
|
||||
std.mem.copy(u8, dst_ptr, src_ptr[0..bitmap.width]);
|
||||
dst_ptr = dst_ptr[tgt_w..];
|
||||
src_ptr += @intCast(usize, bitmap.pitch);
|
||||
}
|
||||
|
||||
// Write the glyph information into the atlas
|
||||
assert(region.width == tgt_w);
|
||||
assert(region.height == tgt_h);
|
||||
atlas.set(region, buffer);
|
||||
|
||||
// Store glyph metadata
|
||||
return Glyph{
|
||||
.width = tgt_w,
|
||||
.height = tgt_h,
|
||||
.offset_x = glyph.*.bitmap_left,
|
||||
.offset_y = glyph.*.bitmap_top,
|
||||
.atlas_x = region.x,
|
||||
.atlas_y = region.y,
|
||||
.advance_x = f26dot6ToFloat(glyph.*.advance.x),
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert 16.6 pixel format to pixels based on the scale factor of the
|
||||
/// current font size.
|
||||
pub fn unitsToPxY(self: Face, units: i32) i32 {
|
||||
return @intCast(i32, ftc.FT_MulFix(units, self.ft_face.*.size.*.metrics.y_scale) >> 6);
|
||||
}
|
||||
|
||||
/// Convert 26.6 pixel format to f32
|
||||
fn f26dot6ToFloat(v: ftc.FT_F26Dot6) f32 {
|
||||
return @intToFloat(f32, v >> 6);
|
||||
}
|
||||
|
||||
test {
|
||||
var ft_lib: ftc.FT_Library = undefined;
|
||||
if (ftc.FT_Init_FreeType(&ft_lib) != ftok)
|
||||
return error.FreeTypeInitFailed;
|
||||
defer _ = ftc.FT_Done_FreeType(ft_lib);
|
||||
|
||||
const alloc = testing.allocator;
|
||||
var atlas = try Atlas.init(alloc, 512);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var font = try init(ft_lib);
|
||||
defer font.deinit();
|
||||
|
||||
try font.loadFaceFromMemory(testFont, 48);
|
||||
|
||||
// Generate all visible ASCII
|
||||
var i: u8 = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
_ = try font.loadGlyph(alloc, &atlas, i);
|
||||
}
|
||||
}
|
||||
|
||||
const testFont = @embedFile("../../fonts/Inconsolata-Regular.ttf");
|
172
src/font/Family.zig
Normal file
172
src/font/Family.zig
Normal file
@ -0,0 +1,172 @@
|
||||
//! 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.zig");
|
||||
const ftok = ftc.FT_Err_Ok;
|
||||
const Face = @import("font.zig").Face;
|
||||
const Glyph = @import("font.zig").Glyph;
|
||||
const Style = @import("font.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,
|
||||
|
||||
/// 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 (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 => unreachable,
|
||||
.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 => unreachable,
|
||||
.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);
|
||||
}
|
||||
}
|
22
src/font/Glyph.zig
Normal file
22
src/font/Glyph.zig
Normal file
@ -0,0 +1,22 @@
|
||||
//! Glyph is a single loaded glyph for a face.
|
||||
const Glyph = @This();
|
||||
|
||||
/// width of glyph in pixels
|
||||
width: u32,
|
||||
|
||||
/// height of glyph in pixels
|
||||
height: u32,
|
||||
|
||||
/// left bearing
|
||||
offset_x: i32,
|
||||
|
||||
/// top bearing
|
||||
offset_y: i32,
|
||||
|
||||
/// coordinates in the atlas of the top-left corner. These have to
|
||||
/// be normalized to be between 0 and 1 prior to use in shaders.
|
||||
atlas_x: u32,
|
||||
atlas_y: u32,
|
||||
|
||||
/// horizontal position to increase drawing position for strings
|
||||
advance_x: f32,
|
20
src/font/font.zig
Normal file
20
src/font/font.zig
Normal file
@ -0,0 +1,20 @@
|
||||
pub const Face = @import("Face.zig");
|
||||
pub const Family = @import("Family.zig");
|
||||
pub const Glyph = @import("Glyph.zig");
|
||||
|
||||
/// Embedded fonts (for now)
|
||||
pub const fontRegular = @import("test.zig").fontRegular;
|
||||
|
||||
/// The styles that a family can take.
|
||||
pub const Style = enum {
|
||||
regular,
|
||||
bold,
|
||||
italic,
|
||||
bold_italic,
|
||||
};
|
||||
|
||||
test {
|
||||
_ = Face;
|
||||
_ = Family;
|
||||
_ = Glyph;
|
||||
}
|
1
src/font/test.zig
Normal file
1
src/font/test.zig
Normal file
@ -0,0 +1 @@
|
||||
pub const fontRegular = @embedFile("../../fonts/Inconsolata-Regular.ttf");
|
@ -59,11 +59,11 @@ pub fn tracy_enabled() bool {
|
||||
|
||||
test {
|
||||
_ = @import("Atlas.zig");
|
||||
_ = @import("FontAtlas.zig");
|
||||
_ = @import("Grid.zig");
|
||||
_ = @import("Pty.zig");
|
||||
_ = @import("Command.zig");
|
||||
_ = @import("TempDir.zig");
|
||||
_ = @import("font/font.zig");
|
||||
_ = @import("terminal/Terminal.zig");
|
||||
|
||||
// Libraries
|
||||
|
Reference in New Issue
Block a user