This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801//! 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 = std.debug.assert;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.add( self.alloc, style, .{ .deferred = face }, );
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.add( self.alloc, style, .{ .deferred = face }, );
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, .regular, .{ .fallback_loaded = try .init( self.font_lib, font.embedded.variable, load_options.faceOptions(), ) }, ); try (try c.getFace(try c.add( self.alloc, .bold, .{ .fallback_loaded = try .init( self.font_lib, font.embedded.variable, load_options.faceOptions(), ) }, ))).setVariations( &.{.{ .id = .init("wght"), .value = 700 }}, load_options.faceOptions(), ); _ = try c.add( self.alloc, .italic, .{ .fallback_loaded = try .init( self.font_lib, font.embedded.variable_italic, load_options.faceOptions(), ) }, ); try (try c.getFace(try c.add( self.alloc, .bold_italic, .{ .fallback_loaded = try .init( self.font_lib, font.embedded.variable_italic, load_options.faceOptions(), ) }, ))).setVariations( &.{.{ .id = .init("wght"), .value = 700 }}, load_options.faceOptions(), );
// Nerd-font symbols fallback. _ = try c.add( self.alloc, .regular, .{ .fallback_loaded = try Face.init( self.font_lib, font.embedded.symbols_nerd_font, load_options.faceOptions(), ) }, );
// 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.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.os.tag.isDarwin() or Discover == void) { _ = try c.add( self.alloc, .regular, .{ .fallback_loaded = try .init( self.font_lib, font.embedded.emoji, load_options.faceOptions(), ) }, ); _ = try c.add( self.alloc, .regular, .{ .fallback_loaded = try .init( self.font_lib, font.embedded.emoji_text, load_options.faceOptions(), ) }, ); }
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:wpub 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).init(alloc); defer descriptors.deinit(); for (config.@"font-family".list.items) |family| { try descriptors.append(.{ .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(.{ .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(.{ .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(.{ .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(), .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());}