font FallbackSet for looking up in fallback TTFs (emoji)

This commit is contained in:
Mitchell Hashimoto
2022-08-19 22:05:27 -07:00
parent 22ed65a818
commit 43c61f57ef
5 changed files with 214 additions and 57 deletions

View File

@ -45,8 +45,7 @@ texture: gl.Texture,
texture_color: gl.Texture, texture_color: gl.Texture,
/// The font atlas. /// The font atlas.
font_atlas: font.Family, font_set: font.FallbackSet,
font_emoji: font.Family,
atlas_dirty: bool, atlas_dirty: bool,
/// Whether the cursor is visible or not. This is used to control cursor /// 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. // font atlas with all the visible ASCII characters since they are common.
var atlas = try Atlas.init(alloc, 512, .greyscale); var atlas = try Atlas.init(alloc, 512, .greyscale);
errdefer atlas.deinit(alloc); 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); var fam = try font.Family.init(atlas);
errdefer fam.deinit(alloc); errdefer fam.deinit(alloc);
try fam.loadFaceFromMemory(.regular, face_ttf, config.@"font-size"); try fam.loadFaceFromMemory(.regular, face_ttf, config.@"font-size");
try fam.loadFaceFromMemory(.bold, face_bold_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 // Load all visible ASCII characters and build our cell width based on
// the widest character that we see. // 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 cell_width: f32 = 0;
var i: u8 = 32; var i: u8 = 32;
while (i <= 126) : (i += 1) { while (i <= 126) : (i += 1) {
const glyph = try fam.addGlyph(alloc, i, .regular); const goa = try font_set.getOrAddGlyph(alloc, i, .regular);
if (glyph.advance_x > cell_width) { if (goa.glyph.advance_x > cell_width) {
cell_width = @ceil(glyph.advance_x); 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 // The cell height is the vertical height required to render underscore
// '_' which should live at the bottom of a cell. // '_' which should live at the bottom of a cell.
const cell_height: f32 = cell_height: { const cell_height: f32 = cell_height: {
const fam = &font_set.families.items[0];
// This is the height reported by the font face // 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.?.ft_face.*.height);
// Determine the height of the underscore char // 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); var res: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender);
res -= glyph.offset_y; res -= glyph.offset_y;
res += @intCast(i32, glyph.height); res += @intCast(i32, glyph.height);
@ -156,19 +179,15 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
break :cell_height @intToFloat(f32, res); 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, f32,
fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender), fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender),
); );
};
log.debug("cell dimensions w={d} h={d} baseline={d}", .{ cell_width, cell_height, cell_baseline }); 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 // Create our shader
const program = try gl.Program.createVF( const program = try gl.Program.createVF(
@embedFile("../shaders/cell.v.glsl"), @embedFile("../shaders/cell.v.glsl"),
@ -183,7 +202,7 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
// Set all of our texture indexes // Set all of our texture indexes
try program.setUniform("text", 0); try program.setUniform("text", 0);
try program.setUniform("text_emoji", 1); try program.setUniform("text_color", 1);
// Setup our VAO // Setup our VAO
const vao = try gl.VertexArray.create(); const vao = try gl.VertexArray.create();
@ -288,8 +307,7 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
.vbo = vbo, .vbo = vbo,
.texture = tex, .texture = tex,
.texture_color = tex_color, .texture_color = tex_color,
.font_atlas = fam, .font_set = font_set,
.font_emoji = fam_emoji,
.atlas_dirty = false, .atlas_dirty = false,
.cursor_visible = true, .cursor_visible = true,
.cursor_style = .box, .cursor_style = .box,
@ -299,10 +317,12 @@ pub fn init(alloc: Allocator, config: *const Config) !Grid {
} }
pub fn deinit(self: *Grid) void { pub fn deinit(self: *Grid) void {
self.font_atlas.atlas.deinit(self.alloc); for (self.font_set.families.items) |*family| {
self.font_atlas.deinit(self.alloc); family.atlas.deinit(self.alloc);
self.font_emoji.atlas.deinit(self.alloc); family.deinit(self.alloc);
self.font_emoji.deinit(self.alloc); }
self.font_set.deinit(self.alloc);
self.texture.destroy(); self.texture.destroy();
self.texture_color.destroy(); self.texture_color.destroy();
self.vbo.destroy(); self.vbo.destroy();
@ -500,26 +520,10 @@ pub fn updateCell(
var mode: u8 = 2; // MODE_FG var mode: u8 = 2; // MODE_FG
// Get our glyph. Try our normal font atlas first. // Get our glyph. Try our normal font atlas first.
const glyph = if (self.font_atlas.getGlyph(cell.char, style)) |glyph| const goa = try self.font_set.getOrAddGlyph(self.alloc, cell.char, style);
glyph if (!goa.found_existing) self.atlas_dirty = true;
else glyph: { if (goa.family == 1) mode = 7; // MODE_FG_COLOR
self.atlas_dirty = true; const glyph = goa.glyph;
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,
};
};
self.cells.appendAssumeCapacity(.{ self.cells.appendAssumeCapacity(.{
.mode = mode, .mode = mode,
@ -594,32 +598,34 @@ pub fn setScreenSize(self: *Grid, dim: ScreenSize) !void {
/// Updates the font texture atlas if it is dirty. /// Updates the font texture atlas if it is dirty.
fn flushAtlas(self: *Grid) !void { fn flushAtlas(self: *Grid) !void {
{ {
const atlas = &self.font_set.families.items[0].atlas;
var texbind = try self.texture.bind(.@"2D"); var texbind = try self.texture.bind(.@"2D");
defer texbind.unbind(); defer texbind.unbind();
try texbind.subImage2D( try texbind.subImage2D(
0, 0,
0, 0,
0, 0,
@intCast(c_int, self.font_atlas.atlas.size), @intCast(c_int, atlas.size),
@intCast(c_int, self.font_atlas.atlas.size), @intCast(c_int, atlas.size),
.Red, .Red,
.UnsignedByte, .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"); var texbind = try self.texture_color.bind(.@"2D");
defer texbind.unbind(); defer texbind.unbind();
try texbind.subImage2D( try texbind.subImage2D(
0, 0,
0, 0,
0, 0,
@intCast(c_int, self.font_emoji.atlas.size), @intCast(c_int, atlas.size),
@intCast(c_int, self.font_emoji.atlas.size), @intCast(c_int, atlas.size),
.BGRA, .BGRA,
.UnsignedByte, .UnsignedByte,
self.font_emoji.atlas.data.ptr, atlas.data.ptr,
); );
} }
} }

View File

@ -97,7 +97,7 @@ pub fn loadGlyph(self: Face, alloc: Allocator, atlas: *Atlas, cp: u32) !Glyph {
if (idx > 0) break :glyph_index idx; if (idx > 0) break :glyph_index idx;
// Unknown glyph. // Unknown glyph.
log.warn("glyph not found: {x}", .{cp}); //log.warn("glyph not found: {x}", .{cp});
return error.GlyphNotFound; return error.GlyphNotFound;
}; };
//log.warn("glyph index: {}", .{glyph_index}); //log.warn("glyph index: {}", .{glyph_index});

150
src/font/FallbackSet.zig Normal file
View 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);
}

View File

@ -137,13 +137,12 @@ pub fn addGlyph(self: *Family, alloc: Allocator, v: anytype, style: Style) !*Gly
errdefer _ = self.glyphs.remove(glyphKey); errdefer _ = self.glyphs.remove(glyphKey);
// Get the glyph and add it to the atlas. // Get the glyph and add it to the atlas.
// TODO: handle glyph not found
gop.value_ptr.* = try face.loadGlyph(alloc, &self.atlas, utf32); gop.value_ptr.* = try face.loadGlyph(alloc, &self.atlas, utf32);
return gop.value_ptr; return gop.value_ptr;
} }
/// Returns the UTF-32 codepoint for the given value. /// 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 // We need a UTF32 codepoint for freetype
return switch (@TypeOf(v)) { return switch (@TypeOf(v)) {
u32 => v, u32 => v,

View File

@ -1,6 +1,7 @@
pub const Face = @import("Face.zig"); pub const Face = @import("Face.zig");
pub const Family = @import("Family.zig"); pub const Family = @import("Family.zig");
pub const Glyph = @import("Glyph.zig"); pub const Glyph = @import("Glyph.zig");
pub const FallbackSet = @import("FallbackSet.zig");
/// Embedded fonts (for now) /// Embedded fonts (for now)
pub const fontRegular = @import("test.zig").fontRegular; pub const fontRegular = @import("test.zig").fontRegular;
@ -18,4 +19,5 @@ test {
_ = Face; _ = Face;
_ = Family; _ = Family;
_ = Glyph; _ = Glyph;
_ = FallbackSet;
} }