From e49c4707a1e40094d181af78c1e42cd4a2365366 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Aug 2024 10:28:58 -0700 Subject: [PATCH 01/10] config: note that styles that aren't found will use regular --- src/config/Config.zig | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index a69371dd9..160e76dfb 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -52,10 +52,17 @@ const c = @cImport({ /// /// The specific styles (bold, italic, bold italic) do not need to be /// explicitly set. If a style is not set, then the regular style (font-family) -/// will be searched for stylistic variants. If an italic style is not found, -/// Ghostty will auto-italicize the regular style by applying a slant. If -/// a bold style is not found, Ghostty will look for another monospace -/// font. +/// will be searched for stylistic variants. If a stylistic variant is not +/// found, Ghostty will use the regular style. This prevents falling back to a +/// different font family just to get a style such as bold. This also applies +/// if you explictly speciy a font family for a style. For example, if you +/// set `font-family-bold = FooBar` and "FooBar" cannot be found, Ghostty will +/// use whatever font is set for `font-family` for the bold style. +/// +/// Finally, some styles may be synthesized if they are not supported. +/// For example, if a font does not have an italic style and no alternative +/// italic font is specified, Ghostty will synthesize an italic style by +/// applying a slant to the regular style. /// /// You can disable styles completely by using the `font-style` set of /// configurations. See the documentation for `font-style` for more information. From c183e71a92bf80ca19da02186486b4cc2dcb68c2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Aug 2024 10:28:58 -0700 Subject: [PATCH 02/10] font: support aliased entries in the font collection style table --- src/font/Collection.zig | 57 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 86231b839..08d5c81c3 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -16,6 +16,7 @@ const Collection = @This(); const std = @import("std"); +const assert = std.debug.assert; const Allocator = std.mem.Allocator; const font = @import("main.zig"); const options = font.options; @@ -104,7 +105,21 @@ pub fn add( pub fn getFace(self: *Collection, index: Index) !*Face { if (index.special() != null) return error.SpecialHasNoFace; const list = self.faces.getPtr(index.style); - const item = &list.items[index.idx]; + const item: *Entry = item: { + var item = &list.items[index.idx]; + switch (item.*) { + .alias => |ptr| item = ptr, + + .deferred, + .fallback_deferred, + .loaded, + .fallback_loaded, + => {}, + } + assert(item.* != .alias); + break :item item; + }; + return switch (item.*) { inline .deferred, .fallback_deferred => |*d, tag| deferred: { const opts = self.load_options orelse @@ -125,6 +140,10 @@ pub fn getFace(self: *Collection, index: Index) !*Face { }, .loaded, .fallback_loaded => |*f| f, + + // When setting `item` above, we ensure we don't end up with + // an alias. + .alias => unreachable, }; } @@ -168,6 +187,23 @@ pub fn hasCodepoint( return list.items[index.idx].hasCodepoint(cp, p_mode); } +/// Ensure we have an option for all styles in the collection, such +/// as italic and bold. +/// +/// This requires that a regular font face is already loaded. +/// This is asserted. If a font style is missing, we will synthesize +/// it if possible. Otherwise, we will use the regular font style. +pub fn completeStyles(self: *Collection, alloc: Allocator) !void { + const regular_list = self.faces.getPtr(.regular); + assert(regular_list.items.len > 0); + + // If we don't have bold, use the regular font. + const bold_list = self.faces.getPtr(.bold); + if (bold_list.items.len == 0) {} + + _ = alloc; +} + /// Automatically create an italicized font from the regular /// font face if we don't have one already. If we already have /// an italicized font face, this does nothing. @@ -243,10 +279,16 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void { var it = self.faces.iterator(); while (it.next()) |entry| { for (entry.value.items) |*elem| switch (elem.*) { - .deferred, .fallback_deferred => continue, .loaded, .fallback_loaded => |*f| try f.setSize( opts.faceOptions(), ), + + // Deferred aren't loaded so we don't need to set their size. + // The size for when they're loaded is set since `opts` changed. + .deferred, .fallback_deferred => continue, + + // Alias faces don't own their size. + .alias => continue, }; } } @@ -318,6 +360,10 @@ pub const Entry = union(enum) { fallback_deferred: DeferredFace, fallback_loaded: Face, + // An alias to another entry. This is used to share the same face, + // avoid memory duplication. An alias must point to a non-alias entry. + alias: *Entry, + pub fn deinit(self: *Entry) void { switch (self.*) { inline .deferred, @@ -325,6 +371,10 @@ pub const Entry = union(enum) { .fallback_deferred, .fallback_loaded, => |*v| v.deinit(), + + // Aliased fonts are not owned by this entry so we let them + // be deallocated by the owner. + .alias => {}, } } @@ -333,6 +383,7 @@ pub const Entry = union(enum) { return switch (self) { .deferred, .fallback_deferred => true, .loaded, .fallback_loaded => false, + .alias => |v| v.isDeferred(), }; } @@ -343,6 +394,8 @@ pub const Entry = union(enum) { p_mode: PresentationMode, ) bool { return switch (self) { + .alias => |v| v.hasCodepoint(cp, p_mode), + // Non-fallback fonts require explicit presentation matching but // otherwise don't care about presentation .deferred => |v| switch (p_mode) { From 1f3ccb2d664cf0fe041943e6073c97c36650d431 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Aug 2024 14:49:51 -0700 Subject: [PATCH 03/10] font: Collection uses SegmentedList for styles for pointer stability --- src/font/CodepointResolver.zig | 6 +-- src/font/Collection.zig | 69 +++++++++++++++++++--------------- src/font/SharedGrid.zig | 2 +- src/font/SharedGridSet.zig | 2 +- src/font/discovery.zig | 12 +++--- src/font/shaper/coretext.zig | 2 +- 6 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index 5caeb728f..c090356ff 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -383,7 +383,7 @@ test getIndex { var lib = try Library.init(); defer lib.deinit(); - var c = try Collection.init(alloc); + var c = Collection.init(); c.load_options = .{ .library = lib }; { @@ -464,7 +464,7 @@ test "getIndex disabled font style" { var lib = try Library.init(); defer lib.deinit(); - var c = try Collection.init(alloc); + var c = Collection.init(); c.load_options = .{ .library = lib }; _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( @@ -516,7 +516,7 @@ test "getIndex box glyph" { var lib = try Library.init(); defer lib.deinit(); - const c = try Collection.init(alloc); + const c = Collection.init(); var r: CodepointResolver = .{ .collection = c, diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 08d5c81c3..f5e1880ed 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -40,21 +40,18 @@ faces: StyleArray, load_options: ?LoadOptions = null, /// Initialize an empty collection. -pub fn init( - alloc: Allocator, -) !Collection { +pub fn init() Collection { // Initialize our styles array, preallocating some space that is // likely to be used. - var faces = StyleArray.initFill(.{}); - for (&faces.values) |*v| try v.ensureTotalCapacityPrecise(alloc, 2); - return .{ .faces = faces }; + return .{ .faces = StyleArray.initFill(.{}) }; } pub fn deinit(self: *Collection, alloc: Allocator) void { var it = self.faces.iterator(); - while (it.next()) |entry| { - for (entry.value.items) |*item| item.deinit(); - entry.value.deinit(alloc); + while (it.next()) |array| { + var entry_it = array.value.iterator(0); + while (entry_it.next()) |entry| entry.deinit(); + array.value.deinit(alloc); } if (self.load_options) |*v| v.deinit(alloc); @@ -85,14 +82,14 @@ pub fn add( const list = self.faces.getPtr(style); // We have some special indexes so we must never pass those. - if (list.items.len >= Index.Special.start - 1) + const idx = list.count(); + if (idx >= Index.Special.start - 1) return error.CollectionFull; // If this is deferred and we don't have load options, we can't. if (face.isDeferred() and self.load_options == null) return error.DeferredLoadingUnavailable; - const idx = list.items.len; try list.append(alloc, face); return .{ .style = style, .idx = @intCast(idx) }; } @@ -106,7 +103,7 @@ pub fn getFace(self: *Collection, index: Index) !*Face { if (index.special() != null) return error.SpecialHasNoFace; const list = self.faces.getPtr(index.style); const item: *Entry = item: { - var item = &list.items[index.idx]; + var item = list.at(index.idx); switch (item.*) { .alias => |ptr| item = ptr, @@ -159,13 +156,17 @@ pub fn getIndex( style: Style, p_mode: PresentationMode, ) ?Index { - for (self.faces.get(style).items, 0..) |elem, i| { - if (elem.hasCodepoint(cp, p_mode)) { + var i: usize = 0; + var it = self.faces.get(style).constIterator(0); + while (it.next()) |entry| { + if (entry.hasCodepoint(cp, p_mode)) { return .{ .style = style, .idx = @intCast(i), }; } + + i += 1; } // Not found @@ -183,8 +184,8 @@ pub fn hasCodepoint( p_mode: PresentationMode, ) bool { const list = self.faces.get(index.style); - if (index.idx >= list.items.len) return false; - return list.items[index.idx].hasCodepoint(cp, p_mode); + if (index.idx >= list.count()) return false; + return list.at(index.idx).hasCodepoint(cp, p_mode); } /// Ensure we have an option for all styles in the collection, such @@ -210,7 +211,7 @@ pub fn completeStyles(self: *Collection, alloc: Allocator) !void { pub fn autoItalicize(self: *Collection, alloc: Allocator) !void { // If we have an italic font, do nothing. const italic_list = self.faces.getPtr(.italic); - if (italic_list.items.len > 0) return; + if (italic_list.count() > 0) return; // Not all font backends support auto-italicization. if (comptime !@hasDecl(Face, "italicize")) { @@ -224,10 +225,10 @@ pub fn autoItalicize(self: *Collection, alloc: Allocator) !void { // Our regular font. If we have no regular font we also do nothing. const regular = regular: { const list = self.faces.get(.regular); - if (list.items.len == 0) return; + if (list.count() == 0) return; // Find our first regular face that has text glyphs. - for (0..list.items.len) |i| { + for (0..list.count()) |i| { const face = try self.getFace(.{ .style = .regular, .idx = @intCast(i), @@ -277,8 +278,9 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void { // Resize all our faces that are loaded var it = self.faces.iterator(); - while (it.next()) |entry| { - for (entry.value.items) |*elem| switch (elem.*) { + while (it.next()) |array| { + var entry_it = array.value.iterator(0); + while (entry_it.next()) |entry| switch (entry.*) { .loaded, .fallback_loaded => |*f| try f.setSize( opts.faceOptions(), ), @@ -299,7 +301,12 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void { /// styles are typically loaded for a terminal session. The overhead per /// style even if it is not used or barely used is minimal given the /// small style count. -const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(Entry)); +/// +/// We use a segmented list because the entry values must be pointer-stable +/// to support the "alias" field in Entry. SegmentedList also lets us do +/// a prealloc which is great for performance since most happy path cases +/// do not use many font fallbacks. +const StyleArray = std.EnumArray(Style, std.SegmentedList(Entry, 4)); /// Load options are used to configure all the details a Collection /// needs to load deferred faces. @@ -520,7 +527,7 @@ test init { const testing = std.testing; const alloc = testing.allocator; - var c = try init(alloc); + var c = init(); defer c.deinit(alloc); } @@ -532,7 +539,7 @@ test "add full" { var lib = try Library.init(); defer lib.deinit(); - var c = try init(alloc); + var c = init(); defer c.deinit(alloc); for (0..Index.Special.start - 1) |_| { @@ -558,7 +565,7 @@ test "add deferred without loading options" { const testing = std.testing; const alloc = testing.allocator; - var c = try init(alloc); + var c = init(); defer c.deinit(alloc); try testing.expectError(error.DeferredLoadingUnavailable, c.add( @@ -578,7 +585,7 @@ test getFace { var lib = try Library.init(); defer lib.deinit(); - var c = try init(alloc); + var c = init(); defer c.deinit(alloc); const idx = try c.add(alloc, .regular, .{ .loaded = try Face.init( @@ -602,7 +609,7 @@ test getIndex { var lib = try Library.init(); defer lib.deinit(); - var c = try init(alloc); + var c = init(); defer c.deinit(alloc); _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( @@ -635,7 +642,7 @@ test autoItalicize { var lib = try Library.init(); defer lib.deinit(); - var c = try init(alloc); + var c = init(); defer c.deinit(alloc); c.load_options = .{ .library = lib }; @@ -658,7 +665,7 @@ test setSize { var lib = try Library.init(); defer lib.deinit(); - var c = try init(alloc); + var c = init(); defer c.deinit(alloc); c.load_options = .{ .library = lib }; @@ -681,7 +688,7 @@ test hasCodepoint { var lib = try Library.init(); defer lib.deinit(); - var c = try init(alloc); + var c = init(); defer c.deinit(alloc); c.load_options = .{ .library = lib }; @@ -705,7 +712,7 @@ test "hasCodepoint emoji default graphical" { var lib = try Library.init(); defer lib.deinit(); - var c = try init(alloc); + var c = init(); defer c.deinit(alloc); c.load_options = .{ .library = lib }; diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index c11c8a4ae..6c5fa43f2 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -325,7 +325,7 @@ const TestMode = enum { normal }; fn testGrid(mode: TestMode, alloc: Allocator, lib: Library) !SharedGrid { const testFont = @import("test.zig").fontRegular; - var c = try Collection.init(alloc); + var c = Collection.init(); c.load_options = .{ .library = lib }; switch (mode) { diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 857368a13..c576e08e1 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -167,7 +167,7 @@ fn collection( .metric_modifiers = key.metric_modifiers, }; - var c = try Collection.init(self.alloc); + var c = Collection.init(); errdefer c.deinit(self.alloc); c.load_options = load_options; diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 70823989e..de17a3fb6 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -453,22 +453,22 @@ pub const CoreText = struct { // here. if (desc.bold and desc.italic) { - const items = collection.faces.get(.bold_italic).items; - if (items.len > 0) { + const entries = collection.faces.get(.bold_italic); + if (entries.count() > 0) { break :original try collection.getFace(.{ .style = .bold_italic }); } } if (desc.bold) { - const items = collection.faces.get(.bold).items; - if (items.len > 0) { + const entries = collection.faces.get(.bold); + if (entries.count() > 0) { break :original try collection.getFace(.{ .style = .bold }); } } if (desc.italic) { - const items = collection.faces.get(.italic).items; - if (items.len > 0) { + const entries = collection.faces.get(.italic); + if (entries.count() > 0) { break :original try collection.getFace(.{ .style = .italic }); } } diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 27d80633c..afb41f596 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -1746,7 +1746,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { var lib = try Library.init(); errdefer lib.deinit(); - var c = try Collection.init(alloc); + var c = Collection.init(); c.load_options = .{ .library = lib }; // Setup group From 874caf29da6b2df4167f17f9be29477400f35eb3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Aug 2024 15:21:10 -0700 Subject: [PATCH 04/10] font: completeStyles --- src/font/Collection.zig | 137 ++++++++++++++++++++++++------------- src/font/SharedGridSet.zig | 14 +--- 2 files changed, 94 insertions(+), 57 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index f5e1880ed..1d6c3e2fe 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -117,28 +117,37 @@ pub fn getFace(self: *Collection, index: Index) !*Face { break :item item; }; - return switch (item.*) { + return self.getFaceFromEntry(item); +} + +/// Get the face from an entry. +/// +/// This entry must not be an alias. +fn getFaceFromEntry(self: *Collection, entry: *Entry) !*Face { + assert(entry.* != .alias); + + return switch (entry.*) { inline .deferred, .fallback_deferred => |*d, tag| deferred: { const opts = self.load_options orelse return error.DeferredLoadingUnavailable; const face = try d.load(opts.library, opts.faceOptions()); d.deinit(); - item.* = switch (tag) { + entry.* = switch (tag) { .deferred => .{ .loaded = face }, .fallback_deferred => .{ .fallback_loaded = face }, else => unreachable, }; break :deferred switch (tag) { - .deferred => &item.loaded, - .fallback_deferred => &item.fallback_loaded, + .deferred => &entry.loaded, + .fallback_deferred => &entry.fallback_loaded, else => unreachable, }; }, .loaded, .fallback_loaded => |*f| f, - // When setting `item` above, we ensure we don't end up with + // When setting `entry` above, we ensure we don't end up with // an alias. .alias => unreachable, }; @@ -188,51 +197,48 @@ pub fn hasCodepoint( return list.at(index.idx).hasCodepoint(cp, p_mode); } +pub const CompleteError = Allocator.Error || error{ + DefaultUnavailable, +}; + /// Ensure we have an option for all styles in the collection, such /// as italic and bold. /// /// This requires that a regular font face is already loaded. /// This is asserted. If a font style is missing, we will synthesize /// it if possible. Otherwise, we will use the regular font style. -pub fn completeStyles(self: *Collection, alloc: Allocator) !void { - const regular_list = self.faces.getPtr(.regular); - assert(regular_list.items.len > 0); +pub fn completeStyles(self: *Collection, alloc: Allocator) CompleteError!void { + // If every style has at least one entry then we're done! + // This is the most common case. + empty: { + var it = self.faces.iterator(); + while (it.next()) |entry| { + if (entry.value.count() == 0) break :empty; + } - // If we don't have bold, use the regular font. - const bold_list = self.faces.getPtr(.bold); - if (bold_list.items.len == 0) {} - - _ = alloc; -} - -/// Automatically create an italicized font from the regular -/// font face if we don't have one already. If we already have -/// an italicized font face, this does nothing. -pub fn autoItalicize(self: *Collection, alloc: Allocator) !void { - // If we have an italic font, do nothing. - const italic_list = self.faces.getPtr(.italic); - if (italic_list.count() > 0) return; - - // Not all font backends support auto-italicization. - if (comptime !@hasDecl(Face, "italicize")) { - log.warn( - "no italic font face available, italics will not render", - .{}, - ); return; } - // Our regular font. If we have no regular font we also do nothing. - const regular = regular: { - const list = self.faces.get(.regular); - if (list.count() == 0) return; + // Find the first regular face that has non-colorized text glyphs. + // This is the font we want to fallback to. This may not be index zero + // if a user configures something like an Emoji font first. + const regular_entry: *Entry = entry: { + const list = self.faces.getPtr(.regular); + assert(list.count() > 0); // Find our first regular face that has text glyphs. - for (0..list.count()) |i| { - const face = try self.getFace(.{ - .style = .regular, - .idx = @intCast(i), - }); + var it = list.iterator(0); + while (it.next()) |entry| { + // Load our face. If we fail to load it, we just skip it and + // continue on to try the next one. + const face = self.getFaceFromEntry(entry) catch |err| { + log.warn("error loading regular entry={d} err={}", .{ + it.index - 1, + err, + }); + + continue; + }; // We have two conditionals here. The color check is obvious: // we want to auto-italicize a normal text font. The second @@ -242,25 +248,62 @@ pub fn autoItalicize(self: *Collection, alloc: Allocator) !void { // it's a reasonable heuristic and the first case will match 99% // of the time. if (!face.hasColor() or face.glyphIndex('A') != null) { - break :regular face; + break :entry entry; } } - // No regular text face found. - return; + // No regular text face found. We can't provide any fallback. + return error.DefaultUnavailable; }; + // If we don't have italic, attempt to create a synthetic italic face. + // If we can't create a synthetic italic face, we'll just use the regular + // face for italic. + const italic_list = self.faces.getPtr(.italic); + if (italic_list.count() == 0) italic: { + const synthetic = self.syntheticItalic(regular_entry) catch |err| { + log.warn("failed to create synthetic italic, italic style will not be available err={}", .{err}); + try italic_list.append(alloc, .{ .alias = regular_entry }); + break :italic; + }; + + log.info("synthetic italic face created", .{}); + try italic_list.append(alloc, .{ .loaded = synthetic }); + } + + // If we don't have bold, use the regular font. + const bold_list = self.faces.getPtr(.bold); + if (bold_list.count() == 0) { + log.warn("bold style not available, using regular font", .{}); + try bold_list.append(alloc, .{ .alias = regular_entry }); + } + + // If we don't have bold italic, use the regular italic font. + const bold_italic_list = self.faces.getPtr(.bold_italic); + if (bold_italic_list.count() == 0) { + log.warn("bold italic style not available, using italic font", .{}); + try bold_italic_list.append(alloc, .{ .alias = italic_list.at(0) }); + } +} + +// Create an synthetic italic font face from the given entry and return it. +fn syntheticItalic(self: *Collection, entry: *Entry) !Face { + // Not all font backends support auto-italicization. + if (comptime !@hasDecl(Face, "italicize")) return error.SyntheticItalicUnavailable; + // We require loading options to auto-italicize. const opts = self.load_options orelse return error.DeferredLoadingUnavailable; // Try to italicize it. + const regular = try self.getFaceFromEntry(entry); const face = try regular.italicize(opts.faceOptions()); - try italic_list.append(alloc, .{ .loaded = face }); var buf: [256]u8 = undefined; if (face.name(&buf)) |name| { log.info("font auto-italicized: {s}", .{name}); } else |_| {} + + return face; } /// Update the size of all faces in the collection. This will @@ -632,9 +675,7 @@ test getIndex { } } -test autoItalicize { - if (comptime !@hasDecl(Face, "italicize")) return error.SkipZigTest; - +test completeStyles { const testing = std.testing; const alloc = testing.allocator; const testFont = @import("test.zig").fontRegular; @@ -652,9 +693,13 @@ test autoItalicize { .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, ) }); + try testing.expect(c.getIndex('A', .bold, .{ .any = {} }) == null); try testing.expect(c.getIndex('A', .italic, .{ .any = {} }) == null); - try c.autoItalicize(alloc); + try testing.expect(c.getIndex('A', .bold_italic, .{ .any = {} }) == null); + try c.completeStyles(alloc); + try testing.expect(c.getIndex('A', .bold, .{ .any = {} }) != null); try testing.expect(c.getIndex('A', .italic, .{ .any = {} }) != null); + try testing.expect(c.getIndex('A', .bold_italic, .{ .any = {} }) != null); } test setSize { diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index c576e08e1..bd55acb05 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -218,15 +218,6 @@ fn collection( load_options.faceOptions(), ) }, ); - _ = try c.add( - self.alloc, - .bold, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_bold_ttf, - load_options.faceOptions(), - ) }, - ); // On macOS, always search for and add the Apple Emoji font // as our preferred emoji font for fallback. We do this in case @@ -271,8 +262,9 @@ fn collection( ); } - // Auto-italicize - try c.autoItalicize(self.alloc); + // Complete our styles to ensure we have something to satisfy every + // possible style request. + try c.completeStyles(self.alloc); return c; } From 5befe75a1f6bfe6bae95414695d8db7d4e85c0f3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Aug 2024 15:30:47 -0700 Subject: [PATCH 05/10] font/harfbuzz: work with new collection API --- src/font/shaper/harfbuzz.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 7599a624d..8c04b759d 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -1207,7 +1207,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { var lib = try Library.init(); errdefer lib.deinit(); - var c = try Collection.init(alloc); + var c = Collection.init(); c.load_options = .{ .library = lib }; // Setup group From 47fb7f0115a3f9bc8198bd304e216ce392426580 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Aug 2024 19:18:07 -0700 Subject: [PATCH 06/10] font: Collection can't use segmentedlist prealloc --- src/font/Collection.zig | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 1d6c3e2fe..18a236629 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -346,10 +346,11 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void { /// small style count. /// /// We use a segmented list because the entry values must be pointer-stable -/// to support the "alias" field in Entry. SegmentedList also lets us do -/// a prealloc which is great for performance since most happy path cases -/// do not use many font fallbacks. -const StyleArray = std.EnumArray(Style, std.SegmentedList(Entry, 4)); +/// to support the "alias" field in Entry. +/// +/// WARNING: We cannot use any prealloc yet for the segmented list because +/// the collection is copied around by value and pointers aren't stable. +const StyleArray = std.EnumArray(Style, std.SegmentedList(Entry, 0)); /// Load options are used to configure all the details a Collection /// needs to load deferred faces. From ad22e068a290a511040a0a161ae50ffcdde0c068 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Aug 2024 19:48:15 -0700 Subject: [PATCH 07/10] font: use proper variation axes for non-default --- src/font/SharedGridSet.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index bd55acb05..3c1cc3cb9 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -481,7 +481,7 @@ pub const Key = struct { .style = style, .size = font_size.points, .bold = style == null, - .variations = config.@"font-variation".list.items, + .variations = config.@"font-variation-bold".list.items, }); } for (config.@"font-family-italic".list.items) |family| { @@ -491,7 +491,7 @@ pub const Key = struct { .style = style, .size = font_size.points, .italic = style == null, - .variations = config.@"font-variation".list.items, + .variations = config.@"font-variation-italic".list.items, }); } for (config.@"font-family-bold-italic".list.items) |family| { @@ -502,7 +502,7 @@ pub const Key = struct { .size = font_size.points, .bold = style == null, .italic = style == null, - .variations = config.@"font-variation".list.items, + .variations = config.@"font-variation-bold-italic".list.items, }); } From 963c843d1aa0167cfa31ca47787cb542f6bc87df Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Aug 2024 19:59:11 -0700 Subject: [PATCH 08/10] font: if variation is set for a style and style isn't found, retry reg --- src/font/SharedGridSet.zig | 62 ++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 3c1cc3cb9..ef8feebc3 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -187,20 +187,56 @@ fn collection( inline for (@typeInfo(Style).Enum.fields) |field| { const style = @field(Style, field.name); for (key.descriptorsForStyle(style)) |desc| { - var disco_it = try disco.discover(self.alloc, desc); - defer disco_it.deinit(); - if (try disco_it.next()) |face| { - log.info("font {s}: {s}", .{ - field.name, - try face.name(&name_buf), - }); + { + var disco_it = try disco.discover(self.alloc, desc); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font {s}: {s}", .{ + field.name, + try face.name(&name_buf), + }); - _ = try c.add( - self.alloc, - style, - .{ .deferred = face }, - ); - } else log.warn("font-family {s} not found: {s}", .{ + _ = try c.add( + self.alloc, + style, + .{ .deferred = face }, + ); + + continue; + } + } + + // If there are variation configurations and we didn't find + // the font, then we retry the discovery with all stylistic + // bits set to false. This is because some fonts may not + // set the stylistic bit in their table but still support + // axes to mimic the style. At the time of writing, Berkeley + // Mono Variable is like this. See #2140. + if (style != .regular and desc.variations.len > 0) { + var disco_it = try disco.discover(self.alloc, desc: { + var copy = desc; + copy.bold = false; + copy.italic = false; + break :desc copy; + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font {s}: {s}", .{ + field.name, + try face.name(&name_buf), + }); + + _ = try c.add( + self.alloc, + style, + .{ .deferred = face }, + ); + + continue; + } + } + + log.warn("font-family {s} not found: {s}", .{ field.name, desc.family.?, }); From db36a596fb649bb2ca217fd876bbf91c7335ed0a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Aug 2024 20:00:34 -0700 Subject: [PATCH 09/10] typos --- src/config/Config.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 160e76dfb..10b2e9524 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -55,7 +55,7 @@ const c = @cImport({ /// will be searched for stylistic variants. If a stylistic variant is not /// found, Ghostty will use the regular style. This prevents falling back to a /// different font family just to get a style such as bold. This also applies -/// if you explictly speciy a font family for a style. For example, if you +/// if you explicitly specify a font family for a style. For example, if you /// set `font-family-bold = FooBar` and "FooBar" cannot be found, Ghostty will /// use whatever font is set for `font-family` for the bold style. /// From 9941440f4714beb491cd511d5b5e0a446541afb6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Aug 2024 20:20:32 -0700 Subject: [PATCH 10/10] font: bold italic fallback has to avoid nested alias entry --- src/font/Collection.zig | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 18a236629..2a8368053 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -282,7 +282,25 @@ pub fn completeStyles(self: *Collection, alloc: Allocator) CompleteError!void { const bold_italic_list = self.faces.getPtr(.bold_italic); if (bold_italic_list.count() == 0) { log.warn("bold italic style not available, using italic font", .{}); - try bold_italic_list.append(alloc, .{ .alias = italic_list.at(0) }); + + // Nested alias isn't allowed so if the italic entry is an + // alias then we use the aliased entry. + const italic_entry = italic_list.at(0); + switch (italic_entry.*) { + .alias => |v| try bold_italic_list.append( + alloc, + .{ .alias = v }, + ), + + .loaded, + .fallback_loaded, + .deferred, + .fallback_deferred, + => try bold_italic_list.append( + alloc, + .{ .alias = italic_entry }, + ), + } } }