diff --git a/src/font/Collection.zig b/src/font/Collection.zig new file mode 100644 index 000000000..09e6c6c09 --- /dev/null +++ b/src/font/Collection.zig @@ -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 } }, + ) }, + )); +} diff --git a/src/font/Group.zig b/src/font/Group.zig index 76c69d0f8..c6ccd01ed 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -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(); diff --git a/src/font/GroupCacheSet.zig b/src/font/GroupCacheSet.zig index 51719306c..818e13e91 100644 --- a/src/font/GroupCacheSet.zig +++ b/src/font/GroupCacheSet.zig @@ -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,105 +112,29 @@ 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); - // Search for fonts - if (Discover != void) discover: { - const disco = try self.discover() orelse { - 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; - - 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), - }); - _ = try group.addFace(style, .{ .deferred = face }); - } else log.warn("font-family {s} not found: {s}", .{ - field.name, - desc.family.?, - }); - } - } - } - - // Our built-in font will be used as a backup - _ = try group.addFace( - .regular, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_ttf, - group.faceOptions(), - ) }, - ); - _ = try group.addFace( - .bold, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_bold_ttf, - group.faceOptions(), - ) }, - ); - // 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; - 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 }); - } - } - - // 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( - .regular, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_emoji_ttf, - group.faceOptions(), - ) }, - ); - _ = try group.addFace( - .regular, - .{ .fallback_loaded = try Face.init( - self.font_lib, - face_emoji_text_ttf, - group.faceOptions(), - ) }, - ); - } - log.info("font loading complete, any non-logged faces are using the built-in font", .{}); break :group group; }); @@ -218,6 +143,124 @@ pub fn groupRef( 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", + .{}, + ); + break :discover; + }; + + // A buffer we use to store the font names for logging. + var name_buf: [256]u8 = undefined; + + 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), + }); + + _ = try c.add( + self.alloc, + style, + .{ .deferred = face }, + ); + } else log.warn("font-family {s} not found: {s}", .{ + field.name, + desc.family.?, + }); + } + } + } + + // Our built-in font will be used as a backup + _ = try c.add( + self.alloc, + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_ttf, + opts, + ) }, + ); + _ = try c.add( + self.alloc, + .bold, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_bold_ttf, + opts, + ) }, + ); + + // 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 = 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 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 c.add( + self.alloc, + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_emoji_ttf, + opts, + ) }, + ); + _ = try c.add( + self.alloc, + .regular, + .{ .fallback_loaded = try Face.init( + self.font_lib, + face_emoji_text_ttf, + opts, + ) }, + ); + } + + return c; +} + /// Decrement the ref count for the given key. If the ref count is zero, /// the GroupCache will be deinitialized and removed from the map.j:w pub fn groupDeref(self: *GroupCacheSet, key: Key) void { diff --git a/src/font/main.zig b/src/font/main.zig index feff26bfe..91a620053 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -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");