mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
Merge pull request #185 from mitchellh/font-features
font-feature config to enable/disable OpenType Font Features
This commit is contained in:
@ -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.
|
||||
|
@ -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 = &.{},
|
||||
};
|
||||
|
@ -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{
|
||||
|
@ -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);
|
||||
|
@ -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.*;
|
||||
}
|
||||
|
||||
|
@ -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.*;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user