diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/font/SharedGridSet.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/font/SharedGridSet.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/font/SharedGridSet.zig | 834 |
1 files changed, 834 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/font/SharedGridSet.zig b/vendor/libghostty-vt/src/font/SharedGridSet.zig new file mode 100644 index 00000000..b832139b --- /dev/null +++ b/vendor/libghostty-vt/src/font/SharedGridSet.zig @@ -0,0 +1,834 @@ +//! This structure contains a set of SharedGrid structures keyed by +//! unique font configuration. +//! +//! Most terminals (surfaces) will share the same font configuration. +//! This structure allows expensive font information such as +//! the font atlas, glyph cache, font faces, etc. to be shared. +//! +//! This structure is thread-safe when the operations are documented +//! as thread-safe. +const SharedGridSet = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = @import("../quirks.zig").inlineAssert; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const font = @import("main.zig"); +const CodepointResolver = font.CodepointResolver; +const Collection = font.Collection; +const Discover = font.Discover; +const Style = font.Style; +const Library = font.Library; +const Metrics = font.Metrics; +const CodepointMap = font.CodepointMap; +const DesiredSize = font.face.DesiredSize; +const Face = font.Face; +const SharedGrid = font.SharedGrid; +const discovery = @import("discovery.zig"); +const configpkg = @import("../config.zig"); +const Config = configpkg.Config; + +const log = std.log.scoped(.font_shared_grid_set); + +/// The allocator to use for all heap allocations. +alloc: Allocator, + +/// The map of font configurations to SharedGrid instances. +map: Map = .{}, + +/// The font library that is used for all font groups. +font_lib: Library, + +/// Font discovery mechanism. +font_discover: ?Discover = null, + +/// Lock to protect multi-threaded access to the map. +lock: std.Thread.Mutex = .{}, + +pub const InitError = Library.InitError; + +/// Initialize a new SharedGridSet. +pub fn init(alloc: Allocator) InitError!SharedGridSet { + var font_lib = try Library.init(alloc); + errdefer font_lib.deinit(); + + return .{ + .alloc = alloc, + .map = .{}, + .font_lib = font_lib, + }; +} + +pub fn deinit(self: *SharedGridSet) void { + var it = self.map.iterator(); + while (it.next()) |entry| { + entry.key_ptr.deinit(); + const v = entry.value_ptr.*; + v.grid.deinit(self.alloc); + self.alloc.destroy(v.grid); + } + self.map.deinit(self.alloc); + + if (comptime Discover != void) { + if (self.font_discover) |*v| v.deinit(); + } + + self.font_lib.deinit(); +} + +/// Returns the number of cached grids. +pub fn count(self: *SharedGridSet) usize { + self.lock.lock(); + defer self.lock.unlock(); + return self.map.count(); +} + +/// Initialize a SharedGrid for the given font configuration. If the +/// SharedGrid is not present it will be initialized with a ref count of +/// 1. If it is present, the ref count will be incremented. +/// +/// This is NOT thread-safe. +/// +/// The returned data (key and grid) should never be freed. The memory is +/// owned by the set and will be freed when the ref count reaches zero. +pub fn ref( + self: *SharedGridSet, + config: *const DerivedConfig, + font_size: DesiredSize, +) !struct { Key, *SharedGrid } { + var key = try Key.init(self.alloc, config, font_size); + errdefer key.deinit(); + + self.lock.lock(); + defer self.lock.unlock(); + + const gop = try self.map.getOrPut(self.alloc, key); + if (gop.found_existing) { + log.debug("found cached grid for font config", .{}); + + // We can deinit the key because we found a cached value. + key.deinit(); + + // Increment our ref count and return the cache + gop.value_ptr.ref += 1; + return .{ gop.key_ptr.*, gop.value_ptr.grid }; + } + errdefer self.map.removeByPtr(gop.key_ptr); + + log.debug("initializing new grid for font config", .{}); + + // A new font config, initialize the cache. + const grid = try self.alloc.create(SharedGrid); + errdefer self.alloc.destroy(grid); + gop.value_ptr.* = .{ + .grid = grid, + .ref = 1, + }; + + grid.* = try .init(self.alloc, resolver: { + // Build our collection. This is the expensive operation that + // involves finding fonts, loading them (maybe, some are deferred), + // etc. + var c = try self.collection(&key, font_size, config); + errdefer c.deinit(self.alloc); + + // Setup our enabled/disabled styles + var styles = CodepointResolver.StyleStatus.initFill(true); + styles.set(.bold, config.@"font-style-bold" != .false); + styles.set(.italic, config.@"font-style-italic" != .false); + styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); + + // Init our resolver which just requires setting fields. + break :resolver .{ + .collection = c, + .styles = styles, + .discover = try self.discover(), + .codepoint_map = key.codepoint_map, + }; + }); + errdefer grid.deinit(self.alloc); + + return .{ gop.key_ptr.*, gop.value_ptr.grid }; +} + +/// Builds the Collection for the given configuration key and +/// initial font size. +fn collection( + self: *SharedGridSet, + key: *const Key, + size: DesiredSize, + config: *const DerivedConfig, +) !Collection { + // A quick note on memory management: + // - font_lib is owned by the SharedGridSet + // - metric_modifiers is owned by the key which is freed only when + // the ref count for this grid reaches zero. + const load_options: Collection.LoadOptions = .{ + .library = self.font_lib, + .size = size, + .freetype_load_flags = key.freetype_load_flags, + }; + + var c = Collection.init(); + errdefer c.deinit(self.alloc); + c.load_options = load_options; + c.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.addDeferred(self.alloc, face, .{ + .style = style, + .fallback = false, + // No size adjustment for primary fonts. + .size_adjustment = .none, + }); + + continue; + } + } + + // If there are variation configurations and we didn't find + // the font, then we retry the discovery with all stylistic + // bits set to false. This is because some fonts may not + // set the stylistic bit in their table but still support + // axes to mimic the style. At the time of writing, Berkeley + // Mono Variable is like this. See #2140. + if (style != .regular and desc.variations.len > 0) { + var disco_it = try disco.discover(self.alloc, desc: { + var copy = desc; + copy.bold = false; + copy.italic = false; + break :desc copy; + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| { + log.info("font {s}: {s}", .{ + field.name, + try face.name(&name_buf), + }); + + _ = try c.addDeferred(self.alloc, face, .{ + .style = style, + .fallback = false, + // No size adjustment for primary fonts. + .size_adjustment = .none, + }); + + continue; + } + } + + log.warn("font-family {s} not found: {s}", .{ + field.name, + desc.family.?, + }); + } + } + } + + // Complete our styles to ensure we have something to satisfy every + // possible style request. We do this before adding our built-in font + // because we want to ensure our built-in styles are fallbacks to + // the configured styles. + try c.completeStyles(self.alloc, config.@"font-synthetic-style"); + + // Our built-in font will be used as a backup + _ = try c.add( + self.alloc, + try .init( + self.font_lib, + font.embedded.variable, + load_options.faceOptions(), + ), + .{ + .style = .regular, + .fallback = true, + .size_adjustment = font.default_fallback_adjustment, + }, + ); + try (try c.getFace(try c.add( + self.alloc, + try .init( + self.font_lib, + font.embedded.variable, + load_options.faceOptions(), + ), + .{ + .style = .bold, + .fallback = true, + .size_adjustment = font.default_fallback_adjustment, + }, + ))).setVariations( + &.{.{ .id = .init("wght"), .value = 700 }}, + load_options.faceOptions(), + ); + _ = try c.add( + self.alloc, + try .init( + self.font_lib, + font.embedded.variable_italic, + load_options.faceOptions(), + ), + .{ + .style = .italic, + .fallback = true, + .size_adjustment = font.default_fallback_adjustment, + }, + ); + try (try c.getFace(try c.add( + self.alloc, + try .init( + self.font_lib, + font.embedded.variable_italic, + load_options.faceOptions(), + ), + .{ + .style = .bold_italic, + .fallback = true, + .size_adjustment = font.default_fallback_adjustment, + }, + ))).setVariations( + &.{.{ .id = .init("wght"), .value = 700 }}, + load_options.faceOptions(), + ); + + // Nerd-font symbols fallback. + _ = try c.add( + self.alloc, + try .init( + self.font_lib, + font.embedded.symbols_nerd_font, + load_options.faceOptions(), + ), + .{ + .style = .regular, + .fallback = true, + // No size adjustment for the symbols font. + .size_adjustment = .none, + }, + ); + + // 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.os.tag.isDarwin() and Discover != void) 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.addDeferred(self.alloc, face, .{ + .style = .regular, + .fallback = true, + // No size adjustment for emojis. + .size_adjustment = .none, + }); + } + } + + // 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.os.tag.isDarwin() or Discover == void) { + _ = try c.add( + self.alloc, + try .init( + self.font_lib, + font.embedded.emoji, + load_options.faceOptions(), + ), + .{ + .style = .regular, + .fallback = true, + // No size adjustment for emojis. + .size_adjustment = .none, + }, + ); + _ = try c.add( + self.alloc, + try .init( + self.font_lib, + font.embedded.emoji_text, + load_options.faceOptions(), + ), + .{ + .style = .regular, + .fallback = true, + // No size adjustment for emojis. + .size_adjustment = .none, + }, + ); + } + + return c; +} + +/// Decrement the ref count for the given key. If the ref count is zero, +/// the grid will be deinitialized and removed from the map.j:w +pub fn deref(self: *SharedGridSet, key: Key) void { + self.lock.lock(); + defer self.lock.unlock(); + + const entry = self.map.getEntry(key) orelse return; + assert(entry.value_ptr.ref >= 1); + + // If we have more than one reference, decrement and return. + if (entry.value_ptr.ref > 1) { + entry.value_ptr.ref -= 1; + return; + } + + // We are at a zero ref count so deinit the group and remove. + entry.key_ptr.deinit(); + entry.value_ptr.grid.deinit(self.alloc); + self.alloc.destroy(entry.value_ptr.grid); + self.map.removeByPtr(entry.key_ptr); +} + +/// Map of font configurations to grid instances. The grid +/// instances are pointers that are heap allocated so that they're +/// stable pointers across hash map resizes. +pub const Map = std.HashMapUnmanaged( + Key, + ReffedGrid, + struct { + const KeyType = Key; + + pub fn hash(ctx: @This(), k: KeyType) u64 { + _ = ctx; + return k.hashcode(); + } + + pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { + return ctx.hash(a) == ctx.hash(b); + } + }, + std.hash_map.default_max_load_percentage, +); + +/// Initialize once and return the font discovery mechanism. This remains +/// initialized throughout the lifetime of the application because some +/// font discovery mechanisms (i.e. fontconfig) are unsafe to reinit. +fn discover(self: *SharedGridSet) !?*Discover { + // If we're built without a font discovery mechanism, return null + if (comptime Discover == void) return null; + + // If we initialized, use it + if (self.font_discover) |*v| return v; + + self.font_discover = .init(); + return &self.font_discover.?; +} + +/// Ref-counted SharedGrid. +const ReffedGrid = struct { + grid: *SharedGrid, + ref: u32 = 0, +}; + +/// This is the configuration required to create a key without having +/// to keep the full Ghostty configuration around. +pub const DerivedConfig = struct { + arena: ArenaAllocator, + + @"font-family": configpkg.RepeatableString, + @"font-family-bold": configpkg.RepeatableString, + @"font-family-italic": configpkg.RepeatableString, + @"font-family-bold-italic": configpkg.RepeatableString, + @"font-style": configpkg.FontStyle, + @"font-style-bold": configpkg.FontStyle, + @"font-style-italic": configpkg.FontStyle, + @"font-style-bold-italic": configpkg.FontStyle, + @"font-variation": configpkg.RepeatableFontVariation, + @"font-variation-bold": configpkg.RepeatableFontVariation, + @"font-variation-italic": configpkg.RepeatableFontVariation, + @"font-variation-bold-italic": configpkg.RepeatableFontVariation, + @"font-codepoint-map": configpkg.RepeatableCodepointMap, + @"font-synthetic-style": configpkg.FontSyntheticStyle, + @"adjust-cell-width": ?Metrics.Modifier, + @"adjust-cell-height": ?Metrics.Modifier, + @"adjust-font-baseline": ?Metrics.Modifier, + @"adjust-underline-position": ?Metrics.Modifier, + @"adjust-underline-thickness": ?Metrics.Modifier, + @"adjust-strikethrough-position": ?Metrics.Modifier, + @"adjust-strikethrough-thickness": ?Metrics.Modifier, + @"adjust-overline-position": ?Metrics.Modifier, + @"adjust-overline-thickness": ?Metrics.Modifier, + @"adjust-cursor-thickness": ?Metrics.Modifier, + @"adjust-cursor-height": ?Metrics.Modifier, + @"adjust-box-thickness": ?Metrics.Modifier, + @"adjust-icon-height": ?Metrics.Modifier, + @"freetype-load-flags": font.face.FreetypeLoadFlags, + + /// Initialize a DerivedConfig. The config should be either a + /// config.Config or another DerivedConfig to clone from. + pub fn init( + alloc_gpa: Allocator, + config: anytype, + ) Allocator.Error!DerivedConfig { + var arena = ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + return .{ + .@"font-family" = try config.@"font-family".clone(alloc), + .@"font-family-bold" = try config.@"font-family-bold".clone(alloc), + .@"font-family-italic" = try config.@"font-family-italic".clone(alloc), + .@"font-family-bold-italic" = try config.@"font-family-bold-italic".clone(alloc), + .@"font-style" = try config.@"font-style".clone(alloc), + .@"font-style-bold" = try config.@"font-style-bold".clone(alloc), + .@"font-style-italic" = try config.@"font-style-italic".clone(alloc), + .@"font-style-bold-italic" = try config.@"font-style-bold-italic".clone(alloc), + .@"font-variation" = try config.@"font-variation".clone(alloc), + .@"font-variation-bold" = try config.@"font-variation-bold".clone(alloc), + .@"font-variation-italic" = try config.@"font-variation-italic".clone(alloc), + .@"font-variation-bold-italic" = try config.@"font-variation-bold-italic".clone(alloc), + .@"font-codepoint-map" = try config.@"font-codepoint-map".clone(alloc), + .@"font-synthetic-style" = config.@"font-synthetic-style", + .@"adjust-cell-width" = config.@"adjust-cell-width", + .@"adjust-cell-height" = config.@"adjust-cell-height", + .@"adjust-font-baseline" = config.@"adjust-font-baseline", + .@"adjust-underline-position" = config.@"adjust-underline-position", + .@"adjust-underline-thickness" = config.@"adjust-underline-thickness", + .@"adjust-strikethrough-position" = config.@"adjust-strikethrough-position", + .@"adjust-strikethrough-thickness" = config.@"adjust-strikethrough-thickness", + .@"adjust-overline-position" = config.@"adjust-overline-position", + .@"adjust-overline-thickness" = config.@"adjust-overline-thickness", + .@"adjust-cursor-thickness" = config.@"adjust-cursor-thickness", + .@"adjust-cursor-height" = config.@"adjust-cursor-height", + .@"adjust-box-thickness" = config.@"adjust-box-thickness", + .@"adjust-icon-height" = config.@"adjust-icon-height", + .@"freetype-load-flags" = if (font.face.FreetypeLoadFlags != void) config.@"freetype-load-flags" else {}, + + // This must be last so the arena contains all our allocations + // from above since Zig does assignment in order. + .arena = arena, + }; + } + + pub fn deinit(self: *DerivedConfig) void { + self.arena.deinit(); + } +}; + +/// The key used to uniquely identify a font configuration. +pub const Key = struct { + arena: ArenaAllocator, + + /// The descriptors used for all the fonts added to the + /// initial group, including all styles. This is hashed + /// in order so the order matters. All users of the struct + /// should ensure that the order is consistent. + descriptors: []const discovery.Descriptor = &.{}, + + /// These are the offsets into the descriptors array for + /// each style. For example, bold is from + /// offsets[@intFromEnum(.bold) - 1] to + /// offsets[@intFromEnum(.bold)]. + style_offsets: StyleOffsets = @splat(0), + + /// The codepoint map configuration. + codepoint_map: CodepointMap = .{}, + + /// The metric modifier set configuration. + metric_modifiers: Metrics.ModifierSet = .{}, + + /// The configured font size for this key. We don't use this + /// directly but it is used as part of the hash for the + /// font grid. + font_size: DesiredSize = .{ .points = 12 }, + + /// The freetype load flags configuration, only non-void if the + /// freetype backend is enabled. + freetype_load_flags: font.face.FreetypeLoadFlags = font.face.freetype_load_flags_default, + + const style_offsets_len = std.enums.directEnumArrayLen(Style, 0); + const StyleOffsets = [style_offsets_len]usize; + + comptime { + // We assume this throughout this structure. If this changes + // we may need to change this structure. + assert(@intFromEnum(Style.regular) == 0); + assert(@intFromEnum(Style.bold) == 1); + assert(@intFromEnum(Style.italic) == 2); + assert(@intFromEnum(Style.bold_italic) == 3); + } + + pub fn init( + alloc_gpa: Allocator, + config_src: *const DerivedConfig, + font_size: DesiredSize, + ) Allocator.Error!Key { + var arena = ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Clone our configuration. We need to do this because the lifetime + // of the derived config is usually shorter than that of a key + // and we use pointers into the derived config for the key. We + // can remove this if we wanted by dupe-ing the memory we use + // from DerivedConfig below. + var config = try DerivedConfig.init(alloc, config_src); + + var descriptors: std.ArrayList(discovery.Descriptor) = .empty; + defer descriptors.deinit(alloc); + for (config.@"font-family".list.items) |family| { + try descriptors.append(alloc, .{ + .family = family, + .style = config.@"font-style".nameValue(), + .size = font_size.points, + .variations = config.@"font-variation".list.items, + }); + } + + // In all the styled cases below, we prefer to specify an exact + // style via the `font-style` configuration. If a style is not + // specified, we use the discovery mechanism to search for a + // style category such as bold, italic, etc. We can't specify both + // because the latter will restrict the search to only that. If + // a user says `font-style = italic` for the bold face for example, + // no results would be found if we restrict to ALSO searching for + // italic. + for (config.@"font-family-bold".list.items) |family| { + const style = config.@"font-style-bold".nameValue(); + try descriptors.append(alloc, .{ + .family = family, + .style = style, + .size = font_size.points, + .bold = style == null, + .variations = config.@"font-variation-bold".list.items, + }); + } + for (config.@"font-family-italic".list.items) |family| { + const style = config.@"font-style-italic".nameValue(); + try descriptors.append(alloc, .{ + .family = family, + .style = style, + .size = font_size.points, + .italic = style == null, + .variations = config.@"font-variation-italic".list.items, + }); + } + for (config.@"font-family-bold-italic".list.items) |family| { + const style = config.@"font-style-bold-italic".nameValue(); + try descriptors.append(alloc, .{ + .family = family, + .style = style, + .size = font_size.points, + .bold = style == null, + .italic = style == null, + .variations = config.@"font-variation-bold-italic".list.items, + }); + } + + // Setup the codepoint map + const codepoint_map: CodepointMap = map: { + const map = config.@"font-codepoint-map"; + if (map.map.list.len == 0) break :map .{}; + const clone = try config.@"font-codepoint-map".clone(alloc); + break :map clone.map; + }; + + // Metric modifiers + const metric_modifiers: Metrics.ModifierSet = set: { + var set: Metrics.ModifierSet = .{}; + if (config.@"adjust-cell-width") |m| try set.put(alloc, .cell_width, m); + if (config.@"adjust-cell-height") |m| try set.put(alloc, .cell_height, m); + if (config.@"adjust-font-baseline") |m| try set.put(alloc, .cell_baseline, m); + if (config.@"adjust-underline-position") |m| try set.put(alloc, .underline_position, m); + if (config.@"adjust-underline-thickness") |m| try set.put(alloc, .underline_thickness, m); + if (config.@"adjust-strikethrough-position") |m| try set.put(alloc, .strikethrough_position, m); + if (config.@"adjust-strikethrough-thickness") |m| try set.put(alloc, .strikethrough_thickness, m); + if (config.@"adjust-overline-position") |m| try set.put(alloc, .overline_position, m); + if (config.@"adjust-overline-thickness") |m| try set.put(alloc, .overline_thickness, m); + if (config.@"adjust-cursor-thickness") |m| try set.put(alloc, .cursor_thickness, m); + if (config.@"adjust-cursor-height") |m| try set.put(alloc, .cursor_height, m); + if (config.@"adjust-box-thickness") |m| try set.put(alloc, .box_thickness, m); + if (config.@"adjust-icon-height") |m| try set.put(alloc, .icon_height, m); + break :set set; + }; + + const regular_offset = config.@"font-family".list.items.len; + const bold_offset = regular_offset + config.@"font-family-bold".list.items.len; + const italic_offset = bold_offset + config.@"font-family-italic".list.items.len; + const bold_italic_offset = italic_offset + config.@"font-family-bold-italic".list.items.len; + + return .{ + .arena = arena, + .descriptors = try descriptors.toOwnedSlice(alloc), + .style_offsets = .{ + regular_offset, + bold_offset, + italic_offset, + bold_italic_offset, + }, + .codepoint_map = codepoint_map, + .metric_modifiers = metric_modifiers, + .font_size = font_size, + .freetype_load_flags = if (font.face.FreetypeLoadFlags != void) + config.@"freetype-load-flags" + else + font.face.freetype_load_flags_default, + }; + } + + pub fn deinit(self: *Key) void { + self.arena.deinit(); + } + + /// Get the descriptors for the given font style that can be + /// used with discovery. + pub fn descriptorsForStyle( + self: Key, + style: Style, + ) []const discovery.Descriptor { + const idx = @intFromEnum(style); + const start: usize = if (idx == 0) 0 else self.style_offsets[idx - 1]; + const end = self.style_offsets[idx]; + return self.descriptors[start..end]; + } + + /// Hash the key with the given hasher. + pub fn hash(self: Key, hasher: anytype) void { + const autoHash = std.hash.autoHash; + autoHash(hasher, @as(u32, @bitCast(self.font_size.points))); + autoHash(hasher, self.font_size.xdpi); + autoHash(hasher, self.font_size.ydpi); + autoHash(hasher, self.descriptors.len); + for (self.descriptors) |d| d.hash(hasher); + self.codepoint_map.hash(hasher); + autoHash(hasher, self.metric_modifiers.count()); + autoHash(hasher, self.freetype_load_flags); + if (self.metric_modifiers.count() > 0) { + inline for (@typeInfo(Metrics.Key).@"enum".fields) |field| { + const key = @field(Metrics.Key, field.name); + if (self.metric_modifiers.get(key)) |value| { + autoHash(hasher, key); + value.hash(hasher); + } + } + } + } + + /// Returns a hash code that can be used to uniquely identify this + /// action. + pub fn hashcode(self: Key) u64 { + var hasher = std.hash.Wyhash.init(0); + self.hash(&hasher); + return hasher.final(); + } +}; + +test "Key" { + const testing = std.testing; + const alloc = testing.allocator; + var cfg = try Config.default(alloc); + defer cfg.deinit(); + + var keycfg = try DerivedConfig.init(alloc, &cfg); + defer keycfg.deinit(); + + var k = try Key.init(alloc, &keycfg, .{ .points = 12 }); + defer k.deinit(); + + var k2 = try Key.init(alloc, &keycfg, .{ .points = 12 }); + defer k2.deinit(); + + try testing.expect(k.hashcode() > 0); + try testing.expectEqual(k.hashcode(), k2.hashcode()); +} + +test "Key different font points" { + const testing = std.testing; + const alloc = testing.allocator; + var cfg = try Config.default(alloc); + defer cfg.deinit(); + + var keycfg = try DerivedConfig.init(alloc, &cfg); + defer keycfg.deinit(); + + var k = try Key.init(alloc, &keycfg, .{ .points = 12 }); + defer k.deinit(); + + var k2 = try Key.init(alloc, &keycfg, .{ .points = 16 }); + defer k2.deinit(); + + try testing.expect(k.hashcode() != k2.hashcode()); +} + +test "Key different font DPI" { + const testing = std.testing; + const alloc = testing.allocator; + var cfg = try Config.default(alloc); + defer cfg.deinit(); + + var keycfg = try DerivedConfig.init(alloc, &cfg); + defer keycfg.deinit(); + + var k = try Key.init(alloc, &keycfg, .{ .points = 12, .xdpi = 1 }); + defer k.deinit(); + + var k2 = try Key.init(alloc, &keycfg, .{ .points = 12, .xdpi = 2 }); + defer k2.deinit(); + + try testing.expect(k.hashcode() != k2.hashcode()); +} + +test SharedGridSet { + const testing = std.testing; + const alloc = testing.allocator; + + var set = try SharedGridSet.init(alloc); + defer set.deinit(); + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + + var keycfg = try DerivedConfig.init(alloc, &cfg); + defer keycfg.deinit(); + + // Get a grid for the given config + const key1, const grid1 = try set.ref(&keycfg, .{ .points = 12 }); + try testing.expectEqual(@as(usize, 1), set.count()); + + // Get another + const key2, const grid2 = try set.ref(&keycfg, .{ .points = 12 }); + try testing.expectEqual(@as(usize, 1), set.count()); + + // They should be pointer equivalent + try testing.expectEqual(@intFromPtr(grid1), @intFromPtr(grid2)); + + // If I deref grid2 then we should still have a count of 1 + set.deref(key2); + try testing.expectEqual(@as(usize, 1), set.count()); + + // If I deref grid1 then we should have a count of 0 + set.deref(key1); + try testing.expectEqual(@as(usize, 0), set.count()); +} |
