diff --git a/src/config.zig b/src/config.zig index cd92cbbf4..456ba97b0 100644 --- a/src/config.zig +++ b/src/config.zig @@ -26,6 +26,17 @@ pub const Config = struct { @"font-family-italic": ?[:0]const u8 = null, @"font-family-bold-italic": ?[:0]const u8 = null, + /// 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). + /// + /// The font feature will apply to all fonts rendered by Ghostty. A + /// future enhancement will allow targetting specific faces. + /// + /// A valid value is the name of a feature. Prefix the feature with a + /// "-" to explicitly disable it. Example: "ss20" or "-ss20". + @"font-feature": RepeatableString = .{}, + /// Font size in points @"font-size": u8 = switch (builtin.os.tag) { // On Mac we default a little bigger since this tends to look better. diff --git a/src/font/shape.zig b/src/font/shape.zig index 93e5a9db4..086e5d8f6 100644 --- a/src/font/shape.zig +++ b/src/font/shape.zig @@ -30,3 +30,20 @@ pub const Cell = struct { /// the runs. glyph_index: u32, }; + +/// Options for shapers. +pub const Options = struct { + /// 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. + cell_buf: []Cell, + + /// 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. + /// + /// Note: eventually, this will move to font.Face probably as we may + /// want to support per-face feature configuration. For now, we only + /// support applying features globally. + features: []const []const u8 = &.{}, +}; diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 1f4cbc817..e10b86edb 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -24,20 +24,49 @@ pub const Shaper = struct { /// The shared memory used for shaping results. cell_buf: []font.shape.Cell, + /// The features to use for shaping. + hb_feats: FeatureList, + + const FeatureList = std.ArrayList(harfbuzz.Feature); + /// 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, cell_buf: []font.shape.Cell) !Shaper { - // Allocator is not used because harfbuzz uses libc - _ = alloc; + 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: { + // 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" }; + + var list = try FeatureList.initCapacity(alloc, opts.features.len + hardcoded_features.len); + errdefer list.deinit(); + + for (hardcoded_features) |name| { + if (harfbuzz.Feature.fromString(name)) |feat| { + try list.append(feat); + } else log.warn("failed to parse font feature: {s}", .{name}); + } + + for (opts.features) |name| { + if (harfbuzz.Feature.fromString(name)) |feat| { + try list.append(feat); + } else log.warn("failed to parse font feature: {s}", .{name}); + } + + break :hb_feats list; + }; + errdefer hb_feats.deinit(); return Shaper{ .hb_buf = try harfbuzz.Buffer.create(), - .cell_buf = cell_buf, + .cell_buf = opts.cell_buf, + .hb_feats = hb_feats, }; } pub fn deinit(self: *Shaper) void { self.hb_buf.destroy(); + self.hb_feats.deinit(); } /// Returns an iterator that returns one text run at a time for the @@ -75,14 +104,8 @@ pub const Shaper = struct { // We only do shaping if the font is not a special-case. For special-case // fonts, the codepoint == glyph_index so we don't need to run any shaping. if (run.font_index.special() == null) { - // TODO: we do not want to hardcode these - const hb_feats = &[_]harfbuzz.Feature{ - harfbuzz.Feature.fromString("dlig").?, - harfbuzz.Feature.fromString("liga").?, - }; - const face = try run.group.group.faceFromIndex(run.font_index); - harfbuzz.shape(face.hb_font, self.hb_buf, hb_feats); + harfbuzz.shape(face.hb_font, self.hb_buf, self.hb_feats.items); } // If our buffer is empty, we short-circuit the rest of the work @@ -657,7 +680,7 @@ fn testShaper(alloc: Allocator) !TestShaper { var cell_buf = try alloc.alloc(font.shape.Cell, 80); errdefer alloc.free(cell_buf); - var shaper = try Shaper.init(alloc, cell_buf); + var shaper = try Shaper.init(alloc, .{ .cell_buf = cell_buf }); errdefer shaper.deinit(); return TestShaper{ diff --git a/src/font/shaper/web_canvas.zig b/src/font/shaper/web_canvas.zig index 3507f89e7..991929488 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -37,10 +37,12 @@ pub const Shaper = struct { /// 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, cell_buf: []font.shape.Cell) !Shaper { + pub fn init(alloc: Allocator, opts: font.shape.Options) !Shaper { + // Note: we do not support opts.font_features + return Shaper{ .alloc = alloc, - .cell_buf = cell_buf, + .cell_buf = opts.cell_buf, .run_buf = .{}, }; } @@ -238,7 +240,7 @@ pub const Wasm = struct { var cell_buf = try alloc.alloc(font.shape.Cell, cap); errdefer alloc.free(cell_buf); - var shaper = try Shaper.init(alloc, cell_buf); + var shaper = try Shaper.init(alloc, .{ .cell_buf = cell_buf }); errdefer shaper.deinit(); var result = try alloc.create(Shaper); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 89a200be0..22b7835d8 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -130,6 +130,7 @@ const GPUCellMode = enum(u8) { /// pass around Config pointers which makes memory management a pain. pub const DerivedConfig = struct { font_thicken: bool, + font_features: std.ArrayList([]const u8), cursor_color: ?terminal.color.RGB, background: terminal.color.RGB, background_opacity: f64, @@ -141,11 +142,17 @@ pub const DerivedConfig = struct { alloc_gpa: Allocator, config: *const configpkg.Config, ) !DerivedConfig { - _ = alloc_gpa; + // Copy our font features + var font_features = features: { + var clone = try config.@"font-feature".list.clone(alloc_gpa); + break :features clone.toManaged(alloc_gpa); + }; + errdefer font_features.deinit(); return .{ .background_opacity = @max(0, @min(1, config.@"background-opacity")), .font_thicken = config.@"font-thicken", + .font_features = font_features, .cursor_color = if (config.@"cursor-color") |col| col.toTerminalRGB() @@ -168,7 +175,7 @@ pub const DerivedConfig = struct { } pub fn deinit(self: *DerivedConfig) void { - _ = self; + self.font_features.deinit(); } }; @@ -228,7 +235,10 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { // avoid allocations later. var shape_buf = try alloc.alloc(font.shape.Cell, 160); errdefer alloc.free(shape_buf); - var font_shaper = try font.Shaper.init(alloc, shape_buf); + var font_shaper = try font.Shaper.init(alloc, .{ + .cell_buf = shape_buf, + .features = options.config.font_features.items, + }); errdefer font_shaper.deinit(); // Initialize our Metal buffers @@ -753,6 +763,20 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void { self.font_group.atlas_color.clear(); } + // We always redo the font shaper in case font features changed. We + // could check to see if there was an actual config change but this is + // easier and rare enough to not cause performance issues. + { + var font_shaper = try font.Shaper.init(self.alloc, .{ + .cell_buf = self.font_shaper.cell_buf, + .features = config.font_features.items, + }); + errdefer font_shaper.deinit(); + self.font_shaper.deinit(); + self.font_shaper = font_shaper; + } + + self.config.deinit(); self.config = config.*; } diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 3d06901de..4bc0d796e 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -236,6 +236,7 @@ const GPUCellMode = enum(u8) { /// pass around Config pointers which makes memory management a pain. pub const DerivedConfig = struct { font_thicken: bool, + font_features: std.ArrayList([]const u8), cursor_color: ?terminal.color.RGB, background: terminal.color.RGB, background_opacity: f64, @@ -247,11 +248,17 @@ pub const DerivedConfig = struct { alloc_gpa: Allocator, config: *const configpkg.Config, ) !DerivedConfig { - _ = alloc_gpa; + // Copy our font features + var font_features = features: { + var clone = try config.@"font-feature".list.clone(alloc_gpa); + break :features clone.toManaged(alloc_gpa); + }; + errdefer font_features.deinit(); return .{ .background_opacity = @max(0, @min(1, config.@"background-opacity")), .font_thicken = config.@"font-thicken", + .font_features = font_features, .cursor_color = if (config.@"cursor-color") |col| col.toTerminalRGB() @@ -274,7 +281,7 @@ pub const DerivedConfig = struct { } pub fn deinit(self: *DerivedConfig) void { - _ = self; + self.font_features.deinit(); } }; @@ -282,7 +289,10 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { // Create the initial font shaper var shape_buf = try alloc.alloc(font.shape.Cell, 1); errdefer alloc.free(shape_buf); - var shaper = try font.Shaper.init(alloc, shape_buf); + var shaper = try font.Shaper.init(alloc, .{ + .cell_buf = shape_buf, + .features = options.config.font_features.items, + }); errdefer shaper.deinit(); // Create our shader @@ -1299,6 +1309,20 @@ pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void { self.font_group.atlas_color.clear(); } + // We always redo the font shaper in case font features changed. We + // could check to see if there was an actual config change but this is + // easier and rare enough to not cause performance issues. + { + var font_shaper = try font.Shaper.init(self.alloc, .{ + .cell_buf = self.font_shaper.cell_buf, + .features = config.font_features.items, + }); + errdefer font_shaper.deinit(); + self.font_shaper.deinit(); + self.font_shaper = font_shaper; + } + + self.config.deinit(); self.config = config.*; }