font: completeStyles

This commit is contained in:
Mitchell Hashimoto
2024-08-23 15:21:10 -07:00
parent 1f3ccb2d66
commit 874caf29da
2 changed files with 94 additions and 57 deletions

View File

@ -117,28 +117,37 @@ pub fn getFace(self: *Collection, index: Index) !*Face {
break :item item; 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: { inline .deferred, .fallback_deferred => |*d, tag| deferred: {
const opts = self.load_options orelse const opts = self.load_options orelse
return error.DeferredLoadingUnavailable; return error.DeferredLoadingUnavailable;
const face = try d.load(opts.library, opts.faceOptions()); const face = try d.load(opts.library, opts.faceOptions());
d.deinit(); d.deinit();
item.* = switch (tag) { entry.* = switch (tag) {
.deferred => .{ .loaded = face }, .deferred => .{ .loaded = face },
.fallback_deferred => .{ .fallback_loaded = face }, .fallback_deferred => .{ .fallback_loaded = face },
else => unreachable, else => unreachable,
}; };
break :deferred switch (tag) { break :deferred switch (tag) {
.deferred => &item.loaded, .deferred => &entry.loaded,
.fallback_deferred => &item.fallback_loaded, .fallback_deferred => &entry.fallback_loaded,
else => unreachable, else => unreachable,
}; };
}, },
.loaded, .fallback_loaded => |*f| f, .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. // an alias.
.alias => unreachable, .alias => unreachable,
}; };
@ -188,51 +197,48 @@ pub fn hasCodepoint(
return list.at(index.idx).hasCodepoint(cp, p_mode); 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 /// Ensure we have an option for all styles in the collection, such
/// as italic and bold. /// as italic and bold.
/// ///
/// This requires that a regular font face is already loaded. /// This requires that a regular font face is already loaded.
/// This is asserted. If a font style is missing, we will synthesize /// This is asserted. If a font style is missing, we will synthesize
/// it if possible. Otherwise, we will use the regular font style. /// it if possible. Otherwise, we will use the regular font style.
pub fn completeStyles(self: *Collection, alloc: Allocator) !void { pub fn completeStyles(self: *Collection, alloc: Allocator) CompleteError!void {
const regular_list = self.faces.getPtr(.regular); // If every style has at least one entry then we're done!
assert(regular_list.items.len > 0); // 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; return;
} }
// Our regular font. If we have no regular font we also do nothing. // Find the first regular face that has non-colorized text glyphs.
const regular = regular: { // This is the font we want to fallback to. This may not be index zero
const list = self.faces.get(.regular); // if a user configures something like an Emoji font first.
if (list.count() == 0) return; const regular_entry: *Entry = entry: {
const list = self.faces.getPtr(.regular);
assert(list.count() > 0);
// Find our first regular face that has text glyphs. // Find our first regular face that has text glyphs.
for (0..list.count()) |i| { var it = list.iterator(0);
const face = try self.getFace(.{ while (it.next()) |entry| {
.style = .regular, // Load our face. If we fail to load it, we just skip it and
.idx = @intCast(i), // 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 have two conditionals here. The color check is obvious:
// we want to auto-italicize a normal text font. The second // 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% // it's a reasonable heuristic and the first case will match 99%
// of the time. // of the time.
if (!face.hasColor() or face.glyphIndex('A') != null) { if (!face.hasColor() or face.glyphIndex('A') != null) {
break :regular face; break :entry entry;
} }
} }
// No regular text face found. // No regular text face found. We can't provide any fallback.
return; 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. // We require loading options to auto-italicize.
const opts = self.load_options orelse return error.DeferredLoadingUnavailable; const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
// Try to italicize it. // Try to italicize it.
const regular = try self.getFaceFromEntry(entry);
const face = try regular.italicize(opts.faceOptions()); const face = try regular.italicize(opts.faceOptions());
try italic_list.append(alloc, .{ .loaded = face });
var buf: [256]u8 = undefined; var buf: [256]u8 = undefined;
if (face.name(&buf)) |name| { if (face.name(&buf)) |name| {
log.info("font auto-italicized: {s}", .{name}); log.info("font auto-italicized: {s}", .{name});
} else |_| {} } else |_| {}
return face;
} }
/// Update the size of all faces in the collection. This will /// Update the size of all faces in the collection. This will
@ -632,9 +675,7 @@ test getIndex {
} }
} }
test autoItalicize { test completeStyles {
if (comptime !@hasDecl(Face, "italicize")) return error.SkipZigTest;
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular; const testFont = @import("test.zig").fontRegular;
@ -652,9 +693,13 @@ test autoItalicize {
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, .{ .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 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', .italic, .{ .any = {} }) != null);
try testing.expect(c.getIndex('A', .bold_italic, .{ .any = {} }) != null);
} }
test setSize { test setSize {

View File

@ -218,15 +218,6 @@ fn collection(
load_options.faceOptions(), 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 // On macOS, always search for and add the Apple Emoji font
// as our preferred emoji font for fallback. We do this in case // as our preferred emoji font for fallback. We do this in case
@ -271,8 +262,9 @@ fn collection(
); );
} }
// Auto-italicize // Complete our styles to ensure we have something to satisfy every
try c.autoItalicize(self.alloc); // possible style request.
try c.completeStyles(self.alloc);
return c; return c;
} }