mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
convert src/font to use new pkg/freetype
This commit is contained in:
@ -106,6 +106,7 @@ pub const LoadFlags = packed struct {
|
||||
monochrome: bool = false,
|
||||
linear_design: bool = false,
|
||||
no_autohint: bool = false,
|
||||
_padding1: u1 = 0,
|
||||
target_normal: bool = false,
|
||||
target_light: bool = false,
|
||||
target_mono: bool = false,
|
||||
@ -114,7 +115,7 @@ pub const LoadFlags = packed struct {
|
||||
color: bool = false,
|
||||
compute_metrics: bool = false,
|
||||
bitmap_metrics_only: bool = false,
|
||||
_padding: u10 = 0,
|
||||
_padding2: u9 = 0,
|
||||
|
||||
test {
|
||||
// This must always be an i32 size so we can bitcast directly.
|
||||
@ -124,11 +125,12 @@ pub const LoadFlags = packed struct {
|
||||
|
||||
test "bitcast" {
|
||||
const testing = std.testing;
|
||||
const cval: i32 = c.FT_LOAD_RENDER | c.FT_LOAD_PEDANTIC;
|
||||
const cval: i32 = c.FT_LOAD_RENDER | c.FT_LOAD_PEDANTIC | c.FT_LOAD_COLOR;
|
||||
const flags = @bitCast(LoadFlags, cval);
|
||||
try testing.expect(!flags.no_hinting);
|
||||
try testing.expect(flags.render);
|
||||
try testing.expect(flags.pedantic);
|
||||
try testing.expect(flags.color);
|
||||
}
|
||||
};
|
||||
|
||||
|
16
src/Grid.zig
16
src/Grid.zig
@ -44,6 +44,7 @@ texture: gl.Texture,
|
||||
texture_color: gl.Texture,
|
||||
|
||||
/// The font atlas.
|
||||
font_lib: font.Library,
|
||||
font_set: font.FallbackSet,
|
||||
|
||||
/// Whether the cursor is visible or not. This is used to control cursor
|
||||
@ -151,9 +152,12 @@ pub fn init(
|
||||
try font_set.families.ensureTotalCapacity(alloc, 2);
|
||||
errdefer font_set.deinit(alloc);
|
||||
|
||||
var font_lib = try font.Library.init();
|
||||
errdefer font_lib.deinit();
|
||||
|
||||
// Regular text
|
||||
font_set.families.appendAssumeCapacity(fam: {
|
||||
var fam = try font.Family.init(atlas);
|
||||
var fam = font.Family.init(font_lib, atlas);
|
||||
errdefer fam.deinit(alloc);
|
||||
try fam.loadFaceFromMemory(.regular, face_ttf, font_size);
|
||||
try fam.loadFaceFromMemory(.bold, face_bold_ttf, font_size);
|
||||
@ -162,7 +166,7 @@ pub fn init(
|
||||
|
||||
// Emoji
|
||||
font_set.families.appendAssumeCapacity(fam: {
|
||||
var fam_emoji = try font.Family.init(atlas_color);
|
||||
var fam_emoji = font.Family.init(font_lib, atlas_color);
|
||||
errdefer fam_emoji.deinit(alloc);
|
||||
try fam_emoji.loadFaceFromMemory(.regular, face_emoji_ttf, font_size);
|
||||
break :fam fam_emoji;
|
||||
@ -189,11 +193,11 @@ pub fn init(
|
||||
const fam = &font_set.families.items[0];
|
||||
|
||||
// This is the height reported by the font face
|
||||
const face_height: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.height);
|
||||
const face_height: i32 = fam.regular.?.unitsToPxY(fam.regular.?.face.?.handle.*.height);
|
||||
|
||||
// Determine the height of the underscore char
|
||||
const glyph = font_set.families.items[0].getGlyph('_', .regular).?;
|
||||
var res: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender);
|
||||
var res: i32 = fam.regular.?.unitsToPxY(fam.regular.?.face.?.handle.*.ascender);
|
||||
res -= glyph.offset_y;
|
||||
res += @intCast(i32, glyph.height);
|
||||
|
||||
@ -207,7 +211,7 @@ pub fn init(
|
||||
const fam = &font_set.families.items[0];
|
||||
break :cell_baseline cell_height - @intToFloat(
|
||||
f32,
|
||||
fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender),
|
||||
fam.regular.?.unitsToPxY(fam.regular.?.face.?.handle.*.ascender),
|
||||
);
|
||||
};
|
||||
log.debug("cell dimensions w={d} h={d} baseline={d}", .{ cell_width, cell_height, cell_baseline });
|
||||
@ -331,6 +335,7 @@ pub fn init(
|
||||
.vbo = vbo,
|
||||
.texture = tex,
|
||||
.texture_color = tex_color,
|
||||
.font_lib = font_lib,
|
||||
.font_set = font_set,
|
||||
.cursor_visible = true,
|
||||
.cursor_style = .box,
|
||||
@ -345,6 +350,7 @@ pub fn deinit(self: *Grid) void {
|
||||
family.deinit(self.alloc);
|
||||
}
|
||||
self.font_set.deinit(self.alloc);
|
||||
self.font_lib.deinit();
|
||||
|
||||
self.texture.destroy();
|
||||
self.texture_color.destroy();
|
||||
|
@ -240,8 +240,8 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
|
||||
const window_size = try window.getSize();
|
||||
var grid = try Grid.init(alloc, .{
|
||||
.points = config.@"font-size",
|
||||
.xdpi = @floatToInt(u32, x_dpi),
|
||||
.ydpi = @floatToInt(u32, y_dpi),
|
||||
.xdpi = @floatToInt(u16, x_dpi),
|
||||
.ydpi = @floatToInt(u16, y_dpi),
|
||||
});
|
||||
try grid.setScreenSize(.{ .width = window_size.width, .height = window_size.height });
|
||||
grid.background = .{
|
||||
|
@ -7,53 +7,49 @@ const Face = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const freetype = @import("freetype");
|
||||
const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ftc = @import("freetype").c;
|
||||
const Atlas = @import("../Atlas.zig");
|
||||
const Glyph = @import("main.zig").Glyph;
|
||||
const Library = @import("main.zig").Library;
|
||||
|
||||
const ftok = ftc.FT_Err_Ok;
|
||||
const log = std.log.scoped(.font_face);
|
||||
|
||||
/// The FreeType library
|
||||
ft_library: ftc.FT_Library,
|
||||
/// The core library
|
||||
library: Library,
|
||||
|
||||
/// Our font face.
|
||||
ft_face: ftc.FT_Face = null,
|
||||
face: ?freetype.Face = null,
|
||||
|
||||
/// If a DPI can't be calculated, this DPI is used. This is probably
|
||||
/// wrong on modern devices so it is highly recommended you get the DPI
|
||||
/// using whatever platform method you can.
|
||||
pub const default_dpi = if (builtin.os.tag == .macos) 72 else 96;
|
||||
|
||||
pub fn init(lib: ftc.FT_Library) !Face {
|
||||
pub fn init(lib: Library) !Face {
|
||||
return Face{
|
||||
.ft_library = lib,
|
||||
.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", .{});
|
||||
}
|
||||
|
||||
if (self.face) |face| face.deinit();
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// The desired size for loading a font.
|
||||
pub const DesiredSize = struct {
|
||||
// Desired size in points
|
||||
points: u32,
|
||||
points: u16,
|
||||
|
||||
// The DPI of the screen so we can convert points to pixels.
|
||||
xdpi: u32 = default_dpi,
|
||||
ydpi: u32 = default_dpi,
|
||||
xdpi: u16 = default_dpi,
|
||||
ydpi: u16 = default_dpi,
|
||||
|
||||
// Converts points to pixels
|
||||
pub fn pixels(self: DesiredSize) u32 {
|
||||
pub fn pixels(self: DesiredSize) u16 {
|
||||
// 1 point = 1/72 inch
|
||||
return (self.points * self.ydpi) / 72;
|
||||
}
|
||||
@ -65,86 +61,66 @@ pub fn loadFaceFromMemory(
|
||||
source: [:0]const u8,
|
||||
size: DesiredSize,
|
||||
) !void {
|
||||
assert(self.ft_face == null);
|
||||
assert(self.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;
|
||||
}
|
||||
const face = try self.library.lib.initMemoryFace(source, 0);
|
||||
errdefer face.deinit();
|
||||
|
||||
if (ftc.FT_Select_Charmap(self.ft_face, ftc.FT_ENCODING_UNICODE) != ftok)
|
||||
return error.FaceLoadFailed;
|
||||
try face.selectCharmap(.unicode);
|
||||
|
||||
// If we have fixed sizes, we just have to try to pick the one closest
|
||||
// to what the user requested. Otherwise, we can choose an arbitrary
|
||||
// pixel size.
|
||||
if (!ftc.FT_HAS_FIXED_SIZES(self.ft_face)) {
|
||||
const size_26dot6 = size.points << 6; // mult by 64
|
||||
if (ftc.FT_Set_Char_Size(self.ft_face, 0, size_26dot6, size.xdpi, size.ydpi) != ftok)
|
||||
return error.FaceLoadFailed;
|
||||
} else try self.selectSizeNearest(size.pixels());
|
||||
if (!face.hasFixedSizes()) {
|
||||
const size_26dot6 = @intCast(i32, size.points << 6); // mult by 64
|
||||
try face.setCharSize(0, size_26dot6, size.xdpi, size.ydpi);
|
||||
} else try selectSizeNearest(face, size.pixels());
|
||||
|
||||
// Success, persist
|
||||
self.face = face;
|
||||
}
|
||||
|
||||
/// Selects the fixed size in the loaded face that is closest to the
|
||||
/// requested pixel size.
|
||||
fn selectSizeNearest(self: *Face, size: u32) !void {
|
||||
var i: usize = 0;
|
||||
var best_i: usize = 0;
|
||||
fn selectSizeNearest(face: freetype.Face, size: u32) !void {
|
||||
var i: i32 = 0;
|
||||
var best_i: i32 = 0;
|
||||
var best_diff: i32 = 0;
|
||||
while (i < self.ft_face.*.num_fixed_sizes) : (i += 1) {
|
||||
const diff = @intCast(i32, size) - @intCast(i32, self.ft_face.*.available_sizes[i].width);
|
||||
while (i < face.handle.*.num_fixed_sizes) : (i += 1) {
|
||||
const width = face.handle.*.available_sizes[@intCast(usize, i)].width;
|
||||
const diff = @intCast(i32, size) - @intCast(i32, width);
|
||||
if (i == 0 or diff < best_diff) {
|
||||
best_diff = diff;
|
||||
best_i = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (ftc.FT_Select_Size(self.ft_face, @intCast(c_int, best_i)) != ftok)
|
||||
return error.FaceSelectSizeFailed;
|
||||
try face.selectSize(best_i);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
const face = self.face.?;
|
||||
|
||||
// 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});
|
||||
return error.GlyphNotFound;
|
||||
};
|
||||
const glyph_index = face.getCharIndex(cp) orelse return error.GlyphNotFound;
|
||||
//log.warn("glyph index: {}", .{glyph_index});
|
||||
|
||||
// If our glyph has color, we want to render the color
|
||||
var load_flags: c_int = ftc.FT_LOAD_RENDER;
|
||||
if (ftc.FT_HAS_COLOR(self.ft_face)) load_flags |= @intCast(c_int, ftc.FT_LOAD_COLOR);
|
||||
try face.loadGlyph(glyph_index, .{
|
||||
.render = true,
|
||||
.color = face.hasColor(),
|
||||
});
|
||||
|
||||
if (ftc.FT_Load_Glyph(
|
||||
self.ft_face,
|
||||
glyph_index,
|
||||
load_flags,
|
||||
) != ftok) return error.LoadGlyphFailed;
|
||||
|
||||
const glyph = self.ft_face.*.glyph;
|
||||
const glyph = face.handle.*.glyph;
|
||||
const bitmap = glyph.*.bitmap;
|
||||
|
||||
// Ensure we know how to work with the font format. And assure that
|
||||
// or color depth is as expected on the texture atlas.
|
||||
const format: Atlas.Format = switch (bitmap.pixel_mode) {
|
||||
ftc.FT_PIXEL_MODE_GRAY => .greyscale,
|
||||
ftc.FT_PIXEL_MODE_BGRA => .rgba,
|
||||
freetype.c.FT_PIXEL_MODE_GRAY => .greyscale,
|
||||
freetype.c.FT_PIXEL_MODE_BGRA => .rgba,
|
||||
else => {
|
||||
log.warn("pixel mode={}", .{bitmap.pixel_mode});
|
||||
@panic("unsupported pixel mode");
|
||||
@ -204,27 +180,28 @@ pub fn loadGlyph(self: Face, alloc: Allocator, atlas: *Atlas, cp: u32) !Glyph {
|
||||
/// 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);
|
||||
return @intCast(i32, freetype.mulFix(
|
||||
units,
|
||||
@intCast(i32, self.face.?.handle.*.size.*.metrics.y_scale),
|
||||
) >> 6);
|
||||
}
|
||||
|
||||
/// Convert 26.6 pixel format to f32
|
||||
fn f26dot6ToFloat(v: ftc.FT_F26Dot6) f32 {
|
||||
fn f26dot6ToFloat(v: freetype.c.FT_F26Dot6) f32 {
|
||||
return @intToFloat(f32, v >> 6);
|
||||
}
|
||||
|
||||
test {
|
||||
const testFont = @import("test.zig").fontRegular;
|
||||
|
||||
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 lib = try Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var atlas = try Atlas.init(alloc, 512, .greyscale);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var font = try init(ft_lib);
|
||||
var font = try init(lib);
|
||||
defer font.deinit();
|
||||
|
||||
try font.loadFaceFromMemory(testFont, .{ .points = 12 });
|
||||
@ -237,18 +214,16 @@ test {
|
||||
}
|
||||
|
||||
test "color emoji" {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = @import("test.zig").fontEmoji;
|
||||
|
||||
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);
|
||||
var lib = try Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
const alloc = testing.allocator;
|
||||
var atlas = try Atlas.init(alloc, 512, .rgba);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var font = try init(ft_lib);
|
||||
var font = try init(lib);
|
||||
defer font.deinit();
|
||||
|
||||
try font.loadFaceFromMemory(testFont, .{ .points = 12 });
|
||||
|
@ -9,6 +9,7 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const Atlas = @import("../Atlas.zig");
|
||||
const Family = @import("main.zig").Family;
|
||||
const Library = @import("main.zig").Library;
|
||||
const Glyph = @import("main.zig").Glyph;
|
||||
const Style = @import("main.zig").Style;
|
||||
const codepoint = @import("main.zig").codepoint;
|
||||
@ -123,14 +124,17 @@ test {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var lib = try Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var set: FallbackSet = .{};
|
||||
try set.families.append(alloc, fam: {
|
||||
var fam = try Family.init(try Atlas.init(alloc, 512, .greyscale));
|
||||
var fam = Family.init(lib, try Atlas.init(alloc, 512, .greyscale));
|
||||
try fam.loadFaceFromMemory(.regular, fontRegular, .{ .points = 48 });
|
||||
break :fam fam;
|
||||
});
|
||||
try set.families.append(alloc, fam: {
|
||||
var fam = try Family.init(try Atlas.init(alloc, 512, .rgba));
|
||||
var fam = Family.init(lib, try Atlas.init(alloc, 512, .rgba));
|
||||
try fam.loadFaceFromMemory(.regular, fontEmoji, .{ .points = 48 });
|
||||
break :fam fam;
|
||||
});
|
||||
|
@ -5,27 +5,22 @@ 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 codepoint = @import("main.zig").codepoint;
|
||||
const Library = @import("main.zig").Library;
|
||||
|
||||
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 library shared state.
|
||||
lib: Library,
|
||||
|
||||
/// The glyphs that are loaded into the atlas, keyed by codepoint.
|
||||
glyphs: std.AutoHashMapUnmanaged(GlyphKey, Glyph) = .{},
|
||||
@ -41,16 +36,11 @@ const GlyphKey = struct {
|
||||
codepoint: u32,
|
||||
};
|
||||
|
||||
pub fn init(atlas: Atlas) !Family {
|
||||
var res = Family{
|
||||
pub fn init(lib: Library, atlas: Atlas) Family {
|
||||
return .{
|
||||
.lib = lib,
|
||||
.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 {
|
||||
@ -59,9 +49,6 @@ pub fn deinit(self: *Family, alloc: Allocator) void {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -74,7 +61,7 @@ pub fn loadFaceFromMemory(
|
||||
source: [:0]const u8,
|
||||
size: Face.DesiredSize,
|
||||
) !void {
|
||||
var face = try Face.init(self.ft_library);
|
||||
var face = try Face.init(self.lib);
|
||||
errdefer face.deinit();
|
||||
try face.loadFaceFromMemory(source, size);
|
||||
|
||||
@ -145,7 +132,11 @@ pub fn addGlyph(self: *Family, alloc: Allocator, v: anytype, style: Style) !*Gly
|
||||
test {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
var fam = try init(try Atlas.init(alloc, 512, .greyscale));
|
||||
|
||||
var lib = try Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var fam = init(lib, try Atlas.init(alloc, 512, .greyscale));
|
||||
defer fam.deinit(alloc);
|
||||
defer fam.atlas.deinit(alloc);
|
||||
try fam.loadFaceFromMemory(.regular, testFont, .{ .points = 12 });
|
||||
|
19
src/font/Library.zig
Normal file
19
src/font/Library.zig
Normal file
@ -0,0 +1,19 @@
|
||||
//! A library represents the shared state that the underlying font
|
||||
//! library implementation(s) require per-process.
|
||||
//!
|
||||
//! In the future, this will be abstracted so that the underlying text
|
||||
//! engine might not be Freetype and may be something like Core Text,
|
||||
//! but the API will remain the same.
|
||||
const Library = @This();
|
||||
|
||||
const freetype = @import("freetype");
|
||||
|
||||
lib: freetype.Library,
|
||||
|
||||
pub fn init() freetype.Error!Library {
|
||||
return Library{ .lib = try freetype.Library.init() };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Library) void {
|
||||
self.lib.deinit();
|
||||
}
|
@ -4,10 +4,7 @@ pub const Face = @import("Face.zig");
|
||||
pub const Family = @import("Family.zig");
|
||||
pub const Glyph = @import("Glyph.zig");
|
||||
pub const FallbackSet = @import("FallbackSet.zig");
|
||||
|
||||
/// Embedded fonts (for now)
|
||||
pub const fontRegular = @import("test.zig").fontRegular;
|
||||
pub const fontBold = @import("test.zig").fontBold;
|
||||
pub const Library = @import("Library.zig");
|
||||
|
||||
/// The styles that a family can take.
|
||||
pub const Style = enum {
|
||||
@ -29,8 +26,5 @@ pub fn codepoint(v: anytype) u32 {
|
||||
}
|
||||
|
||||
test {
|
||||
_ = Face;
|
||||
_ = Family;
|
||||
_ = Glyph;
|
||||
_ = FallbackSet;
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
|
Reference in New Issue
Block a user