mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
font FallbackSet for looking up in fallback TTFs (emoji)
This commit is contained in:
100
src/Grid.zig
100
src/Grid.zig
@ -45,8 +45,7 @@ texture: gl.Texture,
|
||||
texture_color: gl.Texture,
|
||||
|
||||
/// The font atlas.
|
||||
font_atlas: font.Family,
|
||||
font_emoji: font.Family,
|
||||
font_set: font.FallbackSet,
|
||||
atlas_dirty: bool,
|
||||
|
||||
/// Whether the cursor is visible or not. This is used to control cursor
|
||||
@ -118,10 +117,32 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
// font atlas with all the visible ASCII characters since they are common.
|
||||
var atlas = try Atlas.init(alloc, 512, .greyscale);
|
||||
errdefer atlas.deinit(alloc);
|
||||
|
||||
// Load our emoji font
|
||||
var atlas_color = try Atlas.init(alloc, 512, .rgba);
|
||||
errdefer atlas_color.deinit(alloc);
|
||||
|
||||
// Build our fallback set so we can look up all codepoints
|
||||
var font_set: font.FallbackSet = .{};
|
||||
try font_set.families.ensureTotalCapacity(alloc, 2);
|
||||
errdefer font_set.deinit(alloc);
|
||||
|
||||
// Regular text
|
||||
font_set.families.appendAssumeCapacity(fam: {
|
||||
var fam = try font.Family.init(atlas);
|
||||
errdefer fam.deinit(alloc);
|
||||
try fam.loadFaceFromMemory(.regular, face_ttf, config.@"font-size");
|
||||
try fam.loadFaceFromMemory(.bold, face_bold_ttf, config.@"font-size");
|
||||
break :fam fam;
|
||||
});
|
||||
|
||||
// Emoji
|
||||
font_set.families.appendAssumeCapacity(fam: {
|
||||
var fam_emoji = try font.Family.init(atlas_color);
|
||||
errdefer fam_emoji.deinit(alloc);
|
||||
try fam_emoji.loadFaceFromMemory(.regular, face_emoji_ttf, config.@"font-size");
|
||||
break :fam fam_emoji;
|
||||
});
|
||||
|
||||
// Load all visible ASCII characters and build our cell width based on
|
||||
// the widest character that we see.
|
||||
@ -129,9 +150,9 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
var cell_width: f32 = 0;
|
||||
var i: u8 = 32;
|
||||
while (i <= 126) : (i += 1) {
|
||||
const glyph = try fam.addGlyph(alloc, i, .regular);
|
||||
if (glyph.advance_x > cell_width) {
|
||||
cell_width = @ceil(glyph.advance_x);
|
||||
const goa = try font_set.getOrAddGlyph(alloc, i, .regular);
|
||||
if (goa.glyph.advance_x > cell_width) {
|
||||
cell_width = @ceil(goa.glyph.advance_x);
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,11 +162,13 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
// The cell height is the vertical height required to render underscore
|
||||
// '_' which should live at the bottom of a cell.
|
||||
const cell_height: f32 = cell_height: {
|
||||
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);
|
||||
|
||||
// Determine the height of the underscore char
|
||||
const glyph = fam.getGlyph('_', .regular).?;
|
||||
const glyph = font_set.families.items[0].getGlyph('_', .regular).?;
|
||||
var res: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender);
|
||||
res -= glyph.offset_y;
|
||||
res += @intCast(i32, glyph.height);
|
||||
@ -156,19 +179,15 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
|
||||
break :cell_height @intToFloat(f32, res);
|
||||
};
|
||||
const cell_baseline = cell_height - @intToFloat(
|
||||
const cell_baseline = cell_baseline: {
|
||||
const fam = &font_set.families.items[0];
|
||||
break :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 });
|
||||
|
||||
// Load our emoji font
|
||||
var atlas_color = try Atlas.init(alloc, 512, .rgba);
|
||||
errdefer atlas_color.deinit(alloc);
|
||||
var fam_emoji = try font.Family.init(atlas_color);
|
||||
errdefer fam_emoji.deinit(alloc);
|
||||
try fam_emoji.loadFaceFromMemory(.regular, face_emoji_ttf, config.@"font-size");
|
||||
|
||||
// Create our shader
|
||||
const program = try gl.Program.createVF(
|
||||
@embedFile("../shaders/cell.v.glsl"),
|
||||
@ -183,7 +202,7 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
|
||||
// Set all of our texture indexes
|
||||
try program.setUniform("text", 0);
|
||||
try program.setUniform("text_emoji", 1);
|
||||
try program.setUniform("text_color", 1);
|
||||
|
||||
// Setup our VAO
|
||||
const vao = try gl.VertexArray.create();
|
||||
@ -288,8 +307,7 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
.vbo = vbo,
|
||||
.texture = tex,
|
||||
.texture_color = tex_color,
|
||||
.font_atlas = fam,
|
||||
.font_emoji = fam_emoji,
|
||||
.font_set = font_set,
|
||||
.atlas_dirty = false,
|
||||
.cursor_visible = true,
|
||||
.cursor_style = .box,
|
||||
@ -299,10 +317,12 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Grid) void {
|
||||
self.font_atlas.atlas.deinit(self.alloc);
|
||||
self.font_atlas.deinit(self.alloc);
|
||||
self.font_emoji.atlas.deinit(self.alloc);
|
||||
self.font_emoji.deinit(self.alloc);
|
||||
for (self.font_set.families.items) |*family| {
|
||||
family.atlas.deinit(self.alloc);
|
||||
family.deinit(self.alloc);
|
||||
}
|
||||
self.font_set.deinit(self.alloc);
|
||||
|
||||
self.texture.destroy();
|
||||
self.texture_color.destroy();
|
||||
self.vbo.destroy();
|
||||
@ -500,26 +520,10 @@ pub fn updateCell(
|
||||
var mode: u8 = 2; // MODE_FG
|
||||
|
||||
// Get our glyph. Try our normal font atlas first.
|
||||
const glyph = if (self.font_atlas.getGlyph(cell.char, style)) |glyph|
|
||||
glyph
|
||||
else glyph: {
|
||||
self.atlas_dirty = true;
|
||||
break :glyph self.font_atlas.addGlyph(
|
||||
self.alloc,
|
||||
cell.char,
|
||||
style,
|
||||
) catch |err| switch (err) {
|
||||
error.GlyphNotFound => not_found: {
|
||||
mode = 7; // MODE_FG_COLOR
|
||||
break :not_found try self.font_emoji.addGlyph(
|
||||
self.alloc,
|
||||
cell.char,
|
||||
style,
|
||||
);
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
};
|
||||
const goa = try self.font_set.getOrAddGlyph(self.alloc, cell.char, style);
|
||||
if (!goa.found_existing) self.atlas_dirty = true;
|
||||
if (goa.family == 1) mode = 7; // MODE_FG_COLOR
|
||||
const glyph = goa.glyph;
|
||||
|
||||
self.cells.appendAssumeCapacity(.{
|
||||
.mode = mode,
|
||||
@ -594,32 +598,34 @@ pub fn setScreenSize(self: *Grid, dim: ScreenSize) !void {
|
||||
/// Updates the font texture atlas if it is dirty.
|
||||
fn flushAtlas(self: *Grid) !void {
|
||||
{
|
||||
const atlas = &self.font_set.families.items[0].atlas;
|
||||
var texbind = try self.texture.bind(.@"2D");
|
||||
defer texbind.unbind();
|
||||
try texbind.subImage2D(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@intCast(c_int, self.font_atlas.atlas.size),
|
||||
@intCast(c_int, self.font_atlas.atlas.size),
|
||||
@intCast(c_int, atlas.size),
|
||||
@intCast(c_int, atlas.size),
|
||||
.Red,
|
||||
.UnsignedByte,
|
||||
self.font_atlas.atlas.data.ptr,
|
||||
atlas.data.ptr,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const atlas = &self.font_set.families.items[1].atlas;
|
||||
var texbind = try self.texture_color.bind(.@"2D");
|
||||
defer texbind.unbind();
|
||||
try texbind.subImage2D(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@intCast(c_int, self.font_emoji.atlas.size),
|
||||
@intCast(c_int, self.font_emoji.atlas.size),
|
||||
@intCast(c_int, atlas.size),
|
||||
@intCast(c_int, atlas.size),
|
||||
.BGRA,
|
||||
.UnsignedByte,
|
||||
self.font_emoji.atlas.data.ptr,
|
||||
atlas.data.ptr,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ pub fn loadGlyph(self: Face, alloc: Allocator, atlas: *Atlas, cp: u32) !Glyph {
|
||||
if (idx > 0) break :glyph_index idx;
|
||||
|
||||
// Unknown glyph.
|
||||
log.warn("glyph not found: {x}", .{cp});
|
||||
//log.warn("glyph not found: {x}", .{cp});
|
||||
return error.GlyphNotFound;
|
||||
};
|
||||
//log.warn("glyph index: {}", .{glyph_index});
|
||||
|
150
src/font/FallbackSet.zig
Normal file
150
src/font/FallbackSet.zig
Normal file
@ -0,0 +1,150 @@
|
||||
//! FallbackSet represents a set of families in priority order to load a glyph.
|
||||
//! This can be used to merge multiple font families together to find a glyph
|
||||
//! for a codepoint.
|
||||
const FallbackSet = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const ftc = @import("freetype").c;
|
||||
const Atlas = @import("../Atlas.zig");
|
||||
const Family = @import("main.zig").Family;
|
||||
const Glyph = @import("main.zig").Glyph;
|
||||
const Style = @import("main.zig").Style;
|
||||
|
||||
const ftok = ftc.FT_Err_Ok;
|
||||
const log = std.log.scoped(.font_fallback);
|
||||
|
||||
/// The families to look for in order. This should be managed directly
|
||||
/// by the caller of the set. Deinit will deallocate this.
|
||||
families: std.ArrayListUnmanaged(Family) = .{},
|
||||
|
||||
/// A quick lookup that points directly to the family that loaded a glyph.
|
||||
glyphs: std.AutoHashMapUnmanaged(GlyphKey, usize) = .{},
|
||||
|
||||
const GlyphKey = struct {
|
||||
style: Style,
|
||||
codepoint: u32,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *FallbackSet, alloc: Allocator) void {
|
||||
self.families.deinit(alloc);
|
||||
self.glyphs.deinit(alloc);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
pub const GetOrAdd = struct {
|
||||
/// Index of the family where the glyph was loaded from
|
||||
family: usize,
|
||||
|
||||
/// True if the glyph was found or whether it was newly loaded
|
||||
found_existing: bool,
|
||||
|
||||
/// The glyph
|
||||
glyph: *Glyph,
|
||||
};
|
||||
|
||||
pub fn getOrAddGlyph(
|
||||
self: *FallbackSet,
|
||||
alloc: Allocator,
|
||||
v: anytype,
|
||||
style: Style,
|
||||
) !GetOrAdd {
|
||||
assert(self.families.items.len > 0);
|
||||
|
||||
// We need a UTF32 codepoint
|
||||
const utf32 = Family.codepoint(v);
|
||||
|
||||
// If we have this already, load it directly
|
||||
const glyphKey: GlyphKey = .{ .style = style, .codepoint = utf32 };
|
||||
const gop = try self.glyphs.getOrPut(alloc, glyphKey);
|
||||
if (gop.found_existing) {
|
||||
const i = gop.value_ptr.*;
|
||||
assert(i < self.families.items.len);
|
||||
return GetOrAdd{
|
||||
.family = i,
|
||||
.found_existing = true,
|
||||
.glyph = self.families.items[i].getGlyph(v, style) orelse unreachable,
|
||||
};
|
||||
}
|
||||
errdefer _ = self.glyphs.remove(glyphKey);
|
||||
|
||||
// Go through each familiy and look for a matching glyph
|
||||
var fam_i: usize = 0;
|
||||
const glyph = glyph: {
|
||||
var style_current = style;
|
||||
while (true) {
|
||||
for (self.families.items) |*family, i| {
|
||||
fam_i = i;
|
||||
|
||||
// If this family already has it loaded, return it.
|
||||
if (family.getGlyph(v, style_current)) |glyph| break :glyph glyph;
|
||||
|
||||
// Try to load it.
|
||||
if (family.addGlyph(alloc, v, style_current)) |glyph|
|
||||
break :glyph glyph
|
||||
else |err| switch (err) {
|
||||
error.GlyphNotFound => {},
|
||||
else => return err,
|
||||
}
|
||||
}
|
||||
|
||||
// We never found any glyph! For our first fallback, we'll simply
|
||||
// try to the non-styled variant.
|
||||
if (style_current == .regular) break;
|
||||
style_current = .regular;
|
||||
}
|
||||
|
||||
// If we are regular, we use a fallback character
|
||||
log.warn("glyph not found, using fallback. codepoint={x}", .{utf32});
|
||||
break :glyph try self.families.items[0].addGlyph(alloc, ' ', style);
|
||||
};
|
||||
|
||||
gop.value_ptr.* = fam_i;
|
||||
return GetOrAdd{
|
||||
.family = fam_i,
|
||||
.glyph = glyph,
|
||||
|
||||
// Technically possible that we found this in a cache...
|
||||
.found_existing = false,
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
const fontRegular = @import("test.zig").fontRegular;
|
||||
const fontEmoji = @import("test.zig").fontEmoji;
|
||||
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var set: FallbackSet = .{};
|
||||
try set.families.append(alloc, fam: {
|
||||
var fam = try Family.init(try Atlas.init(alloc, 512, .greyscale));
|
||||
try fam.loadFaceFromMemory(.regular, fontRegular, 48);
|
||||
break :fam fam;
|
||||
});
|
||||
try set.families.append(alloc, fam: {
|
||||
var fam = try Family.init(try Atlas.init(alloc, 512, .rgba));
|
||||
try fam.loadFaceFromMemory(.regular, fontEmoji, 48);
|
||||
break :fam fam;
|
||||
});
|
||||
|
||||
defer {
|
||||
for (set.families.items) |*family| {
|
||||
family.atlas.deinit(alloc);
|
||||
family.deinit(alloc);
|
||||
}
|
||||
set.deinit(alloc);
|
||||
}
|
||||
|
||||
// Generate all visible ASCII
|
||||
var i: u8 = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
_ = try set.getOrAddGlyph(alloc, i, .regular);
|
||||
}
|
||||
|
||||
// Emoji should work
|
||||
_ = try set.getOrAddGlyph(alloc, '🥸', .regular);
|
||||
_ = try set.getOrAddGlyph(alloc, '🥸', .bold);
|
||||
}
|
@ -137,13 +137,12 @@ pub fn addGlyph(self: *Family, alloc: Allocator, v: anytype, style: Style) !*Gly
|
||||
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 {
|
||||
pub fn codepoint(v: anytype) u32 {
|
||||
// We need a UTF32 codepoint for freetype
|
||||
return switch (@TypeOf(v)) {
|
||||
u32 => v,
|
||||
|
@ -1,6 +1,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;
|
||||
@ -18,4 +19,5 @@ test {
|
||||
_ = Face;
|
||||
_ = Family;
|
||||
_ = Glyph;
|
||||
_ = FallbackSet;
|
||||
}
|
||||
|
Reference in New Issue
Block a user