font: [broken] working on extracting Collection from Group

This commit is contained in:
Mitchell Hashimoto
2024-04-02 10:24:56 -07:00
parent 7b428367df
commit 72d59956d5
4 changed files with 409 additions and 205 deletions

264
src/font/Collection.zig Normal file
View File

@ -0,0 +1,264 @@
//! A font collection is a list of faces of different styles. The list is
//! ordered by priority (per style). All fonts in a collection share the same
//! size so they can be used interchangeably in cases a glyph is missing in one
//! and present in another.
const Collection = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const font = @import("main.zig");
const DeferredFace = font.DeferredFace;
const Face = font.Face;
const Library = font.Library;
const Presentation = font.Presentation;
const Style = font.Style;
/// The available faces we have. This shouldn't be modified manually.
/// Instead, use the functions available on Collection.
faces: StyleArray,
/// Initialize an empty collection.
pub fn init(alloc: Allocator) !Collection {
// Initialize our styles array, preallocating some space that is
// likely to be used.
var faces = StyleArray.initFill(.{});
for (&faces.values) |*list| try list.ensureTotalCapacityPrecise(alloc, 2);
return .{ .faces = faces };
}
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);
}
}
pub const AddError = Allocator.Error || error{
CollectionFull,
};
/// Add a face to the collection for the given style. This face will be added
/// next in priority if others exist already, i.e. it'll be the _last_ to be
/// searched for a glyph in that list.
///
/// The collection takes ownership of the face. The face will be deallocated
/// when the collection is deallocated.
///
/// If a loaded face is added to the collection, it should be the same
/// size as all the other faces in the collection. This function will not
/// verify or modify the size until the size of the entire collection is
/// changed.
pub fn add(
self: *Collection,
alloc: Allocator,
style: Style,
face: Entry,
) AddError!Index {
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)
return error.CollectionFull;
const idx = list.items.len;
try list.append(alloc, face);
return .{ .style = style, .idx = @intCast(idx) };
}
/// Packed array of all Style enum cases mapped to a growable list of faces.
///
/// We use this data structure because there aren't many styles and all
/// 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));
/// A entry in a collection can be deferred or loaded. A deferred face
/// is not yet fully loaded and only represents the font descriptor
/// and usually uses less resources. A loaded face is fully parsed,
/// ready to rasterize, and usually uses more resources than a
/// deferred version.
///
/// A face can also be a "fallback" variant that is still either
/// deferred or loaded. Today, there is only one difference between
/// fallback and non-fallback (or "explicit") faces: the handling
/// of emoji presentation.
///
/// For explicit faces, when an explicit emoji presentation is
/// not requested, we will use any glyph for that codepoint found
/// even if the font presentation does not match the UCD
/// (Unicode Character Database) value. When an explicit presentation
/// is requested (via either VS15/V16), that is always honored.
/// The reason we do this is because we assume that if a user
/// explicitly chosen a font face (hence it is "explicit" and
/// not "fallback"), they want to use any glyphs possible within that
/// font face. Fallback fonts on the other hand are picked as a
/// last resort, so we should prefer exactness if possible.
pub const Entry = union(enum) {
deferred: DeferredFace, // Not loaded
loaded: Face, // Loaded, explicit use
// The same as deferred/loaded but fallback font semantics (see large
// comment above Entry).
fallback_deferred: DeferredFace,
fallback_loaded: Face,
pub fn deinit(self: *Entry) void {
switch (self.*) {
inline .deferred,
.loaded,
.fallback_deferred,
.fallback_loaded,
=> |*v| v.deinit(),
}
}
/// True if this face satisfies the given codepoint and presentation.
fn hasCodepoint(self: Entry, cp: u32, p_mode: PresentationMode) bool {
return switch (self) {
// Non-fallback fonts require explicit presentation matching but
// otherwise don't care about presentation
.deferred => |v| switch (p_mode) {
.explicit => |p| v.hasCodepoint(cp, p),
.default, .any => v.hasCodepoint(cp, null),
},
.loaded => |face| switch (p_mode) {
.explicit => |p| face.presentation == p and face.glyphIndex(cp) != null,
.default, .any => face.glyphIndex(cp) != null,
},
// Fallback fonts require exact presentation matching.
.fallback_deferred => |v| switch (p_mode) {
.explicit, .default => |p| v.hasCodepoint(cp, p),
.any => v.hasCodepoint(cp, null),
},
.fallback_loaded => |face| switch (p_mode) {
.explicit,
.default,
=> |p| face.presentation == p and face.glyphIndex(cp) != null,
.any => face.glyphIndex(cp) != null,
},
};
}
};
/// The requested presentation for a codepoint.
pub const PresentationMode = union(enum) {
/// The codepoint has an explicit presentation that is required,
/// i.e. VS15/V16.
explicit: Presentation,
/// The codepoint has no explicit presentation and we should use
/// the presentation from the UCd.
default: Presentation,
/// The codepoint can be any presentation.
any: void,
};
/// This represents a specific font in the collection.
///
/// The backing size of this packed struct represents the total number
/// of possible usable fonts in a collection. And the number of bits
/// used for the index and not the style represents the total number
/// of possible usable fonts for a given style.
///
/// The goal is to keep the size of this struct as small as practical. We
/// accept the limitations that this imposes so long as they're reasonable.
/// At the time of writing this comment, this is a 16-bit struct with 13
/// bits used for the index, supporting up to 8192 fonts per style. This
/// seems more than reasonable. There are synthetic scenarios where this
/// could be a limitation but I can't think of any that are practical.
///
/// If you somehow need more fonts per style, you can increase the size of
/// the Backing type and everything should just work fine.
pub const Index = packed struct(Index.Backing) {
const Backing = u16;
const backing_bits = @typeInfo(Backing).Int.bits;
/// The number of bits we use for the index.
const idx_bits = backing_bits - @typeInfo(@typeInfo(Style).Enum.tag_type).Int.bits;
pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } });
/// The special-case fonts that we support.
pub const Special = enum(IndexInt) {
// We start all special fonts at this index so they can be detected.
pub const start = std.math.maxInt(IndexInt);
/// Sprite drawing, this is rendered JIT using 2D graphics APIs.
sprite = start,
};
style: Style = .regular,
idx: IndexInt = 0,
/// Initialize a special font index.
pub fn initSpecial(v: Special) Index {
return .{ .style = .regular, .idx = @intFromEnum(v) };
}
/// Convert to int
pub fn int(self: Index) Backing {
return @bitCast(self);
}
/// Returns true if this is a "special" index which doesn't map to
/// a real font face. We can still render it but there is no face for
/// this font.
pub fn special(self: Index) ?Special {
if (self.idx < Special.start) return null;
return @enumFromInt(self.idx);
}
test {
// We never want to take up more than a byte since font indexes are
// everywhere so if we increase the size of this we'll dramatically
// increase our memory usage.
try std.testing.expectEqual(@sizeOf(Backing), @sizeOf(Index));
// Just so we're aware when this changes. The current maximum number
// of fonts for a style is 13 bits or 8192 fonts.
try std.testing.expectEqual(13, idx_bits);
}
};
test init {
const testing = std.testing;
const alloc = testing.allocator;
var c = try init(alloc);
defer c.deinit(alloc);
}
test "add full" {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
var lib = try Library.init();
defer lib.deinit();
var c = try init(alloc);
defer c.deinit(alloc);
for (0..Index.Special.start - 1) |_| {
_ = try c.add(alloc, .regular, .{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12 } },
) });
}
try testing.expectError(error.CollectionFull, c.add(
alloc,
.regular,
.{ .loaded = try Face.init(
lib,
testFont,
.{ .size = .{ .points = 12 } },
) },
));
}

