mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
Merge pull request #345 from mitchellh/variable-fonts
Variable font support
This commit is contained in:
@ -57,6 +57,11 @@ pub fn initMemoryFace(self: Library, data: []const u8, index: i32) Error!Face {
|
||||
return face;
|
||||
}
|
||||
|
||||
/// Call when you're done with a loaded MM var.
|
||||
pub fn doneMMVar(self: Library, mm: *c.FT_MM_Var) void {
|
||||
_ = c.FT_Done_MM_Var(self.handle, mm);
|
||||
}
|
||||
|
||||
pub const Version = struct {
|
||||
major: i32,
|
||||
minor: i32,
|
||||
|
@ -24,6 +24,12 @@ pub const Face = struct {
|
||||
return c.FT_HAS_COLOR(self.handle);
|
||||
}
|
||||
|
||||
/// A macro that returns true whenever a face object contains some
|
||||
/// multiple masters.
|
||||
pub fn hasMultipleMasters(self: Face) bool {
|
||||
return c.FT_HAS_MULTIPLE_MASTERS(self.handle);
|
||||
}
|
||||
|
||||
/// A macro that returns true whenever a face object contains a scalable
|
||||
/// font face (true for TrueType, Type 1, Type 42, CID, OpenType/CFF,
|
||||
/// and PFR font formats).
|
||||
@ -95,6 +101,33 @@ pub const Face = struct {
|
||||
const res = c.FT_Get_Sfnt_Name(self.handle, @intCast(i), &name);
|
||||
return if (intToError(res)) |_| name else |err| err;
|
||||
}
|
||||
|
||||
/// Retrieve the font variation descriptor for a font.
|
||||
pub fn getMMVar(self: Face) Error!*c.FT_MM_Var {
|
||||
var result: *c.FT_MM_Var = undefined;
|
||||
const res = c.FT_Get_MM_Var(self.handle, @ptrCast(&result));
|
||||
return if (intToError(res)) |_| result else |err| err;
|
||||
}
|
||||
|
||||
/// Get the design coordinates of the currently selected interpolated font.
|
||||
pub fn getVarDesignCoordinates(self: Face, coords: []c.FT_Fixed) Error!void {
|
||||
const res = c.FT_Get_Var_Design_Coordinates(
|
||||
self.handle,
|
||||
@intCast(coords.len),
|
||||
coords.ptr,
|
||||
);
|
||||
return intToError(res);
|
||||
}
|
||||
|
||||
/// Choose an interpolated font design through design coordinates.
|
||||
pub fn setVarDesignCoordinates(self: Face, coords: []c.FT_Fixed) Error!void {
|
||||
const res = c.FT_Set_Var_Design_Coordinates(
|
||||
self.handle,
|
||||
@intCast(coords.len),
|
||||
coords.ptr,
|
||||
);
|
||||
return intToError(res);
|
||||
}
|
||||
};
|
||||
|
||||
/// An enumeration to specify indices of SFNT tables loaded and parsed by
|
||||
|
@ -1,5 +1,6 @@
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
#include FT_TRUETYPE_TABLES_H
|
||||
#include <freetype/ftmm.h>
|
||||
#include <freetype/ftsnames.h>
|
||||
#include <freetype/ttnameid.h>
|
||||
|
@ -24,7 +24,7 @@ pub const Array = opaque {
|
||||
/// constness so that further API calls work correctly. The Foundation
|
||||
/// API doesn't properly mark things const/non-const.
|
||||
pub fn getValueAtIndex(self: *Array, comptime T: type, idx: usize) *T {
|
||||
return @ptrCast(CFArrayGetValueAtIndex(self, idx));
|
||||
return @ptrCast(@alignCast(CFArrayGetValueAtIndex(self, idx)));
|
||||
}
|
||||
|
||||
pub extern "c" fn CFArrayCreate(
|
||||
|
@ -39,6 +39,14 @@ pub const Font = opaque {
|
||||
c.CFRelease(self);
|
||||
}
|
||||
|
||||
pub fn retain(self: *Font) void {
|
||||
_ = c.CFRetain(self);
|
||||
}
|
||||
|
||||
pub fn copyDescriptor(self: *Font) *text.FontDescriptor {
|
||||
return @ptrCast(@constCast(c.CTFontCopyFontDescriptor(@ptrCast(self))));
|
||||
}
|
||||
|
||||
pub fn getGlyphsForCharacters(self: *Font, chars: []const u16, glyphs: []graphics.Glyph) bool {
|
||||
assert(chars.len == glyphs.len);
|
||||
return c.CTFontGetGlyphsForCharacters(
|
||||
|
@ -31,6 +31,21 @@ pub const FontDescriptor = opaque {
|
||||
) orelse Allocator.Error.OutOfMemory;
|
||||
}
|
||||
|
||||
pub fn createCopyWithVariation(
|
||||
original: *FontDescriptor,
|
||||
id: *foundation.Number,
|
||||
value: f64,
|
||||
) Allocator.Error!*FontDescriptor {
|
||||
return @as(
|
||||
?*FontDescriptor,
|
||||
@ptrCast(@constCast(c.CTFontDescriptorCreateCopyWithVariation(
|
||||
@ptrCast(original),
|
||||
@ptrCast(id),
|
||||
value,
|
||||
))),
|
||||
) orelse Allocator.Error.OutOfMemory;
|
||||
}
|
||||
|
||||
pub fn release(self: *FontDescriptor) void {
|
||||
c.CFRelease(self);
|
||||
}
|
||||
@ -75,6 +90,9 @@ pub const FontAttribute = enum {
|
||||
downloadable,
|
||||
downloaded,
|
||||
|
||||
// https://developer.apple.com/documentation/coretext/core_text_constants?language=objc
|
||||
variation_axes,
|
||||
|
||||
pub fn key(self: FontAttribute) *foundation.String {
|
||||
return @as(*foundation.String, @ptrFromInt(@intFromPtr(switch (self) {
|
||||
.url => c.kCTFontURLAttribute,
|
||||
@ -101,6 +119,7 @@ pub const FontAttribute = enum {
|
||||
.enabled => c.kCTFontEnabledAttribute,
|
||||
.downloadable => c.kCTFontDownloadableAttribute,
|
||||
.downloaded => c.kCTFontDownloadedAttribute,
|
||||
.variation_axes => c.kCTFontVariationAxesAttribute,
|
||||
})));
|
||||
}
|
||||
|
||||
@ -130,6 +149,7 @@ pub const FontAttribute = enum {
|
||||
.enabled => *foundation.Number,
|
||||
.downloadable => *anyopaque, // CFBoolean
|
||||
.downloaded => *anyopaque, // CFBoolean
|
||||
.variation_axes => ?*foundation.Array,
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -159,6 +179,38 @@ pub const FontTraitKey = enum {
|
||||
}
|
||||
};
|
||||
|
||||
// https://developer.apple.com/documentation/coretext/ctfont/font_variation_axis_dictionary_keys?language=objc
|
||||
pub const FontVariationAxisKey = enum {
|
||||
identifier,
|
||||
minimum_value,
|
||||
maximum_value,
|
||||
default_value,
|
||||
name,
|
||||
hidden,
|
||||
|
||||
pub fn key(self: FontVariationAxisKey) *foundation.String {
|
||||
return @as(*foundation.String, @ptrFromInt(@intFromPtr(switch (self) {
|
||||
.identifier => c.kCTFontVariationAxisIdentifierKey,
|
||||
.minimum_value => c.kCTFontVariationAxisMinimumValueKey,
|
||||
.maximum_value => c.kCTFontVariationAxisMaximumValueKey,
|
||||
.default_value => c.kCTFontVariationAxisDefaultValueKey,
|
||||
.name => c.kCTFontVariationAxisNameKey,
|
||||
.hidden => c.kCTFontVariationAxisHiddenKey,
|
||||
})));
|
||||
}
|
||||
|
||||
pub fn Value(comptime self: FontVariationAxisKey) type {
|
||||
return switch (self) {
|
||||
.identifier => foundation.Number,
|
||||
.minimum_value => foundation.Number,
|
||||
.maximum_value => foundation.Number,
|
||||
.default_value => foundation.Number,
|
||||
.name => foundation.String,
|
||||
.hidden => foundation.Number,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const FontSymbolicTraits = packed struct(u32) {
|
||||
italic: bool = false,
|
||||
bold: bool = false,
|
||||
|
@ -229,6 +229,7 @@ pub fn init(
|
||||
var disco_it = try disco.discover(.{
|
||||
.family = family,
|
||||
.size = font_size.points,
|
||||
.variations = config.@"font-variation".list.items,
|
||||
});
|
||||
defer disco_it.deinit();
|
||||
if (try disco_it.next()) |face| {
|
||||
@ -241,6 +242,7 @@ pub fn init(
|
||||
.family = family,
|
||||
.size = font_size.points,
|
||||
.bold = true,
|
||||
.variations = config.@"font-variation-bold".list.items,
|
||||
});
|
||||
defer disco_it.deinit();
|
||||
if (try disco_it.next()) |face| {
|
||||
@ -253,6 +255,7 @@ pub fn init(
|
||||
.family = family,
|
||||
.size = font_size.points,
|
||||
.italic = true,
|
||||
.variations = config.@"font-variation-italic".list.items,
|
||||
});
|
||||
defer disco_it.deinit();
|
||||
if (try disco_it.next()) |face| {
|
||||
@ -266,6 +269,7 @@ pub fn init(
|
||||
.size = font_size.points,
|
||||
.bold = true,
|
||||
.italic = true,
|
||||
.variations = config.@"font-variation-bold-italic".list.items,
|
||||
});
|
||||
defer disco_it.deinit();
|
||||
if (try disco_it.next()) |face| {
|
||||
|
116
src/config.zig
116
src/config.zig
@ -3,6 +3,7 @@ const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const fontpkg = @import("font/main.zig");
|
||||
const inputpkg = @import("input.zig");
|
||||
const terminal = @import("terminal/main.zig");
|
||||
const internal_os = @import("os/main.zig");
|
||||
@ -43,6 +44,30 @@ pub const Config = struct {
|
||||
else => 12,
|
||||
},
|
||||
|
||||
/// A repeatable configuration to set one or more font variations values
|
||||
/// for a variable font. A variable font is a single font, usually
|
||||
/// with a filename ending in "-VF.ttf" or "-VF.otf" that contains
|
||||
/// one or more configurable axes for things such as weight, slant,
|
||||
/// etc. Not all fonts support variations; only fonts that explicitly
|
||||
/// state they are variable fonts will work.
|
||||
///
|
||||
/// The format of this is "id=value" where "id" is the axis identifier.
|
||||
/// An axis identifier is always a 4 character string, such as "wght".
|
||||
/// To get the list of supported axes, look at your font documentation
|
||||
/// or use a font inspection tool.
|
||||
///
|
||||
/// Invalid ids and values are usually ignored. For example, if a font
|
||||
/// only supports weights from 100 to 700, setting "wght=800" will
|
||||
/// do nothing (it will not be clamped to 700). You must consult your
|
||||
/// font's documentation to see what values are supported.
|
||||
///
|
||||
/// Common axes are: "wght" (weight), "slnt" (slant), "ital" (italic),
|
||||
/// "opsz" (optical size), "wdth" (width), "GRAD" (gradient), etc.
|
||||
@"font-variation": RepeatableFontVariation = .{},
|
||||
@"font-variation-bold": RepeatableFontVariation = .{},
|
||||
@"font-variation-italic": RepeatableFontVariation = .{},
|
||||
@"font-variation-bold-italic": RepeatableFontVariation = .{},
|
||||
|
||||
/// Draw fonts with a thicker stroke, if supported. This is only supported
|
||||
/// currently on macOS.
|
||||
@"font-thicken": bool = false,
|
||||
@ -1217,6 +1242,97 @@ pub const RepeatableString = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// FontVariation is a repeatable configuration value that sets a single
|
||||
/// font variation value. Font variations are configurations for what
|
||||
/// are often called "variable fonts." The font files usually end in
|
||||
/// "-VF.ttf."
|
||||
///
|
||||
/// The value for this is in the format of `id=value` where `id` is the
|
||||
/// 4-character font variation axis identifier and `value` is the
|
||||
/// floating point value for that axis. For more details on font variations
|
||||
/// see the MDN font-variation-settings documentation since this copies that
|
||||
/// behavior almost exactly:
|
||||
///
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/font-variation-settings
|
||||
pub const RepeatableFontVariation = struct {
|
||||
const Self = @This();
|
||||
|
||||
// Allocator for the list is the arena for the parent config.
|
||||
list: std.ArrayListUnmanaged(fontpkg.face.Variation) = .{},
|
||||
|
||||
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void {
|
||||
const input = input_ orelse return error.ValueRequired;
|
||||
const eql_idx = std.mem.indexOf(u8, input, "=") orelse return error.InvalidFormat;
|
||||
const whitespace = " \t";
|
||||
const key = std.mem.trim(u8, input[0..eql_idx], whitespace);
|
||||
const value = std.mem.trim(u8, input[eql_idx + 1 ..], whitespace);
|
||||
if (key.len != 4) return error.InvalidFormat;
|
||||
try self.list.append(alloc, .{
|
||||
.id = fontpkg.face.Variation.Id.init(@ptrCast(key.ptr)),
|
||||
.value = std.fmt.parseFloat(f64, value) catch return error.InvalidFormat,
|
||||
});
|
||||
}
|
||||
|
||||
/// Deep copy of the struct. Required by Config.
|
||||
pub fn clone(self: *const Self, alloc: Allocator) !Self {
|
||||
return .{
|
||||
.list = try self.list.clone(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
/// Compare if two of our value are requal. Required by Config.
|
||||
pub fn equal(self: Self, other: Self) bool {
|
||||
const itemsA = self.list.items;
|
||||
const itemsB = other.list.items;
|
||||
if (itemsA.len != itemsB.len) return false;
|
||||
for (itemsA, itemsB) |a, b| {
|
||||
if (!std.meta.eql(a, b)) return false;
|
||||
} else return true;
|
||||
}
|
||||
|
||||
test "parseCLI" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var list: Self = .{};
|
||||
try list.parseCLI(alloc, "wght=200");
|
||||
try list.parseCLI(alloc, "slnt=-15");
|
||||
|
||||
try testing.expectEqual(@as(usize, 2), list.list.items.len);
|
||||
try testing.expectEqual(fontpkg.face.Variation{
|
||||
.id = fontpkg.face.Variation.Id.init("wght"),
|
||||
.value = 200,
|
||||
}, list.list.items[0]);
|
||||
try testing.expectEqual(fontpkg.face.Variation{
|
||||
.id = fontpkg.face.Variation.Id.init("slnt"),
|
||||
.value = -15,
|
||||
}, list.list.items[1]);
|
||||
}
|
||||
|
||||
test "parseCLI with whitespace" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var list: Self = .{};
|
||||
try list.parseCLI(alloc, "wght =200");
|
||||
try list.parseCLI(alloc, "slnt= -15");
|
||||
|
||||
try testing.expectEqual(@as(usize, 2), list.list.items.len);
|
||||
try testing.expectEqual(fontpkg.face.Variation{
|
||||
.id = fontpkg.face.Variation.Id.init("wght"),
|
||||
.value = 200,
|
||||
}, list.list.items[0]);
|
||||
try testing.expectEqual(fontpkg.face.Variation{
|
||||
.id = fontpkg.face.Variation.Id.init("slnt"),
|
||||
.value = -15,
|
||||
}, list.list.items[1]);
|
||||
}
|
||||
};
|
||||
|
||||
/// Stores a set of keybinds.
|
||||
pub const Keybinds = struct {
|
||||
set: inputpkg.Binding.Set = .{},
|
||||
|
@ -54,6 +54,9 @@ pub const Fontconfig = struct {
|
||||
charset: *const fontconfig.CharSet,
|
||||
langset: *const fontconfig.LangSet,
|
||||
|
||||
/// Variations to apply to this font.
|
||||
variations: []const font.face.Variation,
|
||||
|
||||
pub fn deinit(self: *Fontconfig) void {
|
||||
self.pattern.destroy();
|
||||
self.* = undefined;
|
||||
@ -154,7 +157,10 @@ fn loadFontconfig(
|
||||
const filename = (try fc.pattern.get(.file, 0)).string;
|
||||
const face_index = (try fc.pattern.get(.index, 0)).integer;
|
||||
|
||||
return try Face.initFile(lib, filename, face_index, size);
|
||||
var face = try Face.initFile(lib, filename, face_index, size);
|
||||
errdefer face.deinit();
|
||||
try face.setVariations(fc.variations);
|
||||
return face;
|
||||
}
|
||||
|
||||
fn loadCoreText(
|
||||
|
@ -5,6 +5,7 @@ const fontconfig = @import("fontconfig");
|
||||
const macos = @import("macos");
|
||||
const options = @import("main.zig").options;
|
||||
const DeferredFace = @import("main.zig").DeferredFace;
|
||||
const Variation = @import("main.zig").face.Variation;
|
||||
|
||||
const log = std.log.scoped(.discovery);
|
||||
|
||||
@ -43,6 +44,11 @@ pub const Descriptor = struct {
|
||||
bold: bool = false,
|
||||
italic: bool = false,
|
||||
|
||||
/// Variation axes to apply to the font. This also impacts searching
|
||||
/// for fonts since fonts with the ability to set these variations
|
||||
/// will be preferred, but not guaranteed.
|
||||
variations: []const Variation = &.{},
|
||||
|
||||
/// Convert to Fontconfig pattern to use for lookup. The pattern does
|
||||
/// not have defaults filled/substituted (Fontconfig thing) so callers
|
||||
/// must still do this.
|
||||
@ -149,7 +155,21 @@ pub const Descriptor = struct {
|
||||
);
|
||||
}
|
||||
|
||||
return try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs));
|
||||
// Build our descriptor from attrs
|
||||
var desc = try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs));
|
||||
errdefer desc.release();
|
||||
|
||||
// Variations are built by copying the descriptor. I don't know a way
|
||||
// to set it on attrs directly.
|
||||
for (self.variations) |v| {
|
||||
const id = try macos.foundation.Number.create(.int, @ptrCast(&v.id));
|
||||
defer id.release();
|
||||
const next = try desc.createCopyWithVariation(id, v.value);
|
||||
desc.release();
|
||||
desc = next;
|
||||
}
|
||||
|
||||
return desc;
|
||||
}
|
||||
};
|
||||
|
||||
@ -185,6 +205,7 @@ pub const Fontconfig = struct {
|
||||
.pattern = pat,
|
||||
.set = res.fs,
|
||||
.fonts = res.fs.fonts(),
|
||||
.variations = desc.variations,
|
||||
.i = 0,
|
||||
};
|
||||
}
|
||||
@ -194,6 +215,7 @@ pub const Fontconfig = struct {
|
||||
pattern: *fontconfig.Pattern,
|
||||
set: *fontconfig.FontSet,
|
||||
fonts: []*fontconfig.Pattern,
|
||||
variations: []const Variation,
|
||||
i: usize,
|
||||
|
||||
pub fn deinit(self: *DiscoverIterator) void {
|
||||
@ -221,6 +243,7 @@ pub const Fontconfig = struct {
|
||||
.pattern = font_pattern,
|
||||
.charset = (try font_pattern.get(.charset, 0)).char_set,
|
||||
.langset = (try font_pattern.get(.lang, 0)).lang_set,
|
||||
.variations = self.variations,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const options = @import("main.zig").options;
|
||||
const freetype = @import("face/freetype.zig");
|
||||
@ -36,6 +37,31 @@ pub const DesiredSize = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// A font variation setting. The best documentation for this I know of
|
||||
/// is actually the CSS font-variation-settings property on MDN:
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/CSS/font-variation-settings
|
||||
pub const Variation = struct {
|
||||
id: Id,
|
||||
value: f64,
|
||||
|
||||
pub const Id = packed struct(u32) {
|
||||
d: u8,
|
||||
c: u8,
|
||||
b: u8,
|
||||
a: u8,
|
||||
|
||||
pub fn init(v: *const [4]u8) Id {
|
||||
return .{ .a = v[0], .b = v[1], .c = v[2], .d = v[3] };
|
||||
}
|
||||
|
||||
/// Converts the ID to a string. The return value is only valid
|
||||
/// for the lifetime of the self pointer.
|
||||
pub fn str(self: Id) [4]u8 {
|
||||
return .{ self.a, self.b, self.c, self.d };
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/// 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.
|
||||
@ -77,3 +103,17 @@ pub const Foo = if (options.backend == .coretext) coretext.Face else void;
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
|
||||
test "Variation.Id: wght should be 2003265652" {
|
||||
const testing = std.testing;
|
||||
const id = Variation.Id.init("wght");
|
||||
try testing.expectEqual(@as(u32, 2003265652), @as(u32, @bitCast(id)));
|
||||
try testing.expectEqualStrings("wght", &(id.str()));
|
||||
}
|
||||
|
||||
test "Variation.Id: slnt should be 1936486004" {
|
||||
const testing = std.testing;
|
||||
const id: Variation.Id = .{ .a = 's', .b = 'l', .c = 'n', .d = 't' };
|
||||
try testing.expectEqual(@as(u32, 1936486004), @as(u32, @bitCast(id)));
|
||||
try testing.expectEqualStrings("slnt", &(id.str()));
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const macos = @import("macos");
|
||||
@ -64,6 +65,12 @@ pub const Face = struct {
|
||||
const ct_font = try base.copyWithAttributes(@floatFromInt(size.pixels()), null, null);
|
||||
errdefer ct_font.release();
|
||||
|
||||
return try initFont(ct_font);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
var hb_font = try harfbuzz.coretext.createFont(ct_font);
|
||||
errdefer hb_font.destroy();
|
||||
|
||||
@ -76,6 +83,52 @@ pub const Face = struct {
|
||||
.metrics = try calcMetrics(ct_font),
|
||||
};
|
||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||
|
||||
// In debug mode, we output information about available variation axes,
|
||||
// if they exist.
|
||||
if (comptime builtin.mode == .Debug) {
|
||||
if (ct_font.copyAttribute(.variation_axes)) |axes| {
|
||||
defer axes.release();
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
log.debug("variation axes font={s}", .{try result.name(&buf)});
|
||||
|
||||
const len = axes.getCount();
|
||||
for (0..len) |i| {
|
||||
const dict = axes.getValueAtIndex(macos.foundation.Dictionary, i);
|
||||
const Key = macos.text.FontVariationAxisKey;
|
||||
const cf_name = dict.getValue(Key.name.Value(), Key.name.key()).?;
|
||||
const cf_id = dict.getValue(Key.identifier.Value(), Key.identifier.key()).?;
|
||||
const cf_min = dict.getValue(Key.minimum_value.Value(), Key.minimum_value.key()).?;
|
||||
const cf_max = dict.getValue(Key.maximum_value.Value(), Key.maximum_value.key()).?;
|
||||
const cf_def = dict.getValue(Key.default_value.Value(), Key.default_value.key()).?;
|
||||
|
||||
const namestr = cf_name.cstring(&buf, .utf8) orelse "";
|
||||
|
||||
var id_raw: c_int = 0;
|
||||
_ = cf_id.getValue(.int, &id_raw);
|
||||
const id: font.face.Variation.Id = @bitCast(id_raw);
|
||||
|
||||
var min: f64 = 0;
|
||||
_ = cf_min.getValue(.double, &min);
|
||||
|
||||
var max: f64 = 0;
|
||||
_ = cf_max.getValue(.double, &max);
|
||||
|
||||
var def: f64 = 0;
|
||||
_ = cf_def.getValue(.double, &def);
|
||||
|
||||
log.debug("variation axis: name={s} id={s} min={} max={} def={}", .{
|
||||
namestr,
|
||||
id.str(),
|
||||
min,
|
||||
max,
|
||||
def,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -89,8 +142,8 @@ pub const Face = struct {
|
||||
/// matrix applied to italicize it.
|
||||
pub fn italicize(self: *const Face) !Face {
|
||||
const ct_font = try self.font.copyWithAttributes(0.0, &italic_skew, null);
|
||||
defer ct_font.release();
|
||||
return try initFontCopy(ct_font, .{ .points = 0 });
|
||||
errdefer ct_font.release();
|
||||
return try initFont(ct_font);
|
||||
}
|
||||
|
||||
/// Returns the font name. If allocation is required, buf will be used,
|
||||
@ -115,6 +168,31 @@ pub const Face = struct {
|
||||
self.* = face;
|
||||
}
|
||||
|
||||
/// Set the variation axes for this font. This will modify this font
|
||||
/// in-place.
|
||||
pub fn setVariations(
|
||||
self: *Face,
|
||||
vs: []const font.face.Variation,
|
||||
) !void {
|
||||
// Create a new font descriptor with all the variations set.
|
||||
var desc = self.font.copyDescriptor();
|
||||
defer desc.release();
|
||||
for (vs) |v| {
|
||||
const id = try macos.foundation.Number.create(.int, @ptrCast(&v.id));
|
||||
defer id.release();
|
||||
const next = try desc.createCopyWithVariation(id, v.value);
|
||||
desc.release();
|
||||
desc = next;
|
||||
}
|
||||
|
||||
// Initialize a font based on these attributes.
|
||||
const ct_font = try self.font.copyWithAttributes(0, null, desc);
|
||||
errdefer ct_font.release();
|
||||
const face = try initFont(ct_font);
|
||||
self.deinit();
|
||||
self.* = face;
|
||||
}
|
||||
|
||||
/// Returns the glyph index for the given Unicode code point. If this
|
||||
/// face doesn't support this glyph, null is returned.
|
||||
pub fn glyphIndex(self: Face, cp: u32) ?u32 {
|
||||
@ -492,3 +570,55 @@ test "in-memory" {
|
||||
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, .{});
|
||||
}
|
||||
}
|
||||
|
||||
test "variable" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const testFont = @import("../test.zig").fontVariable;
|
||||
|
||||
var atlas = try font.Atlas.init(alloc, 512, .greyscale);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var lib = try font.Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try Face.init(lib, testFont, .{ .points = 12 });
|
||||
defer face.deinit();
|
||||
|
||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
||||
|
||||
// Generate all visible ASCII
|
||||
var i: u8 = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
try testing.expect(face.glyphIndex(i) != null);
|
||||
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, .{});
|
||||
}
|
||||
}
|
||||
|
||||
test "variable set variation" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const testFont = @import("../test.zig").fontVariable;
|
||||
|
||||
var atlas = try font.Atlas.init(alloc, 512, .greyscale);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var lib = try font.Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try Face.init(lib, testFont, .{ .points = 12 });
|
||||
defer face.deinit();
|
||||
|
||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
||||
|
||||
try face.setVariations(&.{
|
||||
.{ .id = font.face.Variation.Id.init("wght"), .value = 400 },
|
||||
});
|
||||
|
||||
// Generate all visible ASCII
|
||||
var i: u8 = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
try testing.expect(face.glyphIndex(i) != null);
|
||||
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, .{});
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,9 @@ const quirks = @import("../../quirks.zig");
|
||||
const log = std.log.scoped(.font_face);
|
||||
|
||||
pub const Face = struct {
|
||||
/// Our freetype library
|
||||
lib: freetype.Library,
|
||||
|
||||
/// Our font face.
|
||||
face: freetype.Face,
|
||||
|
||||
@ -43,30 +46,55 @@ pub const Face = struct {
|
||||
pub fn initFile(lib: Library, path: [:0]const u8, index: i32, size: font.face.DesiredSize) !Face {
|
||||
const face = try lib.lib.initFace(path, index);
|
||||
errdefer face.deinit();
|
||||
return try initFace(face, size);
|
||||
return try initFace(lib, face, size);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
const face = try lib.lib.initMemoryFace(source, 0);
|
||||
errdefer face.deinit();
|
||||
return try initFace(face, size);
|
||||
return try initFace(lib, face, size);
|
||||
}
|
||||
|
||||
fn initFace(face: freetype.Face, size: font.face.DesiredSize) !Face {
|
||||
fn initFace(lib: Library, face: freetype.Face, size: font.face.DesiredSize) !Face {
|
||||
try face.selectCharmap(.unicode);
|
||||
try setSize_(face, size);
|
||||
|
||||
const hb_font = try harfbuzz.freetype.createFont(face.handle);
|
||||
var hb_font = try harfbuzz.freetype.createFont(face.handle);
|
||||
errdefer hb_font.destroy();
|
||||
|
||||
var result: Face = .{
|
||||
.lib = lib.lib,
|
||||
.face = face,
|
||||
.hb_font = hb_font,
|
||||
.presentation = if (face.hasColor()) .emoji else .text,
|
||||
.metrics = calcMetrics(face),
|
||||
};
|
||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||
|
||||
// In debug mode, we output information about available variation axes,
|
||||
// if they exist.
|
||||
if (comptime builtin.mode == .Debug) mm: {
|
||||
if (!face.hasMultipleMasters()) break :mm;
|
||||
var buf: [1024]u8 = undefined;
|
||||
log.debug("variation axes font={s}", .{try result.name(&buf)});
|
||||
|
||||
const mm = try face.getMMVar();
|
||||
defer lib.lib.doneMMVar(mm);
|
||||
for (0..mm.num_axis) |i| {
|
||||
const axis = mm.axis[i];
|
||||
const id_raw = std.math.cast(c_int, axis.tag) orelse continue;
|
||||
const id: font.face.Variation.Id = @bitCast(id_raw);
|
||||
log.debug("variation axis: name={s} id={s} min={} max={} def={}", .{
|
||||
std.mem.sliceTo(axis.name, 0),
|
||||
id.str(),
|
||||
axis.minimum >> 16,
|
||||
axis.maximum >> 16,
|
||||
axis.def >> 16,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -132,6 +160,50 @@ pub const Face = struct {
|
||||
try face.selectSize(best_i);
|
||||
}
|
||||
|
||||
/// Set the variation axes for this font. This will modify this font
|
||||
/// in-place.
|
||||
pub fn setVariations(
|
||||
self: *Face,
|
||||
vs: []const font.face.Variation,
|
||||
) !void {
|
||||
// If this font doesn't support variations, we can't do anything.
|
||||
if (!self.face.hasMultipleMasters() or vs.len == 0) return;
|
||||
|
||||
// Freetype requires that we send ALL coordinates in at once so the
|
||||
// first thing we have to do is get all the vars and put them into
|
||||
// an array.
|
||||
const mm = try self.face.getMMVar();
|
||||
defer self.lib.doneMMVar(mm);
|
||||
|
||||
// To avoid allocations, we cap the number of variation axes we can
|
||||
// support. This is arbitrary but Firefox caps this at 16 so I
|
||||
// feel like that's probably safe... and we do double cause its
|
||||
// cheap.
|
||||
var coords_buf: [32]freetype.c.FT_Fixed = undefined;
|
||||
var coords = coords_buf[0..@min(coords_buf.len, mm.num_axis)];
|
||||
try self.face.getVarDesignCoordinates(coords);
|
||||
|
||||
// Now we go through each axis and see if its set. This is slow
|
||||
// but there usually aren't many axes and usually not many set
|
||||
// variations, either.
|
||||
for (0..mm.num_axis) |i| {
|
||||
const axis = mm.axis[i];
|
||||
const id = std.math.cast(u32, axis.tag) orelse continue;
|
||||
for (vs) |v| {
|
||||
if (id == @as(u32, @bitCast(v.id))) {
|
||||
coords[i] = @intFromFloat(v.value * 65536);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set them!
|
||||
try self.face.setVarDesignCoordinates(coords);
|
||||
|
||||
// We need to recalculate font metrics which may have changed.
|
||||
self.metrics = calcMetrics(self.face);
|
||||
}
|
||||
|
||||
/// Returns the glyph index for the given Unicode code point. If this
|
||||
/// face doesn't support this glyph, null is returned.
|
||||
pub fn glyphIndex(self: Face, cp: u32) ?u32 {
|
||||
|
BIN
src/font/res/Lilex-VF.ttf
Normal file
BIN
src/font/res/Lilex-VF.ttf
Normal file
Binary file not shown.
@ -2,3 +2,4 @@ pub const fontRegular = @embedFile("res/Inconsolata-Regular.ttf");
|
||||
pub const fontBold = @embedFile("res/Inconsolata-Bold.ttf");
|
||||
pub const fontEmoji = @embedFile("res/NotoColorEmoji.ttf");
|
||||
pub const fontEmojiText = @embedFile("res/NotoEmoji-Regular.ttf");
|
||||
pub const fontVariable = @embedFile("res/Lilex-VF.ttf");
|
||||
|
Reference in New Issue
Block a user