From 2d174f9bff96cc65c55f6c0a6b27f14e655b7a08 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Dec 2024 20:49:45 -0500 Subject: [PATCH] font: allow non-boolean font feature settings + much more flexible syntax and lenient parser + allows comma-separated list as a single config value This allows, e.g. `cv01 = 2` to select the second variant of `cv01`. --- src/config/Config.zig | 21 +- src/font/shape.zig | 9 +- src/font/shaper/coretext.zig | 142 +++++-------- src/font/shaper/feature.zig | 390 +++++++++++++++++++++++++++++++++++ src/font/shaper/harfbuzz.zig | 50 ++--- 5 files changed, 489 insertions(+), 123 deletions(-) create mode 100644 src/font/shaper/feature.zig diff --git a/src/config/Config.zig b/src/config/Config.zig index a2f71c0c0..da7c1fee0 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -147,23 +147,28 @@ const c = @cImport({ /// By default, synthetic styles are enabled. @"font-synthetic-style": FontSyntheticStyle = .{}, -/// Apply a font feature. This can be repeated multiple times to enable multiple -/// font features. You can NOT set multiple font features with a single value -/// (yet). +/// Apply a font feature. To enable multiple font features you can repeat +/// this multiple times or use a comma-separated list of feature settings. +/// +/// The syntax for feature settings is as follows, where `feat` is a feature: +/// +/// * Enable features with e.g. `feat`, `+feat`, `feat on`, `feat=1`. +/// * Disabled features with e.g. `-feat`, `feat off`, `feat=0`. +/// * Set a feature value with e.g. `feat=2`, `feat = 3`, `feat 4`. +/// * Feature names may be wrapped in quotes, meaning this config should be +/// syntactically compatible with the `font-feature-settings` CSS property. +/// +/// The syntax is fairly loose, but invalid settings will be silently ignored. /// /// The font feature will apply to all fonts rendered by Ghostty. A future /// enhancement will allow targeting specific faces. /// -/// A valid value is the name of a feature. Prefix the feature with a `-` to -/// explicitly disable it. Example: `ss20` or `-ss20`. -/// /// To disable programming ligatures, use `-calt` since this is the typical /// feature name for programming ligatures. To look into what font features /// your font has and what they do, use a font inspection tool such as /// [fontdrop.info](https://fontdrop.info). /// -/// To generally disable most ligatures, use `-calt`, `-liga`, and `-dlig` (as -/// separate repetitive entries in your config). +/// To generally disable most ligatures, use `-calt, -liga, -dlig`. @"font-feature": RepeatableString = .{}, /// Font size in points. This value can be a non-integer and the nearest integer diff --git a/src/font/shape.zig b/src/font/shape.zig index 3721c63a6..cc67fc7a0 100644 --- a/src/font/shape.zig +++ b/src/font/shape.zig @@ -1,6 +1,7 @@ const builtin = @import("builtin"); const options = @import("main.zig").options; const run = @import("shaper/run.zig"); +const feature = @import("shaper/feature.zig"); pub const noop = @import("shaper/noop.zig"); pub const harfbuzz = @import("shaper/harfbuzz.zig"); pub const coretext = @import("shaper/coretext.zig"); @@ -8,6 +9,9 @@ pub const web_canvas = @import("shaper/web_canvas.zig"); pub const Cache = @import("shaper/Cache.zig"); pub const TextRun = run.TextRun; pub const RunIterator = run.RunIterator; +pub const Feature = feature.Feature; +pub const FeatureList = feature.FeatureList; +pub const default_features = feature.default_features; /// Shaper implementation for our compile options. pub const Shaper = switch (options.backend) { @@ -49,10 +53,7 @@ pub const Cell = struct { /// Options for shapers. pub const Options = struct { - /// Font features to use when shaping. These can be in the following - /// formats: "-feat" "+feat" "feat". A "-"-prefix is used to disable - /// a feature and the others are used to enable a feature. If a feature - /// isn't supported or is invalid, it will be ignored. + /// Font features to use when shaping. /// /// Note: eventually, this will move to font.Face probably as we may /// want to support per-face feature configuration. For now, we only diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index dbc9809e3..e084a68c9 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -7,6 +7,9 @@ const trace = @import("tracy").trace; const font = @import("../main.zig"); const os = @import("../../os/main.zig"); const terminal = @import("../../terminal/main.zig"); +const Feature = font.shape.Feature; +const FeatureList = font.shape.FeatureList; +const default_features = font.shape.default_features; const Face = font.Face; const Collection = font.Collection; const DeferredFace = font.DeferredFace; @@ -40,9 +43,10 @@ pub const Shaper = struct { /// The string used for shaping the current run. run_state: RunState, - /// The font features we want to use. The hardcoded features are always - /// set first. - features: FeatureList, + /// CoreFoundation Dictionary which represents our font feature settings. + features: *macos.foundation.Dictionary, + /// A version of the features dictionary with the default features excluded. + features_no_default: *macos.foundation.Dictionary, /// The shared memory used for shaping results. cell_buf: CellBuf, @@ -100,51 +104,17 @@ pub const Shaper = struct { } }; - /// List of font features, parsed into the data structures used by - /// the CoreText API. The CoreText API requires a pretty annoying wrapping - /// to setup font features: - /// - /// - The key parsed into a CFString - /// - The value parsed into a CFNumber - /// - The key and value are then put into a CFDictionary - /// - The CFDictionary is then put into a CFArray - /// - The CFArray is then put into another CFDictionary - /// - The CFDictionary is then passed to the CoreText API to create - /// a new font with the features set. - /// - /// This structure handles up to the point that we have a CFArray of - /// CFDictionary objects representing the font features and provides - /// functions for creating the dictionary to init the font. - const FeatureList = struct { - list: *macos.foundation.MutableArray, + /// Create a CoreFoundation Dictionary suitable for + /// settings the font features of a CoreText font. + fn makeFeaturesDict(feats: []const Feature) !*macos.foundation.Dictionary { + const list = try macos.foundation.MutableArray.create(); + errdefer list.release(); - pub fn init() !FeatureList { - var list = try macos.foundation.MutableArray.create(); - errdefer list.release(); - return .{ .list = list }; - } - - pub fn deinit(self: FeatureList) void { - self.list.release(); - } - - /// Append the given feature to the list. The feature syntax is - /// the same as Harfbuzz: "feat" enables it and "-feat" disables it. - pub fn append(self: *FeatureList, name_raw: []const u8) !void { - // If the name is `-name` then we are disabling the feature, - // otherwise we are enabling it, so we need to parse this out. - const name = if (name_raw[0] == '-') name_raw[1..] else name_raw; - const dict = try featureDict(name, name_raw[0] != '-'); - defer dict.release(); - self.list.appendValue(macos.foundation.Dictionary, dict); - } - - /// Create the dictionary for the given feature and value. - fn featureDict(name: []const u8, v: bool) !*macos.foundation.Dictionary { - const value_num: c_int = @intFromBool(v); + for (feats) |feat| { + const value_num: c_int = @intCast(feat.value); // Keys can only be ASCII. - var key = try macos.foundation.String.createWithBytes(name, .ascii, false); + var key = try macos.foundation.String.createWithBytes(&feat.tag, .ascii, false); defer key.release(); var value = try macos.foundation.Number.create(.int, &value_num); defer value.release(); @@ -154,50 +124,44 @@ pub const Shaper = struct { macos.text.c.kCTFontOpenTypeFeatureTag, macos.text.c.kCTFontOpenTypeFeatureValue, }, - &[_]?*const anyopaque{ - key, - value, - }, + &[_]?*const anyopaque{ key, value }, ); - errdefer dict.release(); - return dict; + defer dict.release(); + + list.appendValue(macos.foundation.Dictionary, dict); } - /// Returns the dictionary to use with the font API to set the - /// features. This should be released by the caller. - pub fn attrsDict( - self: FeatureList, - omit_defaults: bool, - ) !*macos.foundation.Dictionary { - // Get our feature list. If we're omitting defaults then we - // slice off the hardcoded features. - const list = if (!omit_defaults) self.list else list: { - const list = try macos.foundation.MutableArray.createCopy(@ptrCast(self.list)); - for (hardcoded_features) |_| list.removeValue(0); - break :list list; - }; - defer if (omit_defaults) list.release(); + var dict = try macos.foundation.Dictionary.create( + &[_]?*const anyopaque{macos.text.c.kCTFontFeatureSettingsAttribute}, + &[_]?*const anyopaque{list}, + ); + errdefer dict.release(); - var dict = try macos.foundation.Dictionary.create( - &[_]?*const anyopaque{macos.text.c.kCTFontFeatureSettingsAttribute}, - &[_]?*const anyopaque{list}, - ); - errdefer dict.release(); - return dict; - } - }; - - // These features are hardcoded to always be on by default. Users - // can turn them off by setting the features to "-liga" for example. - const hardcoded_features = [_][]const u8{ "dlig", "liga" }; + return dict; + } /// The cell_buf argument is the buffer to use for storing shaped results. /// This should be at least the number of columns in the terminal. pub fn init(alloc: Allocator, opts: font.shape.Options) !Shaper { - var feats = try FeatureList.init(); - errdefer feats.deinit(); - for (hardcoded_features) |name| try feats.append(name); - for (opts.features) |name| try feats.append(name); + var feature_list: FeatureList = .{}; + defer feature_list.deinit(alloc); + for (opts.features) |feature_str| { + try feature_list.appendFromString(alloc, feature_str); + } + + // We need to construct two attrs dictionaries for font features; + // one without the default features included, and one with them. + const feats = feature_list.features.items; + const feats_df = try alloc.alloc(Feature, feats.len + default_features.len); + defer alloc.free(feats_df); + + @memcpy(feats_df[0..default_features.len], &default_features); + @memcpy(feats_df[default_features.len..], feats); + + const features = try makeFeaturesDict(feats_df); + errdefer features.release(); + const features_no_default = try makeFeaturesDict(feats); + errdefer features_no_default.release(); var run_state = RunState.init(); errdefer run_state.deinit(alloc); @@ -242,7 +206,8 @@ pub const Shaper = struct { .alloc = alloc, .cell_buf = .{}, .run_state = run_state, - .features = feats, + .features = features, + .features_no_default = features_no_default, .writing_direction = writing_direction, .cached_fonts = .{}, .cached_font_grid = 0, @@ -255,7 +220,8 @@ pub const Shaper = struct { pub fn deinit(self: *Shaper) void { self.cell_buf.deinit(self.alloc); self.run_state.deinit(self.alloc); - self.features.deinit(); + self.features.release(); + self.features_no_default.release(); self.writing_direction.release(); { @@ -509,8 +475,8 @@ pub const Shaper = struct { // If we have it, return the cached attr dict. if (self.cached_fonts.items[index_int]) |cached| return cached; - // Features dictionary, font descriptor, font - try self.cf_release_pool.ensureUnusedCapacity(self.alloc, 3); + // Font descriptor, font + try self.cf_release_pool.ensureUnusedCapacity(self.alloc, 2); const run_font = font: { // The CoreText shaper relies on CoreText and CoreText claims @@ -533,8 +499,10 @@ pub const Shaper = struct { const face = try grid.resolver.collection.getFace(index); const original = face.font; - const attrs = try self.features.attrsDict(face.quirks_disable_default_font_features); - self.cf_release_pool.appendAssumeCapacity(attrs); + const attrs = if (face.quirks_disable_default_font_features) + self.features_no_default + else + self.features; const desc = try macos.text.FontDescriptor.createWithAttributes(attrs); self.cf_release_pool.appendAssumeCapacity(desc); diff --git a/src/font/shaper/feature.zig b/src/font/shaper/feature.zig new file mode 100644 index 000000000..8e70d51da --- /dev/null +++ b/src/font/shaper/feature.zig @@ -0,0 +1,390 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const log = std.log.scoped(.font_shaper); + +/// Represents an OpenType font feature setting, which consists of a tag and +/// a numeric parameter >= 0. Most features are boolean, so only parameters +/// of 0 and 1 make sense for them, but some (e.g. 'cv01'..'cv99') can take +/// parameters to choose between multiple variants of a given character or +/// characters. +/// +/// Ref: +/// - https://learn.microsoft.com/en-us/typography/opentype/spec/chapter2#features-and-lookups +/// - https://harfbuzz.github.io/shaping-opentype-features.html +pub const Feature = struct { + tag: [4]u8, + value: u32, + + pub fn fromString(str: []const u8) ?Feature { + var fbs = std.io.fixedBufferStream(str); + const reader = fbs.reader(); + return Feature.fromReader(reader); + } + + /// Parse a single font feature setting from a std.io.Reader, with a version + /// of the syntax of HarfBuzz's font feature strings. Stops at end of stream + /// or when a ',' is encountered. + /// + /// This parsing aims to be as error-tolerant as possible while avoiding any + /// assumptions in ambiguous scenarios. When invalid syntax is encountered, + /// the reader is advanced to the next boundary (end-of-stream or ',') so + /// that further features may be read. + /// + /// Ref: https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string + pub fn fromReader(reader: anytype) ?Feature { + var tag: [4]u8 = undefined; + var value: ?u32 = null; + + // TODO: when we move to Zig 0.14 this can be replaced with a + // labeled switch continue pattern rather than this loop. + var state: union(enum) { + /// Initial state. + start: void, + /// Parsing the tag, data is index. + tag: u2, + /// In the space between the tag and the value. + space: void, + /// Parsing an integer parameter directly in to `value`. + int: void, + /// Parsing a boolean keyword parameter ("on"/"off"). + bool: void, + /// Encountered an unrecoverable syntax error, advancing to boundary. + err: void, + /// Done parsing feature. + done: void, + } = .start; + while (true) { + // If we hit the end of the stream we just pretend it's a comma. + const byte = reader.readByte() catch ','; + switch (state) { + // If we're done then we skip whitespace until we see a ','. + .done => switch (byte) { + ' ', '\t' => continue, + ',' => break, + // If we see something other than whitespace or a ',' + // then this is an error since the intent is unclear. + else => { + state = .err; + continue; + }, + }, + + // If we're fast-forwarding from an error we just wanna + // stop at the first boundary and ignore all other bytes. + .err => if (byte == ',') return null, + + .start => switch (byte) { + // Ignore leading whitespace. + ' ', '\t' => continue, + // Empty feature string. + ',' => return null, + // '+' prefix to explicitly enable feature. + '+' => { + value = 1; + state = .{ .tag = 0 }; + continue; + }, + // '-' prefix to explicitly disable feature. + '-' => { + value = 0; + state = .{ .tag = 0 }; + continue; + }, + // Quote mark introducing a tag. + '"', '\'' => { + state = .{ .tag = 0 }; + continue; + }, + // First letter of tag. + else => { + tag[0] = byte; + state = .{ .tag = 1 }; + continue; + }, + }, + + .tag => |*i| switch (byte) { + // If the tag is interrupted by a comma it's invalid. + ',' => return null, + // Ignore quote marks. + '"', '\'' => continue, + // A prefix of '+' or '-' + // In all other cases we add the byte to our tag. + else => { + tag[i.*] = byte; + if (i.* == 3) { + state = .space; + continue; + } + i.* += 1; + }, + }, + + .space => switch (byte) { + ' ', '\t' => continue, + // Ignore quote marks since we might have a + // closing quote from the tag still ahead. + '"', '\'' => continue, + // Allow an '=' (which we can safely ignore) + // only if we don't already have a value due + // to a '+' or '-' prefix. + '=' => if (value != null) { + state = .err; + continue; + }, + ',' => { + // Specifying only a tag turns a feature on. + if (value == null) value = 1; + break; + }, + '0'...'9' => { + // If we already have value because of a + // '+' or '-' prefix then this is an error. + if (value != null) { + state = .err; + continue; + } + value = byte - '0'; + state = .int; + continue; + }, + 'o', 'O' => { + // If we already have value because of a + // '+' or '-' prefix then this is an error. + if (value != null) { + state = .err; + continue; + } + state = .bool; + continue; + }, + else => { + state = .err; + continue; + }, + }, + + .int => switch (byte) { + ',' => break, + '0'...'9' => { + // If our value gets too big while + // parsing we consider it an error. + value = std.math.mul(u32, value.?, 10) catch { + state = .err; + continue; + }; + value.? += byte - '0'; + }, + else => { + state = .err; + continue; + }, + }, + + .bool => switch (byte) { + ',' => return null, + 'n', 'N' => { + // "ofn" + if (value != null) { + assert(value == 0); + state = .err; + continue; + } + value = 1; + state = .done; + continue; + }, + 'f', 'F' => { + // To make sure we consume two 'f's. + if (value == null) { + value = 0; + } else { + assert(value == 0); + state = .done; + continue; + } + }, + else => { + state = .err; + continue; + }, + }, + } + } + + assert(value != null); + + return .{ + .tag = tag, + .value = value.?, + }; + } + + /// Serialize this feature to the provided buffer. + /// The string that this produces should be valid to parse. + pub fn toString(self: *const Feature, buf: []u8) !void { + var fbs = std.io.fixedBufferStream(buf); + try self.format("", .{}, fbs.writer()); + } + + /// Formatter for logging + pub fn format( + self: Feature, + comptime layout: []const u8, + opts: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = layout; + _ = opts; + if (self.value <= 1) { + // Format boolean options as "+tag" for on and "-tag" for off. + try std.fmt.format(writer, "{c}{s}", .{ + "-+"[self.value], + self.tag, + }); + } else { + // Format non-boolean tags as "tag=value". + try std.fmt.format(writer, "{s}={d}", .{ + self.tag, + self.value, + }); + } + } +}; + +/// A list of font feature settings (see `Feature` for more documentation). +pub const FeatureList = struct { + features: std.ArrayListUnmanaged(Feature) = .{}, + + pub fn deinit(self: *FeatureList, alloc: Allocator) void { + self.features.deinit(alloc); + } + + /// Parse a comma separated list of features. + /// See `Feature.fromReader` for more docs. + pub fn fromString(alloc: Allocator, str: []const u8) !FeatureList { + var self: FeatureList = .{}; + try self.appendFromString(alloc, str); + return self; + } + + /// Append features to this list from a string with a comma separated list. + /// See `Feature.fromReader` for more docs. + pub fn appendFromString( + self: *FeatureList, + alloc: Allocator, + str: []const u8, + ) !void { + var fbs = std.io.fixedBufferStream(str); + const reader = fbs.reader(); + while (fbs.pos < fbs.buffer.len) { + const i = fbs.pos; + if (Feature.fromReader(reader)) |feature| { + try self.features.append(alloc, feature); + } else log.warn( + "failed to parse font feature setting: \"{s}\"", + .{fbs.buffer[i..fbs.pos]}, + ); + } + } + + /// Formatter for logging + pub fn format( + self: FeatureList, + comptime layout: []const u8, + opts: std.fmt.FormatOptions, + writer: anytype, + ) !void { + for (self.features.items, 0..) |feature, i| { + try feature.format(layout, opts, writer); + if (i != std.features.items.len - 1) try writer.writeAll(", "); + } + if (self.value <= 1) { + // Format boolean options as "+tag" for on and "-tag" for off. + try std.fmt.format(writer, "{c}{s}", .{ + "-+"[self.value], + self.tag, + }); + } else { + // Format non-boolean tags as "tag=value". + try std.fmt.format(writer, "{s}={d}", .{ + self.tag, + self.value, + }); + } + } +}; + +/// These features are hardcoded to always be on by default. Users +/// can turn them off by setting the features to "-liga" for example. +pub const default_features = [_]Feature{ + .{ .tag = "dlig".*, .value = 1 }, + .{ .tag = "liga".*, .value = 1 }, +}; + +test "Feature.fromString" { + const testing = std.testing; + + // This is not *complete* coverage of every possible + // combination of syntax, but it covers quite a few. + + // Boolean settings (on) + const kern_on = Feature{ .tag = "kern".*, .value = 1 }; + try testing.expectEqual(kern_on, Feature.fromString("kern")); + try testing.expectEqual(kern_on, Feature.fromString("kern, ")); + try testing.expectEqual(kern_on, Feature.fromString("kern on")); + try testing.expectEqual(kern_on, Feature.fromString("kern on, ")); + try testing.expectEqual(kern_on, Feature.fromString("+kern")); + try testing.expectEqual(kern_on, Feature.fromString("+kern, ")); + try testing.expectEqual(kern_on, Feature.fromString("\"kern\" = 1")); + try testing.expectEqual(kern_on, Feature.fromString("\"kern\" = 1, ")); + + // Boolean settings (off) + const kern_off = Feature{ .tag = "kern".*, .value = 0 }; + try testing.expectEqual(kern_off, Feature.fromString("kern off")); + try testing.expectEqual(kern_off, Feature.fromString("kern off, ")); + try testing.expectEqual(kern_off, Feature.fromString("-'kern'")); + try testing.expectEqual(kern_off, Feature.fromString("-'kern', ")); + try testing.expectEqual(kern_off, Feature.fromString("\"kern\" = 0")); + try testing.expectEqual(kern_off, Feature.fromString("\"kern\" = 0, ")); + + // Non-boolean settings + const aalt_2 = Feature{ .tag = "aalt".*, .value = 2 }; + try testing.expectEqual(aalt_2, Feature.fromString("aalt=2")); + try testing.expectEqual(aalt_2, Feature.fromString("aalt=2, ")); + try testing.expectEqual(aalt_2, Feature.fromString("'aalt' 2")); + try testing.expectEqual(aalt_2, Feature.fromString("'aalt' 2, ")); + + // Various ambiguous/error cases which should be null + try testing.expectEqual(null, Feature.fromString("aalt=2x")); // bad number + try testing.expectEqual(null, Feature.fromString("toolong")); // tag too long + try testing.expectEqual(null, Feature.fromString("sht")); // tag too short + try testing.expectEqual(null, Feature.fromString("-kern 1")); // redundant/conflicting + try testing.expectEqual(null, Feature.fromString("-kern on")); // redundant/conflicting + try testing.expectEqual(null, Feature.fromString("aalt=o,")); // bad keyword + try testing.expectEqual(null, Feature.fromString("aalt=ofn,")); // bad keyword +} + +test "FeatureList.fromString" { + const testing = std.testing; + + const str = + " kern, kern on , +kern, \"kern\" = 1," ++ // Boolean settings (on) + "kern off, -'kern' , \"kern\"=0," ++ // Boolean settings (off) + "aalt=2, 'aalt'\t2," ++ // Non-boolean settings + "aalt=2x, toolong, sht, -kern 1, -kern on, aalt=o, aalt=ofn," ++ // Invalid cases + "last"; // To ensure final element is included correctly. + var feats = try FeatureList.fromString(testing.allocator, str); + defer feats.deinit(testing.allocator); + try testing.expectEqualSlices( + Feature, + &(.{Feature{ .tag = "kern".*, .value = 1 }} ** 4 ++ + .{Feature{ .tag = "kern".*, .value = 0 }} ** 3 ++ + .{Feature{ .tag = "aalt".*, .value = 2 }} ** 2 ++ + .{Feature{ .tag = "last".*, .value = 1 }}), + feats.features.items, + ); +} diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index ccb422f20..97292b9b0 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -3,6 +3,10 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const harfbuzz = @import("harfbuzz"); const font = @import("../main.zig"); +const terminal = @import("../../terminal/main.zig"); +const Feature = font.shape.Feature; +const FeatureList = font.shape.FeatureList; +const default_features = font.shape.default_features; const Face = font.Face; const Collection = font.Collection; const DeferredFace = font.DeferredFace; @@ -10,7 +14,6 @@ const Library = font.Library; const SharedGrid = font.SharedGrid; const Style = font.Style; const Presentation = font.Presentation; -const terminal = @import("../../terminal/main.zig"); const log = std.log.scoped(.font_shaper); @@ -27,38 +30,37 @@ pub const Shaper = struct { cell_buf: CellBuf, /// The features to use for shaping. - hb_feats: FeatureList, + hb_feats: []harfbuzz.Feature, const CellBuf = std.ArrayListUnmanaged(font.shape.Cell); - const FeatureList = std.ArrayListUnmanaged(harfbuzz.Feature); - - // These features are hardcoded to always be on by default. Users - // can turn them off by setting the features to "-liga" for example. - const hardcoded_features = [_][]const u8{ "dlig", "liga" }; /// The cell_buf argument is the buffer to use for storing shaped results. /// This should be at least the number of columns in the terminal. pub fn init(alloc: Allocator, opts: font.shape.Options) !Shaper { - // Parse all the features we want to use. We use - var hb_feats = hb_feats: { - var list = try FeatureList.initCapacity(alloc, opts.features.len + hardcoded_features.len); - errdefer list.deinit(alloc); - - for (hardcoded_features) |name| { - if (harfbuzz.Feature.fromString(name)) |feat| { - try list.append(alloc, feat); - } else log.warn("failed to parse font feature: {s}", .{name}); + // Parse all the features we want to use. + const hb_feats = hb_feats: { + var feature_list: FeatureList = .{}; + defer feature_list.deinit(alloc); + try feature_list.features.appendSlice(alloc, &default_features); + for (opts.features) |feature_str| { + try feature_list.appendFromString(alloc, feature_str); } - for (opts.features) |name| { - if (harfbuzz.Feature.fromString(name)) |feat| { - try list.append(alloc, feat); - } else log.warn("failed to parse font feature: {s}", .{name}); + var list = try alloc.alloc(harfbuzz.Feature, feature_list.features.items.len); + errdefer alloc.free(list); + + for (feature_list.features.items, 0..) |feature, i| { + list[i] = .{ + .tag = std.mem.nativeToBig(u32, @bitCast(feature.tag)), + .value = feature.value, + .start = harfbuzz.c.HB_FEATURE_GLOBAL_START, + .end = harfbuzz.c.HB_FEATURE_GLOBAL_END, + }; } break :hb_feats list; }; - errdefer hb_feats.deinit(alloc); + errdefer alloc.free(hb_feats); return Shaper{ .alloc = alloc, @@ -71,7 +73,7 @@ pub const Shaper = struct { pub fn deinit(self: *Shaper) void { self.hb_buf.destroy(); self.cell_buf.deinit(self.alloc); - self.hb_feats.deinit(self.alloc); + self.alloc.free(self.hb_feats); } pub fn endFrame(self: *const Shaper) void { @@ -125,10 +127,10 @@ pub const Shaper = struct { // If we are disabling default font features we just offset // our features by the hardcoded items because always // add those at the beginning. - break :i hardcoded_features.len; + break :i default_features.len; }; - harfbuzz.shape(face.hb_font, self.hb_buf, self.hb_feats.items[i..]); + harfbuzz.shape(face.hb_font, self.hb_buf, self.hb_feats[i..]); } // If our buffer is empty, we short-circuit the rest of the work