font-feature config to enable/disable OpenType Font Features

This commit is contained in:
Mitchell Hashimoto
2023-07-05 13:05:51 -07:00
parent 853f15ef19
commit 45ac9b5d4c
6 changed files with 122 additions and 21 deletions

View File

@ -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.

View File

@ -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 = &.{},
};

View File

@ -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{

View File

@ -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);

View File

@ -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.*;
}

View File

@ -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.*;
}