View File

@ -16,6 +16,7 @@ const Allocator = std.mem.Allocator;
const ziglyph = @import("ziglyph");
const font = @import("main.zig");
const Collection = @import("main.zig").Collection;
const DeferredFace = @import("main.zig").DeferredFace;
const Face = @import("main.zig").Face;
const Library = @import("main.zig").Library;
@ -27,13 +28,6 @@ const quirks = @import("../quirks.zig");
const log = std.log.scoped(.font_group);
/// Packed array to map our styles to a set of faces.
// Note: this is not the most efficient way to store these, but there is
// usually only one font group for the entire process so this isn't the
// most important memory efficiency we can look for. This is totally opaque
// to the user so we can change this later.
const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(GroupFace));
/// Packed array of booleans to indicate if a style is enabled or not.
pub const StyleStatus = std.EnumArray(Style, bool);
@ -92,7 +86,7 @@ metric_modifiers: ?font.face.Metrics.ModifierSet = null,
/// The available faces we have. This shouldn't be modified manually.
/// Instead, use the functions available on Group.
faces: StyleArray,
faces: Collection,
/// The set of statuses and whether they're enabled or not. This defaults
/// to true. This can be changed at runtime with no ill effect. If you
@ -119,105 +113,25 @@ descriptor_cache: DescriptorCache = .{},
/// terminal rendering will look wrong.
sprite: ?font.sprite.Face = null,
/// A face in a group can be deferred or loaded. A deferred face
/// is not yet fully loaded and only represents the font descriptor
/// and usually uses less resources. A loaded face is fully parsed,
/// ready to rasterize, and usually uses more resources than a
/// deferred version.
///
/// A face can also be a "fallback" variant that is still either
/// deferred or loaded. Today, there is only one different between
/// fallback and non-fallback (or "explicit") faces: the handling
/// of emoji presentation.
///
/// For explicit faces, when an explicit emoji presentation is
/// not requested, we will use any glyph for that codepoint found
/// even if the font presentation does not match the UCD
/// (Unicode Character Database) value. When an explicit presentation
/// is requested (via either VS15/V16), that is always honored.
/// The reason we do this is because we assume that if a user
/// explicitly chosen a font face (hence it is "explicit" and
/// not "fallback"), they want to use any glyphs possible within that
/// font face. Fallback fonts on the other hand are picked as a
/// last resort, so we should prefer exactness if possible.
pub const GroupFace = union(enum) {
deferred: DeferredFace, // Not loaded
loaded: Face, // Loaded, explicit use
// The same as deferred/loaded but fallback font semantics (see large
// comment above GroupFace).
fallback_deferred: DeferredFace,
fallback_loaded: Face,
pub fn deinit(self: *GroupFace) void {
switch (self.*) {
inline .deferred,
.loaded,
.fallback_deferred,
.fallback_loaded,
=> |*v| v.deinit(),
}
}
/// True if this face satisfies the given codepoint and presentation.
fn hasCodepoint(self: GroupFace, cp: u32, p_mode: PresentationMode) bool {
return switch (self) {
// Non-fallback fonts require explicit presentation matching but
// otherwise don't care about presentation
.deferred => |v| switch (p_mode) {
.explicit => |p| v.hasCodepoint(cp, p),
.default, .any => v.hasCodepoint(cp, null),
},
.loaded => |face| switch (p_mode) {
.explicit => |p| face.presentation == p and face.glyphIndex(cp) != null,
.default, .any => face.glyphIndex(cp) != null,
},
// Fallback fonts require exact presentation matching.
.fallback_deferred => |v| switch (p_mode) {
.explicit, .default => |p| v.hasCodepoint(cp, p),
.any => v.hasCodepoint(cp, null),
},
.fallback_loaded => |face| switch (p_mode) {
.explicit,
.default,
=> |p| face.presentation == p and face.glyphIndex(cp) != null,
.any => face.glyphIndex(cp) != null,
},
};
}
};
/// Initializes an empty group. This is not useful until faces are added
/// and finalizeInit is called.
pub fn init(
alloc: Allocator,
lib: Library,
size: font.face.DesiredSize,
collection: Collection,
) !Group {
var result = Group{ .alloc = alloc, .lib = lib, .size = size, .faces = undefined };
// Initialize all our styles to initially sized lists.
var i: usize = 0;
while (i < StyleArray.len) : (i += 1) {
result.faces.values[i] = .{};
try result.faces.values[i].ensureTotalCapacityPrecise(alloc, 2);
}
return result;
return .{
.alloc = alloc,
.lib = lib,
.size = size,
.faces = collection,
};
}
pub fn deinit(self: *Group) void {
{
var it = self.faces.iterator();
while (it.next()) |entry| {
for (entry.value.items) |*item| item.deinit();
entry.value.deinit(self.alloc);
}
}
self.faces.deinit(self.alloc);
if (self.metric_modifiers) |*v| v.deinit(self.alloc);
self.descriptor_cache.deinit(self.alloc);
}
@ -230,23 +144,6 @@ pub fn faceOptions(self: *const Group) font.face.Options {
};
}
/// Add a face to the list for the given style. This face will be added as
/// next in priority if others exist already, i.e. it'll be the _last_ to be
/// searched for a glyph in that list.
///
/// The group takes ownership of the face. The face will be deallocated when
/// the group is deallocated.
pub fn addFace(self: *Group, style: Style, face: GroupFace) !FontIndex {
const list = self.faces.getPtr(style);
// We have some special indexes so we must never pass those.
if (list.items.len >= FontIndex.Special.start - 1) return error.GroupFull;
const idx = list.items.len;
try list.append(self.alloc, face);
return .{ .style = style, .idx = @intCast(idx) };
}
/// This will automatically create an italicized font from the regular
/// font face if we don't have any italicized fonts.
pub fn italicize(self: *Group) !void {
@ -472,7 +369,9 @@ pub fn indexForCodepoint(
// Discovery is supposed to only return faces that have our
// codepoint but we can't search presentation in discovery so
// we have to check it here.
const face: GroupFace = .{ .fallback_deferred = deferred_face };
const face: Collection.Entry = .{
.fallback_deferred = deferred_face,
};
if (!face.hasCodepoint(cp, p_mode)) {
deferred_face.deinit();
continue;
@ -895,9 +794,6 @@ test "face count limit" {
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
var atlas_greyscale = try font.Atlas.init(alloc, 512, .greyscale);
defer atlas_greyscale.deinit(alloc);
var lib = try Library.init();
defer lib.deinit();

View File

@ -19,6 +19,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const fontpkg = @import("main.zig");
const Collection = fontpkg.Collection;
const Discover = fontpkg.Discover;
const Style = fontpkg.Style;
const Library = fontpkg.Library;
@ -111,24 +112,61 @@ pub fn groupRef(
.ref = 1,
};
// Build our font face collection that we'll use to initialize
// the Group.
cache.* = try GroupCache.init(self.alloc, group: {
var group = try Group.init(self.alloc, self.font_lib, font_size);
var group = try Group.init(
self.alloc,
self.font_lib,
font_size,
try self.collection(&key, font_size),
);
errdefer group.deinit();
group.metric_modifiers = key.metric_modifiers;
group.codepoint_map = key.codepoint_map;
group.discover = try self.discover();
// Set our styles
group.styles.set(.bold, config.@"font-style-bold" != .false);
group.styles.set(.italic, config.@"font-style-italic" != .false);
group.styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
// Auto-italicize if we have to.
try group.italicize();
log.info("font loading complete, any non-logged faces are using the built-in font", .{});
break :group group;
});
errdefer cache.deinit(self.alloc);
return .{ gop.key_ptr.*, gop.value_ptr.cache };
}
/// Builds the Collection for the given configuration key and
/// initial font size.
fn collection(
self: *GroupCacheSet,
key: *const Key,
size: DesiredSize,
) !Collection {
var c = try Collection.init(self.alloc);
errdefer c.deinit(self.alloc);
const opts: fontpkg.face.Options = .{
.size = size,
.metric_modifiers = &key.metric_modifiers,
};
// Search for fonts
if (Discover != void) discover: {
const disco = try self.discover() orelse {
log.warn("font discovery not available, cannot search for fonts", .{});
log.warn(
"font discovery not available, cannot search for fonts",
.{},
);
break :discover;
};
group.discover = disco;
// A buffer we use to store the font names for logging.
var name_buf: [256]u8 = undefined;
@ -143,7 +181,12 @@ pub fn groupRef(
field.name,
try face.name(&name_buf),
});
_ = try group.addFace(style, .{ .deferred = face });
_ = try c.add(
self.alloc,
style,
.{ .deferred = face },
);
} else log.warn("font-family {s} not found: {s}", .{
field.name,
desc.family.?,
@ -153,69 +196,69 @@ pub fn groupRef(
}
// Our built-in font will be used as a backup
_ = try group.addFace(
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_loaded = try Face.init(
self.font_lib,
face_ttf,
group.faceOptions(),
opts,
) },
);
_ = try group.addFace(
_ = try c.add(
self.alloc,
.bold,
.{ .fallback_loaded = try Face.init(
self.font_lib,
face_bold_ttf,
group.faceOptions(),
opts,
) },
);
// Auto-italicize if we have to.
try group.italicize();
// On macOS, always search for and add the Apple Emoji font
// as our preferred emoji font for fallback. We do this in case
// people add other emoji fonts to their system, we always want to
// prefer the official one. Users can override this by explicitly
// specifying a font-family for emoji.
if (comptime builtin.target.isDarwin()) apple_emoji: {
const disco = group.discover orelse break :apple_emoji;
const disco = try self.discover() orelse break :apple_emoji;
var disco_it = try disco.discover(self.alloc, .{
.family = "Apple Color Emoji",
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
_ = try group.addFace(.regular, .{ .fallback_deferred = face });
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_deferred = face },
);
}
}
// Emoji fallback. We don't include this on Mac since Mac is expected
// to always have the Apple Emoji available on the system.
if (comptime !builtin.target.isDarwin() or Discover == void) {
_ = try group.addFace(
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_loaded = try Face.init(
self.font_lib,
face_emoji_ttf,
group.faceOptions(),
opts,
) },
);
_ = try group.addFace(
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_loaded = try Face.init(
self.font_lib,
face_emoji_text_ttf,
group.faceOptions(),
opts,
) },
);
}
log.info("font loading complete, any non-logged faces are using the built-in font", .{});
break :group group;
});
errdefer cache.deinit(self.alloc);
return .{ gop.key_ptr.*, gop.value_ptr.cache };
return c;
}
/// Decrement the ref count for the given key. If the ref count is zero,

View File

@ -6,6 +6,7 @@ pub const Atlas = @import("Atlas.zig");
pub const discovery = @import("discovery.zig");
pub const face = @import("face.zig");
pub const CodepointMap = @import("CodepointMap.zig");
pub const Collection = @import("Collection.zig");
pub const DeferredFace = @import("DeferredFace.zig");
pub const Face = face.Face;
pub const Group = @import("Group.zig");