mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00
Merge pull request #617 from mitchellh/font-modify
Configurations to adjust various font metrics
This commit is contained in:
@ -224,6 +224,20 @@ pub fn init(
|
||||
var group = try font.Group.init(alloc, font_lib, font_size);
|
||||
errdefer group.deinit();
|
||||
|
||||
// Setup our font metric modifiers if we have any.
|
||||
group.metric_modifiers = set: {
|
||||
var set: font.face.Metrics.ModifierSet = .{};
|
||||
errdefer set.deinit(alloc);
|
||||
if (config.@"adjust-cell-width") |m| try set.put(alloc, .cell_width, m);
|
||||
if (config.@"adjust-cell-height") |m| try set.put(alloc, .cell_height, m);
|
||||
if (config.@"adjust-font-baseline") |m| try set.put(alloc, .cell_baseline, m);
|
||||
if (config.@"adjust-underline-position") |m| try set.put(alloc, .underline_position, m);
|
||||
if (config.@"adjust-underline-thickness") |m| try set.put(alloc, .underline_thickness, m);
|
||||
if (config.@"adjust-strikethrough-position") |m| try set.put(alloc, .strikethrough_position, m);
|
||||
if (config.@"adjust-strikethrough-thickness") |m| try set.put(alloc, .strikethrough_thickness, m);
|
||||
break :set set;
|
||||
};
|
||||
|
||||
// If we have codepoint mappings, set those.
|
||||
if (config.@"font-codepoint-map".map.list.len > 0) {
|
||||
group.codepoint_map = config.@"font-codepoint-map".map;
|
||||
@ -306,11 +320,11 @@ pub fn init(
|
||||
// Our built-in font will be used as a backup
|
||||
_ = try group.addFace(
|
||||
.regular,
|
||||
.{ .loaded = try font.Face.init(font_lib, face_ttf, font_size) },
|
||||
.{ .loaded = try font.Face.init(font_lib, face_ttf, group.faceOptions()) },
|
||||
);
|
||||
_ = try group.addFace(
|
||||
.bold,
|
||||
.{ .loaded = try font.Face.init(font_lib, face_bold_ttf, font_size) },
|
||||
.{ .loaded = try font.Face.init(font_lib, face_bold_ttf, group.faceOptions()) },
|
||||
);
|
||||
|
||||
// Auto-italicize if we have to.
|
||||
@ -321,11 +335,11 @@ pub fn init(
|
||||
if (builtin.os.tag != .macos or font.Discover == void) {
|
||||
_ = try group.addFace(
|
||||
.regular,
|
||||
.{ .loaded = try font.Face.init(font_lib, face_emoji_ttf, font_size) },
|
||||
.{ .loaded = try font.Face.init(font_lib, face_emoji_ttf, group.faceOptions()) },
|
||||
);
|
||||
_ = try group.addFace(
|
||||
.regular,
|
||||
.{ .loaded = try font.Face.init(font_lib, face_emoji_text_ttf, font_size) },
|
||||
.{ .loaded = try font.Face.init(font_lib, face_emoji_text_ttf, group.faceOptions()) },
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ const cli = @import("../cli.zig");
|
||||
const Key = @import("key.zig").Key;
|
||||
const KeyValue = @import("key.zig").Value;
|
||||
const ErrorList = @import("ErrorList.zig");
|
||||
const MetricModifier = fontpkg.face.Metrics.Modifier;
|
||||
|
||||
const log = std.log.scoped(.config);
|
||||
|
||||
@ -122,6 +123,32 @@ const c = @cImport({
|
||||
/// currently on macOS.
|
||||
@"font-thicken": bool = false,
|
||||
|
||||
/// All of the configurations behavior adjust various metrics determined
|
||||
/// by the font. The values can be integers (1, -1, etc.) or a percentage
|
||||
/// (20%, -15%, etc.). In each case, the values represent the amount to
|
||||
/// change the original value.
|
||||
///
|
||||
/// For example, a value of "1" increases the value by 1; it does not set
|
||||
/// it to literally 1. A value of "20%" increases the value by 20%. And so
|
||||
/// on.
|
||||
///
|
||||
/// There is little to no validation on these values so the wrong values
|
||||
/// (i.e. "-100%") can cause the terminal to be unusable. Use with caution
|
||||
/// and reason.
|
||||
///
|
||||
/// Some values are clamped to minimum or maximum values. This can make it
|
||||
/// appear that certain values are ignored. For example, the underline
|
||||
/// position is clamped to the height of a cell. If you set the underline
|
||||
/// position so high that it extends beyond the bottom of the cell size,
|
||||
/// it will be clamped to the bottom of the cell.
|
||||
@"adjust-cell-width": ?MetricModifier = null,
|
||||
@"adjust-cell-height": ?MetricModifier = null,
|
||||
@"adjust-font-baseline": ?MetricModifier = null,
|
||||
@"adjust-underline-position": ?MetricModifier = null,
|
||||
@"adjust-underline-thickness": ?MetricModifier = null,
|
||||
@"adjust-strikethrough-position": ?MetricModifier = null,
|
||||
@"adjust-strikethrough-thickness": ?MetricModifier = null,
|
||||
|
||||
/// Background color for the window.
|
||||
background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },
|
||||
|
||||
|
@ -154,13 +154,13 @@ pub fn name(self: DeferredFace, buf: []u8) ![]const u8 {
|
||||
pub fn load(
|
||||
self: *DeferredFace,
|
||||
lib: Library,
|
||||
size: font.face.DesiredSize,
|
||||
opts: font.face.Options,
|
||||
) !Face {
|
||||
return switch (options.backend) {
|
||||
.fontconfig_freetype => try self.loadFontconfig(lib, size),
|
||||
.coretext => try self.loadCoreText(lib, size),
|
||||
.coretext_freetype => try self.loadCoreTextFreetype(lib, size),
|
||||
.web_canvas => try self.loadWebCanvas(size),
|
||||
.fontconfig_freetype => try self.loadFontconfig(lib, opts),
|
||||
.coretext => try self.loadCoreText(lib, opts),
|
||||
.coretext_freetype => try self.loadCoreTextFreetype(lib, opts),
|
||||
.web_canvas => try self.loadWebCanvas(opts),
|
||||
|
||||
// Unreachable because we must be already loaded or have the
|
||||
// proper configuration for one of the other deferred mechanisms.
|
||||
@ -171,7 +171,7 @@ pub fn load(
|
||||
fn loadFontconfig(
|
||||
self: *DeferredFace,
|
||||
lib: Library,
|
||||
size: font.face.DesiredSize,
|
||||
opts: font.face.Options,
|
||||
) !Face {
|
||||
const fc = self.fc.?;
|
||||
|
||||
@ -179,26 +179,26 @@ fn loadFontconfig(
|
||||
const filename = (try fc.pattern.get(.file, 0)).string;
|
||||
const face_index = (try fc.pattern.get(.index, 0)).integer;
|
||||
|
||||
var face = try Face.initFile(lib, filename, face_index, size);
|
||||
var face = try Face.initFile(lib, filename, face_index, opts);
|
||||
errdefer face.deinit();
|
||||
try face.setVariations(fc.variations);
|
||||
try face.setVariations(fc.variations, opts);
|
||||
return face;
|
||||
}
|
||||
|
||||
fn loadCoreText(
|
||||
self: *DeferredFace,
|
||||
lib: Library,
|
||||
size: font.face.DesiredSize,
|
||||
opts: font.face.Options,
|
||||
) !Face {
|
||||
_ = lib;
|
||||
const ct = self.ct.?;
|
||||
return try Face.initFontCopy(ct.font, size);
|
||||
return try Face.initFontCopy(ct.font, opts);
|
||||
}
|
||||
|
||||
fn loadCoreTextFreetype(
|
||||
self: *DeferredFace,
|
||||
lib: Library,
|
||||
size: font.face.DesiredSize,
|
||||
opts: font.face.Options,
|
||||
) !Face {
|
||||
const ct = self.ct.?;
|
||||
|
||||
@ -231,15 +231,15 @@ fn loadCoreTextFreetype(
|
||||
// TODO: face index 0 is not correct long term and we should switch
|
||||
// to using CoreText for rendering, too.
|
||||
//std.log.warn("path={s}", .{path_slice});
|
||||
return try Face.initFile(lib, buf[0..path_slice.len :0], 0, size);
|
||||
return try Face.initFile(lib, buf[0..path_slice.len :0], 0, opts);
|
||||
}
|
||||
|
||||
fn loadWebCanvas(
|
||||
self: *DeferredFace,
|
||||
size: font.face.DesiredSize,
|
||||
opts: font.face.Options,
|
||||
) !Face {
|
||||
const wc = self.wc.?;
|
||||
return try Face.initNamed(wc.alloc, wc.font_str, size, wc.presentation);
|
||||
return try Face.initNamed(wc.alloc, wc.font_str, opts, wc.presentation);
|
||||
}
|
||||
|
||||
/// Returns true if this face can satisfy the given codepoint and
|
||||
@ -400,7 +400,7 @@ test "fontconfig" {
|
||||
try testing.expect(n.len > 0);
|
||||
|
||||
// Load it and verify it works
|
||||
const face = try def.load(lib, .{ .points = 12 });
|
||||
const face = try def.load(lib, .{ .size = .{ .points = 12 } });
|
||||
try testing.expect(face.glyphIndex(' ') != null);
|
||||
}
|
||||
|
||||
|
@ -71,6 +71,10 @@ lib: Library,
|
||||
/// The desired font size. All fonts in a group must share the same size.
|
||||
size: font.face.DesiredSize,
|
||||
|
||||
/// Metric modifiers to apply to loaded fonts. The Group takes ownership
|
||||
/// over the memory and will use the associated allocator to free it.
|
||||
metric_modifiers: ?font.face.Metrics.ModifierSet = null,
|
||||
|
||||
/// The available faces we have. This shouldn't be modified manually.
|
||||
/// Instead, use the functions available on Group.
|
||||
faces: StyleArray,
|
||||
@ -139,9 +143,20 @@ pub fn deinit(self: *Group) void {
|
||||
}
|
||||
}
|
||||
|
||||
if (self.metric_modifiers) |*v| v.deinit(self.alloc);
|
||||
|
||||
self.descriptor_cache.deinit(self.alloc);
|
||||
}
|
||||
|
||||
/// Returns the options for initializing a face based on the options associated
|
||||
/// with this font group.
|
||||
pub fn faceOptions(self: *const Group) font.face.Options {
|
||||
return .{
|
||||
.size = self.size,
|
||||
.metric_modifiers = if (self.metric_modifiers) |*v| v else null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Add a face to the list for the given style. This face will be added as
|
||||
/// next in priority if others exist already, i.e. it'll be the _last_ to be
|
||||
/// searched for a glyph in that list.
|
||||
@ -185,7 +200,7 @@ pub fn italicize(self: *Group) !void {
|
||||
};
|
||||
|
||||
// Try to italicize it.
|
||||
const face = try regular.italicize();
|
||||
const face = try regular.italicize(self.faceOptions());
|
||||
try italic_list.append(self.alloc, .{ .loaded = face });
|
||||
|
||||
var buf: [128]u8 = undefined;
|
||||
@ -200,17 +215,17 @@ pub fn setSize(self: *Group, size: font.face.DesiredSize) !void {
|
||||
// currently handle it in any meaningful way if one face can resize
|
||||
// but another can't.
|
||||
|
||||
// Set our size for future loads
|
||||
self.size = size;
|
||||
|
||||
// Resize all our faces that are loaded
|
||||
var it = self.faces.iterator();
|
||||
while (it.next()) |entry| {
|
||||
for (entry.value.items) |*elem| switch (elem.*) {
|
||||
.deferred => continue,
|
||||
.loaded => |*f| try f.setSize(size),
|
||||
.loaded => |*f| try f.setSize(self.faceOptions()),
|
||||
};
|
||||
}
|
||||
|
||||
// Set our size for future loads
|
||||
self.size = size;
|
||||
}
|
||||
|
||||
/// This represents a specific font in the group.
|
||||
@ -456,7 +471,7 @@ pub fn faceFromIndex(self: *Group, index: FontIndex) !*Face {
|
||||
const item = &list.items[index.idx];
|
||||
return switch (item.*) {
|
||||
.deferred => |*d| deferred: {
|
||||
const face = try d.load(self.lib, self.size);
|
||||
const face = try d.load(self.lib, self.faceOptions());
|
||||
d.deinit();
|
||||
item.* = .{ .loaded = face };
|
||||
break :deferred &item.loaded;
|
||||
@ -623,13 +638,22 @@ test {
|
||||
var group = try init(alloc, lib, .{ .points = 12 });
|
||||
defer group.deinit();
|
||||
|
||||
_ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) });
|
||||
_ = try group.addFace(
|
||||
.regular,
|
||||
.{ .loaded = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }) },
|
||||
);
|
||||
|
||||
if (font.options.backend != .coretext) {
|
||||
// Coretext doesn't support Noto's format
|
||||
_ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testEmoji, .{ .points = 12 }) });
|
||||
_ = try group.addFace(
|
||||
.regular,
|
||||
.{ .loaded = try Face.init(lib, testEmoji, .{ .size = .{ .points = 12 } }) },
|
||||
);
|
||||
}
|
||||
_ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testEmojiText, .{ .points = 12 }) });
|
||||
_ = try group.addFace(
|
||||
.regular,
|
||||
.{ .loaded = try Face.init(lib, testEmojiText, .{ .size = .{ .points = 12 } }) },
|
||||
);
|
||||
|
||||
// Should find all visible ASCII
|
||||
var i: u32 = 32;
|
||||
@ -694,9 +718,10 @@ test "disabled font style" {
|
||||
group.styles.set(.bold, false);
|
||||
|
||||
// Same font but we can test the style in the index
|
||||
_ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) });
|
||||
_ = try group.addFace(.bold, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) });
|
||||
_ = try group.addFace(.italic, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) });
|
||||
const opts: font.face.Options = .{ .size = .{ .points = 12 } };
|
||||
_ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, opts) });
|
||||
_ = try group.addFace(.bold, .{ .loaded = try Face.init(lib, testFont, opts) });
|
||||
_ = try group.addFace(.italic, .{ .loaded = try Face.init(lib, testFont, opts) });
|
||||
|
||||
// Regular should work fine
|
||||
{
|
||||
@ -731,16 +756,17 @@ test "face count limit" {
|
||||
var lib = try Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var group = try init(alloc, lib, .{ .points = 12 });
|
||||
const opts: font.face.Options = .{ .size = .{ .points = 12 } };
|
||||
var group = try init(alloc, lib, opts.size);
|
||||
defer group.deinit();
|
||||
|
||||
for (0..FontIndex.Special.start - 1) |_| {
|
||||
_ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) });
|
||||
_ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, opts) });
|
||||
}
|
||||
|
||||
try testing.expectError(error.GroupFull, group.addFace(
|
||||
.regular,
|
||||
.{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) },
|
||||
.{ .loaded = try Face.init(lib, testFont, opts) },
|
||||
));
|
||||
}
|
||||
|
||||
@ -790,7 +816,11 @@ test "resize" {
|
||||
var group = try init(alloc, lib, .{ .points = 12, .xdpi = 96, .ydpi = 96 });
|
||||
defer group.deinit();
|
||||
|
||||
_ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 }) });
|
||||
_ = try group.addFace(.regular, .{ .loaded = try Face.init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
) });
|
||||
|
||||
// Load a letter
|
||||
{
|
||||
@ -881,7 +911,11 @@ test "faceFromIndex returns pointer" {
|
||||
var group = try init(alloc, lib, .{ .points = 12, .xdpi = 96, .ydpi = 96 });
|
||||
defer group.deinit();
|
||||
|
||||
_ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 }) });
|
||||
_ = try group.addFace(.regular, .{ .loaded = try Face.init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
) });
|
||||
|
||||
{
|
||||
const idx = group.indexForCodepoint('A', .regular, null).?;
|
||||
|
@ -184,7 +184,7 @@ test {
|
||||
// Setup group
|
||||
_ = try cache.group.addFace(
|
||||
.regular,
|
||||
.{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) },
|
||||
.{ .loaded = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }) },
|
||||
);
|
||||
var group = cache.group;
|
||||
|
||||
@ -340,7 +340,11 @@ test "resize" {
|
||||
// Setup group
|
||||
_ = try cache.group.addFace(
|
||||
.regular,
|
||||
.{ .loaded = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 }) },
|
||||
.{ .loaded = try Face.init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
) },
|
||||
);
|
||||
|
||||
// Load a letter
|
||||
|
@ -1,6 +1,7 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const options = @import("main.zig").options;
|
||||
pub const Metrics = @import("face/Metrics.zig");
|
||||
const freetype = @import("face/freetype.zig");
|
||||
const coretext = @import("face/coretext.zig");
|
||||
pub const web_canvas = @import("face/web_canvas.zig");
|
||||
@ -21,6 +22,12 @@ pub const Face = switch (options.backend) {
|
||||
/// using whatever platform method you can.
|
||||
pub const default_dpi = if (builtin.os.tag == .macos) 72 else 96;
|
||||
|
||||
/// Options for initializing a font face.
|
||||
pub const Options = struct {
|
||||
size: DesiredSize,
|
||||
metric_modifiers: ?*const Metrics.ModifierSet = null,
|
||||
};
|
||||
|
||||
/// The desired size for loading a font.
|
||||
pub const DesiredSize = struct {
|
||||
// Desired size in points
|
||||
@ -62,28 +69,6 @@ pub const Variation = struct {
|
||||
};
|
||||
};
|
||||
|
||||
/// Metrics associated with the font that are useful for renderers to know.
|
||||
pub const Metrics = struct {
|
||||
/// Recommended cell width and height for a monospace grid using this font.
|
||||
cell_width: u32,
|
||||
cell_height: u32,
|
||||
|
||||
/// For monospace grids, the recommended y-value from the bottom to set
|
||||
/// the baseline for font rendering. This is chosen so that things such
|
||||
/// as the bottom of a "g" or "y" do not drop below the cell.
|
||||
cell_baseline: u32,
|
||||
|
||||
/// The position of the underline from the top of the cell and the
|
||||
/// thickness in pixels.
|
||||
underline_position: u32,
|
||||
underline_thickness: u32,
|
||||
|
||||
/// The position and thickness of a strikethrough. Same units/style
|
||||
/// as the underline fields.
|
||||
strikethrough_position: u32,
|
||||
strikethrough_thickness: u32,
|
||||
};
|
||||
|
||||
/// Additional options for rendering glyphs.
|
||||
pub const RenderOptions = struct {
|
||||
/// The maximum height of the glyph. If this is set, then any glyph
|
||||
|
226
src/font/face/Metrics.zig
Normal file
226
src/font/face/Metrics.zig
Normal file
@ -0,0 +1,226 @@
|
||||
const Metrics = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// Recommended cell width and height for a monospace grid using this font.
|
||||
cell_width: u32,
|
||||
cell_height: u32,
|
||||
|
||||
/// For monospace grids, the recommended y-value from the bottom to set
|
||||
/// the baseline for font rendering. This is chosen so that things such
|
||||
/// as the bottom of a "g" or "y" do not drop below the cell.
|
||||
cell_baseline: u32,
|
||||
|
||||
/// The position of the underline from the top of the cell and the
|
||||
/// thickness in pixels.
|
||||
underline_position: u32,
|
||||
underline_thickness: u32,
|
||||
|
||||
/// The position and thickness of a strikethrough. Same units/style
|
||||
/// as the underline fields.
|
||||
strikethrough_position: u32,
|
||||
strikethrough_thickness: u32,
|
||||
|
||||
/// Apply a set of modifiers.
|
||||
pub fn apply(self: *Metrics, mods: ModifierSet) void {
|
||||
var it = mods.iterator();
|
||||
while (it.next()) |entry| {
|
||||
switch (entry.key_ptr.*) {
|
||||
// We clamp these values to a minimum of 1 to prevent divide-by-zero
|
||||
// in downstream operations.
|
||||
inline .cell_width,
|
||||
.cell_height,
|
||||
=> |tag| {
|
||||
const original = @field(self, @tagName(tag));
|
||||
@field(self, @tagName(tag)) = @max(
|
||||
entry.value_ptr.apply(original),
|
||||
1,
|
||||
);
|
||||
},
|
||||
|
||||
inline else => |tag| {
|
||||
@field(self, @tagName(tag)) = entry.value_ptr.apply(@field(self, @tagName(tag)));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of modifiers to apply to metrics. We use a hash map because
|
||||
/// we expect most metrics to be unmodified and want to take up as
|
||||
/// little space as possible.
|
||||
pub const ModifierSet = std.AutoHashMapUnmanaged(Key, Modifier);
|
||||
|
||||
/// A modifier to apply to a metrics value. The modifier value represents
|
||||
/// a delta, so percent is a percentage to change, not a percentage of.
|
||||
/// For example, "20%" is 20% larger, not 20% of the value. Likewise,
|
||||
/// an absolute value of "20" is 20 larger, not literally 20.
|
||||
pub const Modifier = union(enum) {
|
||||
percent: f64,
|
||||
absolute: i32,
|
||||
|
||||
/// Parses the modifier value. If the value ends in "%" it is assumed
|
||||
/// to be a percent, otherwise the value is parsed as an integer.
|
||||
pub fn parse(input: []const u8) !Modifier {
|
||||
if (input.len == 0) return error.InvalidFormat;
|
||||
|
||||
if (input[input.len - 1] == '%') {
|
||||
var percent = std.fmt.parseFloat(
|
||||
f64,
|
||||
input[0 .. input.len - 1],
|
||||
) catch return error.InvalidFormat;
|
||||
percent /= 100;
|
||||
|
||||
if (percent <= -1) return .{ .percent = 0 };
|
||||
if (percent < 0) return .{ .percent = 1 + percent };
|
||||
return .{ .percent = 1 + percent };
|
||||
}
|
||||
|
||||
return .{
|
||||
.absolute = std.fmt.parseInt(i32, input, 10) catch
|
||||
return error.InvalidFormat,
|
||||
};
|
||||
}
|
||||
|
||||
/// So it works with the config framework.
|
||||
pub fn parseCLI(input: ?[]const u8) !Modifier {
|
||||
return try parse(input orelse return error.ValueRequired);
|
||||
}
|
||||
|
||||
/// Apply a modifier to a numeric value.
|
||||
pub fn apply(self: Modifier, v: u32) u32 {
|
||||
return switch (self) {
|
||||
.percent => |p| percent: {
|
||||
const p_clamped: f64 = @max(0, p);
|
||||
const v_f64: f64 = @floatFromInt(v);
|
||||
const applied_f64: f64 = @round(v_f64 * p_clamped);
|
||||
const applied_u32: u32 = @intFromFloat(applied_f64);
|
||||
break :percent applied_u32;
|
||||
},
|
||||
|
||||
.absolute => |abs| absolute: {
|
||||
const v_i64: i64 = @intCast(v);
|
||||
const abs_i64: i64 = @intCast(abs);
|
||||
const applied_i64: i64 = @max(0, v_i64 +| abs_i64);
|
||||
const applied_u32: u32 = std.math.cast(u32, applied_i64) orelse
|
||||
std.math.maxInt(u32);
|
||||
break :absolute applied_u32;
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Key is an enum of all the available metrics keys.
|
||||
pub const Key = key: {
|
||||
const field_infos = std.meta.fields(Metrics);
|
||||
var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined;
|
||||
for (field_infos, 0..) |field, i| {
|
||||
enumFields[i] = .{
|
||||
.name = field.name,
|
||||
.value = i,
|
||||
};
|
||||
}
|
||||
|
||||
var decls = [_]std.builtin.Type.Declaration{};
|
||||
break :key @Type(.{
|
||||
.Enum = .{
|
||||
.tag_type = std.math.IntFittingRange(0, field_infos.len - 1),
|
||||
.fields = &enumFields,
|
||||
.decls = &decls,
|
||||
.is_exhaustive = true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// NOTE: This is purposely not pub because we want to force outside callers
|
||||
// to use the `.{}` syntax so unused fields are detected by the compiler.
|
||||
fn init() Metrics {
|
||||
return .{
|
||||
.cell_width = 0,
|
||||
.cell_height = 0,
|
||||
.cell_baseline = 0,
|
||||
.underline_position = 0,
|
||||
.underline_thickness = 0,
|
||||
.strikethrough_position = 0,
|
||||
.strikethrough_thickness = 0,
|
||||
};
|
||||
}
|
||||
|
||||
test "Metrics: apply modifiers" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var set: ModifierSet = .{};
|
||||
defer set.deinit(alloc);
|
||||
try set.put(alloc, .cell_width, .{ .percent = 1.2 });
|
||||
|
||||
var m: Metrics = init();
|
||||
m.cell_width = 100;
|
||||
m.apply(set);
|
||||
try testing.expectEqual(@as(u32, 120), m.cell_width);
|
||||
}
|
||||
|
||||
test "Modifier: parse absolute" {
|
||||
const testing = std.testing;
|
||||
|
||||
{
|
||||
const m = try Modifier.parse("100");
|
||||
try testing.expectEqual(Modifier{ .absolute = 100 }, m);
|
||||
}
|
||||
|
||||
{
|
||||
const m = try Modifier.parse("-100");
|
||||
try testing.expectEqual(Modifier{ .absolute = -100 }, m);
|
||||
}
|
||||
}
|
||||
|
||||
test "Modifier: parse percent" {
|
||||
const testing = std.testing;
|
||||
|
||||
{
|
||||
const m = try Modifier.parse("20%");
|
||||
try testing.expectEqual(Modifier{ .percent = 1.2 }, m);
|
||||
}
|
||||
{
|
||||
const m = try Modifier.parse("-20%");
|
||||
try testing.expectEqual(Modifier{ .percent = 0.8 }, m);
|
||||
}
|
||||
{
|
||||
const m = try Modifier.parse("0%");
|
||||
try testing.expectEqual(Modifier{ .percent = 1 }, m);
|
||||
}
|
||||
}
|
||||
|
||||
test "Modifier: percent" {
|
||||
const testing = std.testing;
|
||||
|
||||
{
|
||||
const m: Modifier = .{ .percent = 0.8 };
|
||||
const v: u32 = m.apply(100);
|
||||
try testing.expectEqual(@as(u32, 80), v);
|
||||
}
|
||||
{
|
||||
const m: Modifier = .{ .percent = 1.8 };
|
||||
const v: u32 = m.apply(100);
|
||||
try testing.expectEqual(@as(u32, 180), v);
|
||||
}
|
||||
}
|
||||
|
||||
test "Modifier: absolute" {
|
||||
const testing = std.testing;
|
||||
|
||||
{
|
||||
const m: Modifier = .{ .absolute = -100 };
|
||||
const v: u32 = m.apply(100);
|
||||
try testing.expectEqual(@as(u32, 0), v);
|
||||
}
|
||||
{
|
||||
const m: Modifier = .{ .absolute = -120 };
|
||||
const v: u32 = m.apply(100);
|
||||
try testing.expectEqual(@as(u32, 0), v);
|
||||
}
|
||||
{
|
||||
const m: Modifier = .{ .absolute = 100 };
|
||||
const v: u32 = m.apply(100);
|
||||
try testing.expectEqual(@as(u32, 200), v);
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@ pub const Face = struct {
|
||||
};
|
||||
|
||||
/// Initialize a CoreText-based font from a TTF/TTC in memory.
|
||||
pub fn init(lib: font.Library, source: [:0]const u8, size: font.face.DesiredSize) !Face {
|
||||
pub fn init(lib: font.Library, source: [:0]const u8, opts: font.face.Options) !Face {
|
||||
_ = lib;
|
||||
|
||||
const data = try macos.foundation.Data.createWithBytesNoCopy(source);
|
||||
@ -51,36 +51,45 @@ pub const Face = struct {
|
||||
const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12);
|
||||
defer ct_font.release();
|
||||
|
||||
return try initFontCopy(ct_font, size);
|
||||
return try initFontCopy(ct_font, opts);
|
||||
}
|
||||
|
||||
/// Initialize a CoreText-based face from another initialized font face
|
||||
/// but with a new size. This is often how CoreText fonts are initialized
|
||||
/// because the font is loaded at a default size during discovery, and then
|
||||
/// adjusted to the final size for final load.
|
||||
pub fn initFontCopy(base: *macos.text.Font, size: font.face.DesiredSize) !Face {
|
||||
pub fn initFontCopy(base: *macos.text.Font, opts: font.face.Options) !Face {
|
||||
// Create a copy. The copyWithAttributes docs say the size is in points,
|
||||
// but we need to scale the points by the DPI and to do that we use our
|
||||
// function called "pixels".
|
||||
const ct_font = try base.copyWithAttributes(@floatFromInt(size.pixels()), null, null);
|
||||
const ct_font = try base.copyWithAttributes(
|
||||
@floatFromInt(opts.size.pixels()),
|
||||
null,
|
||||
null,
|
||||
);
|
||||
errdefer ct_font.release();
|
||||
|
||||
return try initFont(ct_font);
|
||||
return try initFont(ct_font, opts);
|
||||
}
|
||||
|
||||
/// Initialize a face with a CTFont. This will take ownership over
|
||||
/// the CTFont. This does NOT copy or retain the CTFont.
|
||||
pub fn initFont(ct_font: *macos.text.Font) !Face {
|
||||
pub fn initFont(ct_font: *macos.text.Font, opts: font.face.Options) !Face {
|
||||
var hb_font = try harfbuzz.coretext.createFont(ct_font);
|
||||
errdefer hb_font.destroy();
|
||||
|
||||
const traits = ct_font.getSymbolicTraits();
|
||||
const metrics = metrics: {
|
||||
var metrics = try calcMetrics(ct_font);
|
||||
if (opts.metric_modifiers) |v| metrics.apply(v.*);
|
||||
break :metrics metrics;
|
||||
};
|
||||
|
||||
var result: Face = .{
|
||||
.font = ct_font,
|
||||
.hb_font = hb_font,
|
||||
.presentation = if (traits.color_glyphs) .emoji else .text,
|
||||
.metrics = try calcMetrics(ct_font),
|
||||
.metrics = metrics,
|
||||
};
|
||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||
|
||||
@ -140,10 +149,10 @@ pub const Face = struct {
|
||||
|
||||
/// Return a new face that is the same as this but has a transformation
|
||||
/// matrix applied to italicize it.
|
||||
pub fn italicize(self: *const Face) !Face {
|
||||
pub fn italicize(self: *const Face, opts: font.face.Options) !Face {
|
||||
const ct_font = try self.font.copyWithAttributes(0.0, &italic_skew, null);
|
||||
errdefer ct_font.release();
|
||||
return try initFont(ct_font);
|
||||
return try initFont(ct_font, opts);
|
||||
}
|
||||
|
||||
/// Returns the font name. If allocation is required, buf will be used,
|
||||
@ -161,9 +170,9 @@ pub const Face = struct {
|
||||
|
||||
/// Resize the font in-place. If this succeeds, the caller is responsible
|
||||
/// for clearing any glyph caches, font atlas data, etc.
|
||||
pub fn setSize(self: *Face, size: font.face.DesiredSize) !void {
|
||||
pub fn setSize(self: *Face, opts: font.face.Options) !void {
|
||||
// We just create a copy and replace ourself
|
||||
const face = try initFontCopy(self.font, size);
|
||||
const face = try initFontCopy(self.font, opts);
|
||||
self.deinit();
|
||||
self.* = face;
|
||||
}
|
||||
@ -514,7 +523,7 @@ test {
|
||||
const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12);
|
||||
defer ct_font.release();
|
||||
|
||||
var face = try Face.initFontCopy(ct_font, .{ .points = 12 });
|
||||
var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
|
||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
||||
@ -537,7 +546,7 @@ test "emoji" {
|
||||
const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12);
|
||||
defer ct_font.release();
|
||||
|
||||
var face = try Face.initFontCopy(ct_font, .{ .points = 18 });
|
||||
var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 18 } });
|
||||
defer face.deinit();
|
||||
|
||||
// Presentation
|
||||
@ -558,7 +567,7 @@ test "in-memory" {
|
||||
var lib = try font.Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try Face.init(lib, testFont, .{ .points = 12 });
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
|
||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
||||
@ -582,7 +591,7 @@ test "variable" {
|
||||
var lib = try font.Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try Face.init(lib, testFont, .{ .points = 12 });
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
|
||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
||||
@ -606,7 +615,7 @@ test "variable set variation" {
|
||||
var lib = try font.Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try Face.init(lib, testFont, .{ .points = 12 });
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
|
||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
||||
|
@ -43,22 +43,22 @@ pub const Face = struct {
|
||||
quirks_disable_default_font_features: bool = false,
|
||||
|
||||
/// Initialize a new font face with the given source in-memory.
|
||||
pub fn initFile(lib: Library, path: [:0]const u8, index: i32, size: font.face.DesiredSize) !Face {
|
||||
pub fn initFile(lib: Library, path: [:0]const u8, index: i32, opts: font.face.Options) !Face {
|
||||
const face = try lib.lib.initFace(path, index);
|
||||
errdefer face.deinit();
|
||||
return try initFace(lib, face, size);
|
||||
return try initFace(lib, face, opts);
|
||||
}
|
||||
|
||||
/// Initialize a new font face with the given source in-memory.
|
||||
pub fn init(lib: Library, source: [:0]const u8, size: font.face.DesiredSize) !Face {
|
||||
pub fn init(lib: Library, source: [:0]const u8, opts: font.face.Options) !Face {
|
||||
const face = try lib.lib.initMemoryFace(source, 0);
|
||||
errdefer face.deinit();
|
||||
return try initFace(lib, face, size);
|
||||
return try initFace(lib, face, opts);
|
||||
}
|
||||
|
||||
fn initFace(lib: Library, face: freetype.Face, size: font.face.DesiredSize) !Face {
|
||||
fn initFace(lib: Library, face: freetype.Face, opts: font.face.Options) !Face {
|
||||
try face.selectCharmap(.unicode);
|
||||
try setSize_(face, size);
|
||||
try setSize_(face, opts.size);
|
||||
|
||||
var hb_font = try harfbuzz.freetype.createFont(face.handle);
|
||||
errdefer hb_font.destroy();
|
||||
@ -68,7 +68,7 @@ pub const Face = struct {
|
||||
.face = face,
|
||||
.hb_font = hb_font,
|
||||
.presentation = if (face.hasColor()) .emoji else .text,
|
||||
.metrics = calcMetrics(face),
|
||||
.metrics = calcMetrics(face, opts.metric_modifiers),
|
||||
};
|
||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||
|
||||
@ -127,9 +127,9 @@ pub const Face = struct {
|
||||
|
||||
/// Resize the font in-place. If this succeeds, the caller is responsible
|
||||
/// for clearing any glyph caches, font atlas data, etc.
|
||||
pub fn setSize(self: *Face, size: font.face.DesiredSize) !void {
|
||||
try setSize_(self.face, size);
|
||||
self.metrics = calcMetrics(self.face);
|
||||
pub fn setSize(self: *Face, opts: font.face.Options) !void {
|
||||
try setSize_(self.face, opts.size);
|
||||
self.metrics = calcMetrics(self.face, opts.metric_modifiers);
|
||||
}
|
||||
|
||||
fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void {
|
||||
@ -165,6 +165,7 @@ pub const Face = struct {
|
||||
pub fn setVariations(
|
||||
self: *Face,
|
||||
vs: []const font.face.Variation,
|
||||
opts: font.face.Options,
|
||||
) !void {
|
||||
// If this font doesn't support variations, we can't do anything.
|
||||
if (!self.face.hasMultipleMasters() or vs.len == 0) return;
|
||||
@ -201,7 +202,7 @@ pub const Face = struct {
|
||||
try self.face.setVarDesignCoordinates(coords);
|
||||
|
||||
// We need to recalculate font metrics which may have changed.
|
||||
self.metrics = calcMetrics(self.face);
|
||||
self.metrics = calcMetrics(self.face, opts.metric_modifiers);
|
||||
}
|
||||
|
||||
/// Returns the glyph index for the given Unicode code point. If this
|
||||
@ -470,7 +471,10 @@ pub const Face = struct {
|
||||
/// the faces with DeferredFaces and reload on demand. A Face can't be converted
|
||||
/// into a DeferredFace but a Face that comes from a DeferredFace can be
|
||||
/// deinitialized anytime and reloaded with the deferred face.
|
||||
fn calcMetrics(face: freetype.Face) font.face.Metrics {
|
||||
fn calcMetrics(
|
||||
face: freetype.Face,
|
||||
modifiers: ?*const font.face.Metrics.ModifierSet,
|
||||
) font.face.Metrics {
|
||||
const size_metrics = face.handle.*.size.*.metrics;
|
||||
|
||||
// Cell width is calculated by preferring to use 'M' as the width of a
|
||||
@ -574,7 +578,7 @@ pub const Face = struct {
|
||||
.thickness = underline_thickness,
|
||||
};
|
||||
|
||||
const result = font.face.Metrics{
|
||||
var result = font.face.Metrics{
|
||||
.cell_width = @intFromFloat(cell_width),
|
||||
.cell_height = @intFromFloat(cell_height),
|
||||
.cell_baseline = @intFromFloat(cell_baseline),
|
||||
@ -583,6 +587,7 @@ pub const Face = struct {
|
||||
.strikethrough_position = @intFromFloat(strikethrough.pos),
|
||||
.strikethrough_thickness = @intFromFloat(strikethrough.thickness),
|
||||
};
|
||||
if (modifiers) |m| result.apply(m.*);
|
||||
|
||||
// std.log.warn("font metrics={}", .{result});
|
||||
|
||||
@ -607,7 +612,11 @@ test {
|
||||
var atlas = try font.Atlas.init(alloc, 512, .greyscale);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var ft_font = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 });
|
||||
var ft_font = try Face.init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
);
|
||||
defer ft_font.deinit();
|
||||
|
||||
try testing.expectEqual(Presentation.text, ft_font.presentation);
|
||||
@ -623,7 +632,7 @@ test {
|
||||
const g1 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, .{});
|
||||
try testing.expectEqual(@as(u32, 11), g1.height);
|
||||
|
||||
try ft_font.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 });
|
||||
try ft_font.setSize(.{ .size = .{ .points = 24, .xdpi = 96, .ydpi = 96 } });
|
||||
const g2 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, .{});
|
||||
try testing.expectEqual(@as(u32, 21), g2.height);
|
||||
}
|
||||
@ -639,7 +648,11 @@ test "color emoji" {
|
||||
var atlas = try font.Atlas.init(alloc, 512, .rgba);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var ft_font = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 });
|
||||
var ft_font = try Face.init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
);
|
||||
defer ft_font.deinit();
|
||||
|
||||
try testing.expectEqual(Presentation.emoji, ft_font.presentation);
|
||||
@ -665,7 +678,11 @@ test "metrics" {
|
||||
var atlas = try font.Atlas.init(alloc, 512, .greyscale);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var ft_font = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 });
|
||||
var ft_font = try Face.init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
);
|
||||
defer ft_font.deinit();
|
||||
|
||||
try testing.expectEqual(font.face.Metrics{
|
||||
@ -679,7 +696,7 @@ test "metrics" {
|
||||
}, ft_font.metrics);
|
||||
|
||||
// Resize should change metrics
|
||||
try ft_font.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 });
|
||||
try ft_font.setSize(.{ .size = .{ .points = 24, .xdpi = 96, .ydpi = 96 } });
|
||||
try testing.expectEqual(font.face.Metrics{
|
||||
.cell_width = 16,
|
||||
.cell_height = 35,
|
||||
@ -701,7 +718,7 @@ test "mono to rgba" {
|
||||
var atlas = try font.Atlas.init(alloc, 512, .rgba);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var ft_font = try Face.init(lib, testFont, .{ .points = 12 });
|
||||
var ft_font = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
defer ft_font.deinit();
|
||||
|
||||
// glyph 3 is mono in Noto
|
||||
|
@ -899,11 +899,19 @@ fn testShaper(alloc: Allocator) !TestShaper {
|
||||
errdefer cache_ptr.*.deinit(alloc);
|
||||
|
||||
// Setup group
|
||||
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) });
|
||||
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(
|
||||
lib,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12 } },
|
||||
) });
|
||||
|
||||
if (font.options.backend != .coretext) {
|
||||
// Coretext doesn't support Noto's format
|
||||
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(lib, testEmoji, .{ .points = 12 }) });
|
||||
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(
|
||||
lib,
|
||||
testEmoji,
|
||||
.{ .size = .{ .points = 12 } },
|
||||
) });
|
||||
} else {
|
||||
// On CoreText we want to load Apple Emoji, we should have it.
|
||||
var disco = font.Discover.init();
|
||||
@ -918,7 +926,11 @@ fn testShaper(alloc: Allocator) !TestShaper {
|
||||
errdefer face.deinit();
|
||||
_ = try cache_ptr.group.addFace(.regular, .{ .deferred = face });
|
||||
}
|
||||
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(lib, testEmojiText, .{ .points = 12 }) });
|
||||
_ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(
|
||||
lib,
|
||||
testEmojiText,
|
||||
.{ .size = .{ .points = 12 } },
|
||||
) });
|
||||
|
||||
var shaper = try Shaper.init(alloc, .{});
|
||||
errdefer shaper.deinit();
|
||||
|
@ -171,8 +171,9 @@ const Draw = struct {
|
||||
// wave. This constant is arbitrary, change it for aesthetics.
|
||||
const pos = pos: {
|
||||
const MIN_HEIGHT = 7;
|
||||
const height = y_max - self.pos;
|
||||
break :pos if (height < MIN_HEIGHT) self.pos -| MIN_HEIGHT else self.pos;
|
||||
const clamped_pos = @min(y_max, self.pos);
|
||||
const height = y_max - clamped_pos;
|
||||
break :pos if (height < MIN_HEIGHT) clamped_pos -| MIN_HEIGHT else clamped_pos;
|
||||
};
|
||||
|
||||
// The full heightof the wave can be from the bottom to the
|
||||
|
@ -211,7 +211,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
||||
options.font_group.group.sprite = font.sprite.Face{
|
||||
.width = metrics.cell_width,
|
||||
.height = metrics.cell_height,
|
||||
.thickness = 2 * @as(u32, if (options.config.font_thicken) 2 else 1),
|
||||
.thickness = metrics.underline_thickness *
|
||||
@as(u32, if (options.config.font_thicken) 2 else 1),
|
||||
.underline_position = metrics.underline_position,
|
||||
};
|
||||
|
||||
|
@ -548,7 +548,7 @@ fn resetFontMetrics(
|
||||
font_group.group.sprite = font.sprite.Face{
|
||||
.width = metrics.cell_width,
|
||||
.height = metrics.cell_height,
|
||||
.thickness = 2 * @as(u32, if (font_thicken) 2 else 1),
|
||||
.thickness = metrics.underline_thickness * @as(u32, if (font_thicken) 2 else 1),
|
||||
.underline_position = metrics.underline_position,
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user