mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +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-italic": ?[:0]const u8 = null,
|
||||||
@"font-family-bold-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 in points
|
||||||
@"font-size": u8 = switch (builtin.os.tag) {
|
@"font-size": u8 = switch (builtin.os.tag) {
|
||||||
// On Mac we default a little bigger since this tends to look better.
|
// On Mac we default a little bigger since this tends to look better.
|
||||||
|
@ -30,3 +30,20 @@ pub const Cell = struct {
|
|||||||
/// the runs.
|
/// the runs.
|
||||||
glyph_index: u32,
|
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.
|
/// The shared memory used for shaping results.
|
||||||
cell_buf: []font.shape.Cell,
|
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.
|
/// 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.
|
/// 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 {
|
||||||
// Allocator is not used because harfbuzz uses libc
|
// Parse all the features we want to use. We use
|
||||||
_ = alloc;
|
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{
|
return Shaper{
|
||||||
.hb_buf = try harfbuzz.Buffer.create(),
|
.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 {
|
pub fn deinit(self: *Shaper) void {
|
||||||
self.hb_buf.destroy();
|
self.hb_buf.destroy();
|
||||||
|
self.hb_feats.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an iterator that returns one text run at a time for the
|
/// 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
|
// 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.
|
// fonts, the codepoint == glyph_index so we don't need to run any shaping.
|
||||||
if (run.font_index.special() == null) {
|
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);
|
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
|
// 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);
|
var cell_buf = try alloc.alloc(font.shape.Cell, 80);
|
||||||
errdefer alloc.free(cell_buf);
|
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();
|
errdefer shaper.deinit();
|
||||||
|
|
||||||
return TestShaper{
|
return TestShaper{
|
||||||
|
@ -37,10 +37,12 @@ pub const Shaper = struct {
|
|||||||
|
|
||||||
/// The cell_buf argument is the buffer to use for storing shaped results.
|
/// 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.
|
/// 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{
|
return Shaper{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.cell_buf = cell_buf,
|
.cell_buf = opts.cell_buf,
|
||||||
.run_buf = .{},
|
.run_buf = .{},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -238,7 +240,7 @@ pub const Wasm = struct {
|
|||||||
var cell_buf = try alloc.alloc(font.shape.Cell, cap);
|
var cell_buf = try alloc.alloc(font.shape.Cell, cap);
|
||||||
errdefer alloc.free(cell_buf);
|
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();
|
errdefer shaper.deinit();
|
||||||
|
|
||||||
var result = try alloc.create(Shaper);
|
var result = try alloc.create(Shaper);
|
||||||
|
@ -130,6 +130,7 @@ const GPUCellMode = enum(u8) {
|
|||||||
/// pass around Config pointers which makes memory management a pain.
|
/// pass around Config pointers which makes memory management a pain.
|
||||||
pub const DerivedConfig = struct {
|
pub const DerivedConfig = struct {
|
||||||
font_thicken: bool,
|
font_thicken: bool,
|
||||||
|
font_features: std.ArrayList([]const u8),
|
||||||
cursor_color: ?terminal.color.RGB,
|
cursor_color: ?terminal.color.RGB,
|
||||||
background: terminal.color.RGB,
|
background: terminal.color.RGB,
|
||||||
background_opacity: f64,
|
background_opacity: f64,
|
||||||
@ -141,11 +142,17 @@ pub const DerivedConfig = struct {
|
|||||||
alloc_gpa: Allocator,
|
alloc_gpa: Allocator,
|
||||||
config: *const configpkg.Config,
|
config: *const configpkg.Config,
|
||||||
) !DerivedConfig {
|
) !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 .{
|
return .{
|
||||||
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
|
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
|
||||||
.font_thicken = config.@"font-thicken",
|
.font_thicken = config.@"font-thicken",
|
||||||
|
.font_features = font_features,
|
||||||
|
|
||||||
.cursor_color = if (config.@"cursor-color") |col|
|
.cursor_color = if (config.@"cursor-color") |col|
|
||||||
col.toTerminalRGB()
|
col.toTerminalRGB()
|
||||||
@ -168,7 +175,7 @@ pub const DerivedConfig = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *DerivedConfig) void {
|
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.
|
// avoid allocations later.
|
||||||
var shape_buf = try alloc.alloc(font.shape.Cell, 160);
|
var shape_buf = try alloc.alloc(font.shape.Cell, 160);
|
||||||
errdefer alloc.free(shape_buf);
|
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();
|
errdefer font_shaper.deinit();
|
||||||
|
|
||||||
// Initialize our Metal buffers
|
// Initialize our Metal buffers
|
||||||
@ -753,6 +763,20 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
|||||||
self.font_group.atlas_color.clear();
|
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.*;
|
self.config = config.*;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,6 +236,7 @@ const GPUCellMode = enum(u8) {
|
|||||||
/// pass around Config pointers which makes memory management a pain.
|
/// pass around Config pointers which makes memory management a pain.
|
||||||
pub const DerivedConfig = struct {
|
pub const DerivedConfig = struct {
|
||||||
font_thicken: bool,
|
font_thicken: bool,
|
||||||
|
font_features: std.ArrayList([]const u8),
|
||||||
cursor_color: ?terminal.color.RGB,
|
cursor_color: ?terminal.color.RGB,
|
||||||
background: terminal.color.RGB,
|
background: terminal.color.RGB,
|
||||||
background_opacity: f64,
|
background_opacity: f64,
|
||||||
@ -247,11 +248,17 @@ pub const DerivedConfig = struct {
|
|||||||
alloc_gpa: Allocator,
|
alloc_gpa: Allocator,
|
||||||
config: *const configpkg.Config,
|
config: *const configpkg.Config,
|
||||||
) !DerivedConfig {
|
) !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 .{
|
return .{
|
||||||
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
|
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
|
||||||
.font_thicken = config.@"font-thicken",
|
.font_thicken = config.@"font-thicken",
|
||||||
|
.font_features = font_features,
|
||||||
|
|
||||||
.cursor_color = if (config.@"cursor-color") |col|
|
.cursor_color = if (config.@"cursor-color") |col|
|
||||||
col.toTerminalRGB()
|
col.toTerminalRGB()
|
||||||
@ -274,7 +281,7 @@ pub const DerivedConfig = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *DerivedConfig) void {
|
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
|
// Create the initial font shaper
|
||||||
var shape_buf = try alloc.alloc(font.shape.Cell, 1);
|
var shape_buf = try alloc.alloc(font.shape.Cell, 1);
|
||||||
errdefer alloc.free(shape_buf);
|
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();
|
errdefer shaper.deinit();
|
||||||
|
|
||||||
// Create our shader
|
// Create our shader
|
||||||
@ -1299,6 +1309,20 @@ pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void {
|
|||||||
self.font_group.atlas_color.clear();
|
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.*;
|
self.config = config.*;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user