aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/font/CodepointResolver.zig
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/libghostty-vt/src/font/CodepointResolver.zig')
-rw-r--r--vendor/libghostty-vt/src/font/CodepointResolver.zig562
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);
+}