mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
font: completeStyles
This commit is contained in:
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user