diff options
Diffstat (limited to 'vendor/libghostty-vt/src/font/CodepointResolver.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/font/CodepointResolver.zig | 562 |
1 files changed, 562 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/font/CodepointResolver.zig b/vendor/libghostty-vt/src/font/CodepointResolver.zig new file mode 100644 index 00000000..a4f13c29 --- /dev/null +++ b/vendor/libghostty-vt/src/font/CodepointResolver.zig @@ -0,0 +1,562 @@ +//! CodepointResolver maps a codepoint to a font. It is more dynamic +//! than "Collection" since it supports mapping codepoint ranges to +//! specific fonts, searching for fallback fonts, and more. +//! +//! To initialize the codepoint resolver, manually initialize using +//! Zig initialization syntax: .{}-style. Set the fields you want set, +//! and begin using the resolver. +//! +//! Deinit must still be called on the resolver to free any memory +//! allocated during use. All functions that take allocators should use +//! the same allocator. +const CodepointResolver = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const uucode = @import("uucode"); +const font = @import("main.zig"); +const Atlas = font.Atlas; +const CodepointMap = font.CodepointMap; +const Collection = font.Collection; +const Discover = font.Discover; +const DiscoveryDescriptor = font.discovery.Descriptor; +const Face = font.Face; +const Glyph = font.Glyph; +const Library = font.Library; +const Presentation = font.Presentation; +const RenderOptions = font.face.RenderOptions; +const SpriteFace = font.SpriteFace; +const Style = font.Style; + +const log = std.log.scoped(.font_codepoint_resolver); + +/// The underlying collection of fonts. This will be modified as +/// new fonts are found via the resolver. The resolver takes ownership +/// of the collection and will deinit it when it is deinitialized. +collection: 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. +styles: StyleStatus = .initFill(true), + +/// If discovery is available, we'll look up fonts where we can't find +/// the codepoint. This can be set after initialization. +discover: ?*Discover = null, + +/// A map of codepoints to font requests for codepoint-level overrides. +/// The memory associated with the map is owned by the caller and is not +/// modified or freed by Group. +codepoint_map: ?CodepointMap = null, + +/// The descriptor cache is used to cache the descriptor to font face +/// mapping for codepoint maps. +descriptor_cache: DescriptorCache = .{}, + +/// Set this to a non-null value to enable sprite glyph drawing. If this +/// isn't enabled we'll just fall through to trying to use regular fonts +/// to render sprite glyphs. But more than likely, if this isn't set then +/// terminal rendering will look wrong. +sprite: ?SpriteFace = null, + +pub fn deinit(self: *CodepointResolver, alloc: Allocator) void { + self.collection.deinit(alloc); + self.descriptor_cache.deinit(alloc); +} + +/// Looks up the font that should be used for a specific codepoint. +/// The font index is valid as long as font faces aren't removed. This +/// isn't cached; it is expected that downstream users handle caching if +/// that is important. +/// +/// Optionally, a presentation format can be specified. This presentation +/// format will be preferred but if it can't be found in this format, +/// any format will be accepted. If presentation is null, the UCD +/// (Unicode Character Database) will be used to determine the default +/// presentation for the codepoint. +/// a code point. +/// +/// An allocator is required because certain functionality (codepoint +/// mapping, fallback fonts, etc.) may require memory allocation. Curiously, +/// this function cannot error! If an error occurs for any reason, including +/// memory allocation, the associated functionality is ignored and the +/// resolver attempts to use a different method to satisfy the codepoint. +/// This behavior is intentional to make the resolver apply best-effort +/// logic to satisfy the codepoint since its better to render something +/// than nothing. +/// +/// This logic is relatively complex so the exact algorithm is documented +/// here. If this gets out of sync with the code, ask questions. +/// +/// 1. If a font style is requested that is disabled (i.e. bold), +/// we start over with the regular font style. The regular font style +/// cannot be disabled, but it can be replaced with a stylized font +/// face. +/// +/// 2. If there is a codepoint override for the codepoint, we satisfy +/// that requirement if we can, no matter what style or presentation. +/// +/// 3. If this is a sprite codepoint (such as an underline), then the +/// sprite font always is the result. +/// +/// 4. If the exact style and presentation request can be satisfied by +/// one of our loaded fonts, we return that value. We search loaded +/// fonts in the order they're added to the group, so the caller must +/// set the priority order. +/// +/// 5. If the style isn't regular, we restart this process at this point +/// but with the regular style. This lets us fall back to regular with +/// our loaded fonts before trying a fallback. We'd rather show a regular +/// version of a codepoint from a loaded font than find a new font in +/// the correct style because styles in other fonts often change +/// metrics like glyph widths. +/// +/// 6. If the style is regular, and font discovery is enabled, we look +/// for a fallback font to satisfy our request. +/// +/// 7. Finally, as a last resort, we fall back to restarting this whole +/// process with a regular font face satisfying ANY presentation for +/// the codepoint. If this fails, we return null. +/// +pub fn getIndex( + self: *CodepointResolver, + alloc: Allocator, + cp: u32, + style: Style, + p: ?Presentation, +) ?Collection.Index { + // If we've disabled a font style, then fall back to regular. + if (style != .regular and !self.styles.get(style)) { + return self.getIndex(alloc, cp, .regular, p); + } + + // Codepoint overrides. + if (self.getIndexCodepointOverride(alloc, cp)) |idx_| { + if (idx_) |idx| return idx; + } else |err| { + log.warn("codepoint override failed codepoint={} err={}", .{ cp, err }); + } + + // If we have sprite drawing enabled, check if our sprite face can + // handle this. + if (self.sprite) |sprite| { + if (sprite.hasCodepoint(cp, p)) { + return .initSpecial(.sprite); + } + } + + // Build our presentation mode. If we don't have an explicit presentation + // given then we use the UCD (Unicode Character Database) to determine + // the default presentation. Note there is some inefficiency here because + // we'll do this multiple times if we recurse, but this is a cached function + // call higher up (GroupCache) so this should be rare. + const p_mode: Collection.PresentationMode = if (p) |v| .{ .explicit = v } else .{ + .default = if (uucode.get(.is_emoji_presentation, @intCast(cp))) + .emoji + else + .text, + }; + + // If we can find the exact value, then return that. + if (self.collection.getIndex(cp, style, p_mode)) |value| return value; + + // If we're not a regular font style, try looking for a regular font + // that will satisfy this request. Blindly looking for unmatched styled + // fonts to satisfy one codepoint results in some ugly rendering. + if (style != .regular) { + if (self.getIndex(alloc, cp, .regular, p)) |value| return value; + } + + // If we are regular, try looking for a fallback using discovery. + if (style == .regular and font.Discover != void) { + log.debug("searching for a fallback font for cp={X}", .{cp}); + if (self.discover) |disco| discover: { + const load_opts = self.collection.load_options orelse + break :discover; + var disco_it = disco.discoverFallback(alloc, &self.collection, .{ + .codepoint = cp, + .size = load_opts.size.points, + .bold = style == .bold or style == .bold_italic, + .italic = style == .italic or style == .bold_italic, + .monospace = false, + }) catch break :discover; + defer disco_it.deinit(); + + while (true) { + var deferred_face = (disco_it.next() catch |err| { + log.warn("fallback search failed with error err={}", .{err}); + break; + }) orelse break; + + // 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: Collection.Entry = .{ + .face = .{ .deferred = deferred_face }, + .fallback = true, + }; + if (!face.hasCodepoint(cp, p_mode)) { + deferred_face.deinit(); + continue; + } + + var buf: [256]u8 = undefined; + log.info("found codepoint 0x{X} in fallback face={s}", .{ + cp, + deferred_face.name(&buf) catch "<error>", + }); + return self.collection.addDeferred(alloc, deferred_face, .{ + .style = style, + .fallback = true, + .size_adjustment = font.default_fallback_adjustment, + }) catch { + deferred_face.deinit(); + break :discover; + }; + } + + log.debug("no fallback face found for cp={X}", .{cp}); + } + } + + // If this is regular with any matching presentation, then we are done + // there is nothing more we can do. Otherwise we fall through and do + // an any presentation search. + if (style == .regular and p_mode == .any) return null; + + // For non-regular fonts, we fall back to regular with any presentation + return self.collection.getIndex(cp, .regular, .{ .any = {} }); +} + +/// Checks if the codepoint is in the map of codepoint overrides, +/// finds the override font, and returns it. +fn getIndexCodepointOverride( + self: *CodepointResolver, + alloc: Allocator, + cp: u32, +) !?Collection.Index { + // If discovery is disabled then we can't do codepoint overrides + // since the override is based on discovery to find the font. + if (comptime font.Discover == void) return null; + + // Get our codepoint map. If we have no map set then we have no + // codepoint overrides and we're done. + const map = self.codepoint_map orelse return null; + + // If we have a codepoint too large or isn't in the map, then we + // don't have an override. The map returns a descriptor that can be + // used with font discovery to search for a matching font. + const cp_u21 = std.math.cast(u21, cp) orelse return null; + const desc = map.get(cp_u21) orelse return null; + + // Fast path: the descriptor is already loaded. This means that we + // already did the search before and we have an exact font for this + // codepoint. + const idx_: ?Collection.Index = self.descriptor_cache.get(desc) orelse idx: { + // Slow path: we have to find this descriptor and load the font + const discover = self.discover orelse return null; + var disco_it = try discover.discover(alloc, desc); + defer disco_it.deinit(); + + const face = (try disco_it.next()) orelse { + log.warn( + "font lookup for codepoint map failed codepoint={} err=FontNotFound", + .{cp}, + ); + + // Add null to the cache so we don't do a lookup again later. + try self.descriptor_cache.put(alloc, desc, null); + return null; + }; + + // Add the font to our list of fonts so we can get an index for it, + // and ensure the index is stored in the descriptor cache for next time. + const idx = try self.collection.addDeferred(alloc, face, .{ + .style = .regular, + .fallback = false, + .size_adjustment = font.default_fallback_adjustment, + }); + try self.descriptor_cache.put(alloc, desc, idx); + + break :idx idx; + }; + + // The descriptor cache will populate null if the descriptor is not found + // to avoid expensive discoveries later, so if it is null then we already + // searched and found nothing. + const idx = idx_ orelse return null; + + // We need to verify that this index has the codepoint we want. + if (self.collection.hasCodepoint(idx, cp, .{ .any = {} })) { + log.debug("codepoint override based on config codepoint={} family={s}", .{ + cp, + desc.family orelse "", + }); + + return idx; + } + + return null; +} + +/// Returns the presentation for a specific font index. This is useful for +/// determining what atlas is needed. +pub fn getPresentation( + self: *CodepointResolver, + index: Collection.Index, + glyph_index: u32, +) !Presentation { + if (index.special()) |sp| return switch (sp) { + .sprite => .text, + }; + + const face = try self.collection.getFace(index); + return if (face.isColorGlyph(glyph_index)) .emoji else .text; +} + +/// Render a glyph by glyph index into the given font atlas and return +/// metadata about it. +/// +/// This performs no caching, it is up to the caller to cache calls to this +/// if they want. This will also not resize the atlas if it is full. +/// +/// IMPORTANT: this renders by /glyph index/ and not by /codepoint/. The caller +/// is expected to translate codepoints to glyph indexes in some way. The most +/// trivial way to do this is to get the Face and call glyphIndex. If you're +/// doing text shaping, the text shaping library (i.e. HarfBuzz) will automatically +/// determine glyph indexes for a text run. +pub fn renderGlyph( + self: *CodepointResolver, + alloc: Allocator, + atlas: *Atlas, + index: Collection.Index, + glyph_index: u32, + opts: RenderOptions, +) !Glyph { + // Special-case fonts are rendered directly. + if (index.special()) |sp| switch (sp) { + .sprite => return try self.sprite.?.renderGlyph( + alloc, + atlas, + glyph_index, + opts, + ), + }; + + const face = try self.collection.getFace(index); + const glyph = try face.renderGlyph(alloc, atlas, glyph_index, opts); + // log.warn("GLYPH={}", .{glyph}); + return glyph; +} + +/// Packed array of booleans to indicate if a style is enabled or not. +pub const StyleStatus = std.EnumArray(Style, bool); + +/// Map of descriptors to faces. This is used with manual codepoint maps +/// to ensure that we don't load the same font multiple times. +/// +/// Note that the current implementation will load the same font multiple +/// times if the font used for a codepoint map is identical to a font used +/// for a regular style. That's just an inefficient choice made now because +/// the implementation is simpler and codepoint maps matching a regular +/// font is a rare case. +const DescriptorCache = std.HashMapUnmanaged( + DiscoveryDescriptor, + ?Collection.Index, + struct { + const KeyType = DiscoveryDescriptor; + + pub fn hash(ctx: @This(), k: KeyType) u64 { + _ = ctx; + return k.hashcode(); + } + + pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { + // Note that this means its possible to have two different + // descriptors match when there is a hash collision so we + // should button this up later. + return ctx.hash(a) == ctx.hash(b); + } + }, + std.hash_map.default_max_load_percentage, +); + +test getIndex { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = font.embedded.regular; + const testEmoji = font.embedded.emoji; + const testEmojiText = font.embedded.emoji_text; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var c = Collection.init(); + c.load_options = .{ .library = lib }; + + { + errdefer c.deinit(alloc); + _ = try c.add(alloc, try .init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + if (comptime !font.options.backend.hasCoretext()) { + // Coretext doesn't support Noto's format + _ = try c.add(alloc, try .init( + lib, + testEmoji, + .{ .size = .{ .points = 12 } }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + } + _ = try c.add(alloc, try .init( + lib, + testEmojiText, + .{ .size = .{ .points = 12 } }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + } + + var r: CodepointResolver = .{ .collection = c }; + defer r.deinit(alloc); + + // Should find all visible ASCII + var i: u32 = 32; + while (i < 127) : (i += 1) { + const idx = r.getIndex(alloc, i, .regular, null).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); + } + + // Try emoji + { + const idx = r.getIndex(alloc, '🥸', .regular, null).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 1), idx.idx); + } + + // Try text emoji + { + const idx = r.getIndex(alloc, 0x270C, .regular, .text).?; + try testing.expectEqual(Style.regular, idx.style); + const text_idx = if (comptime font.options.backend.hasCoretext()) 1 else 2; + try testing.expectEqual(@as(Collection.Index.IndexInt, text_idx), idx.idx); + } + { + const idx = r.getIndex(alloc, 0x270C, .regular, .emoji).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 1), idx.idx); + } + + // Box glyph should be null since we didn't set a box font + { + try testing.expect(r.getIndex(alloc, 0x1FB00, .regular, null) == null); + } +} + +test "getIndex disabled font style" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = font.embedded.regular; + + var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas_grayscale.deinit(alloc); + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var c = Collection.init(); + c.load_options = .{ .library = lib }; + + _ = try c.add(alloc, try .init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + _ = try c.add(alloc, try .init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ), .{ + .style = .bold, + .fallback = false, + .size_adjustment = .none, + }); + _ = try c.add(alloc, try .init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ), .{ + .style = .italic, + .fallback = false, + .size_adjustment = .none, + }); + + var r: CodepointResolver = .{ .collection = c }; + defer r.deinit(alloc); + r.styles.set(.bold, false); // Disable bold + + // Regular should work fine + { + const idx = r.getIndex(alloc, 'A', .regular, null).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); + } + + // Bold should go to regular + { + const idx = r.getIndex(alloc, 'A', .bold, null).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); + } + + // Italic should still work + { + const idx = r.getIndex(alloc, 'A', .italic, null).?; + try testing.expectEqual(Style.italic, idx.style); + try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); + } +} + +test "getIndex box glyph" { + const testing = std.testing; + const alloc = testing.allocator; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + const c = Collection.init(); + + var r: CodepointResolver = .{ + .collection = c, + .sprite = .{ + .metrics = font.Metrics.calc(.{ + .px_per_em = 30.0, + .cell_width = 18.0, + .ascent = 30.0, + .descent = -6.0, + .line_gap = 0.0, + }), + }, + }; + defer r.deinit(alloc); + + // Should find a box glyph + const idx = r.getIndex(alloc, 0x2500, .regular, null).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@intFromEnum(Collection.Index.Special.sprite), idx.idx); +} |
