diff --git a/src/Grid.zig b/src/Grid.zig index cf8459049..1151893d2 100644 --- a/src/Grid.zig +++ b/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); - 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"); + + // 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( - f32, - fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender), - ); + 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, ); } } diff --git a/src/font/Face.zig b/src/font/Face.zig index 1f6cb770b..c5ba7002a 100644 --- a/src/font/Face.zig +++ b/src/font/Face.zig @@ -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}); diff --git a/src/font/FallbackSet.zig b/src/font/FallbackSet.zig new file mode 100644 index 000000000..c2cc06045 --- /dev/null +++ b/src/font/FallbackSet.zig @@ -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); +} diff --git a/src/font/Family.zig b/src/font/Family.zig index a1c25268f..d1b00d223 100644 --- a/src/font/Family.zig +++ b/src/font/Family.zig @@ -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, diff --git a/src/font/main.zig b/src/font/main.zig index 5882ce145..cfcd24f07 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -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; }