mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +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;
|
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 {
|
pub const Version = struct {
|
||||||
major: i32,
|
major: i32,
|
||||||
minor: i32,
|
minor: i32,
|
||||||
|
@ -24,6 +24,12 @@ pub const Face = struct {
|
|||||||
return c.FT_HAS_COLOR(self.handle);
|
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
|
/// A macro that returns true whenever a face object contains a scalable
|
||||||
/// font face (true for TrueType, Type 1, Type 42, CID, OpenType/CFF,
|
/// font face (true for TrueType, Type 1, Type 42, CID, OpenType/CFF,
|
||||||
/// and PFR font formats).
|
/// and PFR font formats).
|
||||||
@ -95,6 +101,33 @@ pub const Face = struct {
|
|||||||
const res = c.FT_Get_Sfnt_Name(self.handle, @intCast(i), &name);
|
const res = c.FT_Get_Sfnt_Name(self.handle, @intCast(i), &name);
|
||||||
return if (intToError(res)) |_| name else |err| err;
|
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
|
/// An enumeration to specify indices of SFNT tables loaded and parsed by
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#include <ft2build.h>
|
#include <ft2build.h>
|
||||||
#include FT_FREETYPE_H
|
#include FT_FREETYPE_H
|
||||||
#include FT_TRUETYPE_TABLES_H
|
#include FT_TRUETYPE_TABLES_H
|
||||||
|
#include <freetype/ftmm.h>
|
||||||
#include <freetype/ftsnames.h>
|
#include <freetype/ftsnames.h>
|
||||||
#include <freetype/ttnameid.h>
|
#include <freetype/ttnameid.h>
|
||||||
|
@ -24,7 +24,7 @@ pub const Array = opaque {
|
|||||||
/// constness so that further API calls work correctly. The Foundation
|
/// constness so that further API calls work correctly. The Foundation
|
||||||
/// API doesn't properly mark things const/non-const.
|
/// API doesn't properly mark things const/non-const.
|
||||||
pub fn getValueAtIndex(self: *Array, comptime T: type, idx: usize) *T {
|
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(
|
pub extern "c" fn CFArrayCreate(
|
||||||
|
@ -39,6 +39,14 @@ pub const Font = opaque {
|
|||||||
c.CFRelease(self);
|
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 {
|
pub fn getGlyphsForCharacters(self: *Font, chars: []const u16, glyphs: []graphics.Glyph) bool {
|
||||||
assert(chars.len == glyphs.len);
|
assert(chars.len == glyphs.len);
|
||||||
return c.CTFontGetGlyphsForCharacters(
|
return c.CTFontGetGlyphsForCharacters(
|
||||||
|
@ -31,6 +31,21 @@ pub const FontDescriptor = opaque {
|
|||||||
) orelse Allocator.Error.OutOfMemory;
|
) 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 {
|
pub fn release(self: *FontDescriptor) void {
|
||||||
c.CFRelease(self);
|
c.CFRelease(self);
|
||||||
}
|
}
|
||||||
@ -75,6 +90,9 @@ pub const FontAttribute = enum {
|
|||||||
downloadable,
|
downloadable,
|
||||||
downloaded,
|
downloaded,
|
||||||
|
|
||||||
|
// https://developer.apple.com/documentation/coretext/core_text_constants?language=objc
|
||||||
|
variation_axes,
|
||||||
|
|
||||||
pub fn key(self: FontAttribute) *foundation.String {
|
pub fn key(self: FontAttribute) *foundation.String {
|
||||||
return @as(*foundation.String, @ptrFromInt(@intFromPtr(switch (self) {
|
return @as(*foundation.String, @ptrFromInt(@intFromPtr(switch (self) {
|
||||||
.url => c.kCTFontURLAttribute,
|
.url => c.kCTFontURLAttribute,
|
||||||
@ -101,6 +119,7 @@ pub const FontAttribute = enum {
|
|||||||
.enabled => c.kCTFontEnabledAttribute,
|
.enabled => c.kCTFontEnabledAttribute,
|
||||||
.downloadable => c.kCTFontDownloadableAttribute,
|
.downloadable => c.kCTFontDownloadableAttribute,
|
||||||
.downloaded => c.kCTFontDownloadedAttribute,
|
.downloaded => c.kCTFontDownloadedAttribute,
|
||||||
|
.variation_axes => c.kCTFontVariationAxesAttribute,
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,6 +149,7 @@ pub const FontAttribute = enum {
|
|||||||
.enabled => *foundation.Number,
|
.enabled => *foundation.Number,
|
||||||
.downloadable => *anyopaque, // CFBoolean
|
.downloadable => *anyopaque, // CFBoolean
|
||||||
.downloaded => *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) {
|
pub const FontSymbolicTraits = packed struct(u32) {
|
||||||
italic: bool = false,
|
italic: bool = false,
|
||||||
bold: bool = false,
|
bold: bool = false,
|
||||||
|
@ -229,6 +229,7 @@ pub fn init(
|
|||||||
var disco_it = try disco.discover(.{
|
var disco_it = try disco.discover(.{
|
||||||
.family = family,
|
.family = family,
|
||||||
.size = font_size.points,
|
.size = font_size.points,
|
||||||
|
.variations = config.@"font-variation".list.items,
|
||||||
});
|
});
|
||||||
defer disco_it.deinit();
|
defer disco_it.deinit();
|
||||||
if (try disco_it.next()) |face| {
|
if (try disco_it.next()) |face| {
|
||||||
@ -241,6 +242,7 @@ pub fn init(
|
|||||||
.family = family,
|
.family = family,
|
||||||
.size = font_size.points,
|
.size = font_size.points,
|
||||||
.bold = true,
|
.bold = true,
|
||||||
|
.variations = config.@"font-variation-bold".list.items,
|
||||||
});
|
});
|
||||||
defer disco_it.deinit();
|
defer disco_it.deinit();
|
||||||
if (try disco_it.next()) |face| {
|
if (try disco_it.next()) |face| {
|
||||||
@ -253,6 +255,7 @@ pub fn init(
|
|||||||
.family = family,
|
.family = family,
|
||||||
.size = font_size.points,
|
.size = font_size.points,
|
||||||
.italic = true,
|
.italic = true,
|
||||||
|
.variations = config.@"font-variation-italic".list.items,
|
||||||
});
|
});
|
||||||
defer disco_it.deinit();
|
defer disco_it.deinit();
|
||||||
if (try disco_it.next()) |face| {
|
if (try disco_it.next()) |face| {
|
||||||
@ -266,6 +269,7 @@ pub fn init(
|
|||||||
.size = font_size.points,
|
.size = font_size.points,
|
||||||
.bold = true,
|
.bold = true,
|
||||||
.italic = true,
|
.italic = true,
|
||||||
|
.variations = config.@"font-variation-bold-italic".list.items,
|
||||||
});
|
});
|
||||||
defer disco_it.deinit();
|
defer disco_it.deinit();
|
||||||
if (try disco_it.next()) |face| {
|
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 builtin = @import("builtin");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
const fontpkg = @import("font/main.zig");
|
||||||
const inputpkg = @import("input.zig");
|
const inputpkg = @import("input.zig");
|
||||||
const terminal = @import("terminal/main.zig");
|
const terminal = @import("terminal/main.zig");
|
||||||
const internal_os = @import("os/main.zig");
|
const internal_os = @import("os/main.zig");
|
||||||
@ -43,6 +44,30 @@ pub const Config = struct {
|
|||||||
else => 12,
|
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
|
/// Draw fonts with a thicker stroke, if supported. This is only supported
|
||||||
/// currently on macOS.
|
/// currently on macOS.
|
||||||
@"font-thicken": bool = false,
|
@"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.
|
/// Stores a set of keybinds.
|
||||||
pub const Keybinds = struct {
|
pub const Keybinds = struct {
|
||||||
set: inputpkg.Binding.Set = .{},
|
set: inputpkg.Binding.Set = .{},
|
||||||
|
@ -54,6 +54,9 @@ pub const Fontconfig = struct {
|
|||||||
charset: *const fontconfig.CharSet,
|
charset: *const fontconfig.CharSet,
|
||||||
langset: *const fontconfig.LangSet,
|
langset: *const fontconfig.LangSet,
|
||||||
|
|
||||||
|
/// Variations to apply to this font.
|
||||||
|
variations: []const font.face.Variation,
|
||||||
|
|
||||||
pub fn deinit(self: *Fontconfig) void {
|
pub fn deinit(self: *Fontconfig) void {
|
||||||
self.pattern.destroy();
|
self.pattern.destroy();
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
@ -154,7 +157,10 @@ fn loadFontconfig(
|
|||||||
const filename = (try fc.pattern.get(.file, 0)).string;
|
const filename = (try fc.pattern.get(.file, 0)).string;
|
||||||
const face_index = (try fc.pattern.get(.index, 0)).integer;
|
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(
|
fn loadCoreText(
|
||||||
|
@ -5,6 +5,7 @@ const fontconfig = @import("fontconfig");
|
|||||||
const macos = @import("macos");
|
const macos = @import("macos");
|
||||||
const options = @import("main.zig").options;
|
const options = @import("main.zig").options;
|
||||||
const DeferredFace = @import("main.zig").DeferredFace;
|
const DeferredFace = @import("main.zig").DeferredFace;
|
||||||
|
const Variation = @import("main.zig").face.Variation;
|
||||||
|
|
||||||
const log = std.log.scoped(.discovery);
|
const log = std.log.scoped(.discovery);
|
||||||
|
|
||||||
@ -43,6 +44,11 @@ pub const Descriptor = struct {
|
|||||||
bold: bool = false,
|
bold: bool = false,
|
||||||
italic: 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
|
/// Convert to Fontconfig pattern to use for lookup. The pattern does
|
||||||
/// not have defaults filled/substituted (Fontconfig thing) so callers
|
/// not have defaults filled/substituted (Fontconfig thing) so callers
|
||||||
/// must still do this.
|
/// 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,
|
.pattern = pat,
|
||||||
.set = res.fs,
|
.set = res.fs,
|
||||||
.fonts = res.fs.fonts(),
|
.fonts = res.fs.fonts(),
|
||||||
|
.variations = desc.variations,
|
||||||
.i = 0,
|
.i = 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -194,6 +215,7 @@ pub const Fontconfig = struct {
|
|||||||
pattern: *fontconfig.Pattern,
|
pattern: *fontconfig.Pattern,
|
||||||
set: *fontconfig.FontSet,
|
set: *fontconfig.FontSet,
|
||||||
fonts: []*fontconfig.Pattern,
|
fonts: []*fontconfig.Pattern,
|
||||||
|
variations: []const Variation,
|
||||||
i: usize,
|
i: usize,
|
||||||
|
|
||||||
pub fn deinit(self: *DiscoverIterator) void {
|
pub fn deinit(self: *DiscoverIterator) void {
|
||||||
@ -221,6 +243,7 @@ pub const Fontconfig = struct {
|
|||||||
.pattern = font_pattern,
|
.pattern = font_pattern,
|
||||||
.charset = (try font_pattern.get(.charset, 0)).char_set,
|
.charset = (try font_pattern.get(.charset, 0)).char_set,
|
||||||
.langset = (try font_pattern.get(.lang, 0)).lang_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 builtin = @import("builtin");
|
||||||
const options = @import("main.zig").options;
|
const options = @import("main.zig").options;
|
||||||
const freetype = @import("face/freetype.zig");
|
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.
|
/// Metrics associated with the font that are useful for renderers to know.
|
||||||
pub const Metrics = struct {
|
pub const Metrics = struct {
|
||||||
/// Recommended cell width and height for a monospace grid using this font.
|
/// 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 {
|
test {
|
||||||
@import("std").testing.refAllDecls(@This());
|
@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 std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const macos = @import("macos");
|
const macos = @import("macos");
|
||||||
@ -64,6 +65,12 @@ pub const Face = struct {
|
|||||||
const ct_font = try base.copyWithAttributes(@floatFromInt(size.pixels()), null, null);
|
const ct_font = try base.copyWithAttributes(@floatFromInt(size.pixels()), null, null);
|
||||||
errdefer ct_font.release();
|
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);
|
var hb_font = try harfbuzz.coretext.createFont(ct_font);
|
||||||
errdefer hb_font.destroy();
|
errdefer hb_font.destroy();
|
||||||
|
|
||||||
@ -76,6 +83,52 @@ pub const Face = struct {
|
|||||||
.metrics = try calcMetrics(ct_font),
|
.metrics = try calcMetrics(ct_font),
|
||||||
};
|
};
|
||||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,8 +142,8 @@ pub const Face = struct {
|
|||||||
/// matrix applied to italicize it.
|
/// matrix applied to italicize it.
|
||||||
pub fn italicize(self: *const Face) !Face {
|
pub fn italicize(self: *const Face) !Face {
|
||||||
const ct_font = try self.font.copyWithAttributes(0.0, &italic_skew, null);
|
const ct_font = try self.font.copyWithAttributes(0.0, &italic_skew, null);
|
||||||
defer ct_font.release();
|
errdefer ct_font.release();
|
||||||
return try initFontCopy(ct_font, .{ .points = 0 });
|
return try initFont(ct_font);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the font name. If allocation is required, buf will be used,
|
/// Returns the font name. If allocation is required, buf will be used,
|
||||||
@ -115,6 +168,31 @@ pub const Face = struct {
|
|||||||
self.* = face;
|
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
|
/// Returns the glyph index for the given Unicode code point. If this
|
||||||
/// face doesn't support this glyph, null is returned.
|
/// face doesn't support this glyph, null is returned.
|
||||||
pub fn glyphIndex(self: Face, cp: u32) ?u32 {
|
pub fn glyphIndex(self: Face, cp: u32) ?u32 {
|
||||||
@ -492,3 +570,55 @@ test "in-memory" {
|
|||||||
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, .{});
|
_ = 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);
|
const log = std.log.scoped(.font_face);
|
||||||
|
|
||||||
pub const Face = struct {
|
pub const Face = struct {
|
||||||
|
/// Our freetype library
|
||||||
|
lib: freetype.Library,
|
||||||
|
|
||||||
/// Our font face.
|
/// Our font face.
|
||||||
face: freetype.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 {
|
pub fn initFile(lib: Library, path: [:0]const u8, index: i32, size: font.face.DesiredSize) !Face {
|
||||||
const face = try lib.lib.initFace(path, index);
|
const face = try lib.lib.initFace(path, index);
|
||||||
errdefer face.deinit();
|
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.
|
/// 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, size: font.face.DesiredSize) !Face {
|
||||||
const face = try lib.lib.initMemoryFace(source, 0);
|
const face = try lib.lib.initMemoryFace(source, 0);
|
||||||
errdefer face.deinit();
|
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 face.selectCharmap(.unicode);
|
||||||
try setSize_(face, size);
|
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();
|
errdefer hb_font.destroy();
|
||||||
|
|
||||||
var result: Face = .{
|
var result: Face = .{
|
||||||
|
.lib = lib.lib,
|
||||||
.face = face,
|
.face = face,
|
||||||
.hb_font = hb_font,
|
.hb_font = hb_font,
|
||||||
.presentation = if (face.hasColor()) .emoji else .text,
|
.presentation = if (face.hasColor()) .emoji else .text,
|
||||||
.metrics = calcMetrics(face),
|
.metrics = calcMetrics(face),
|
||||||
};
|
};
|
||||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,6 +160,50 @@ pub const Face = struct {
|
|||||||
try face.selectSize(best_i);
|
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
|
/// Returns the glyph index for the given Unicode code point. If this
|
||||||
/// face doesn't support this glyph, null is returned.
|
/// face doesn't support this glyph, null is returned.
|
||||||
pub fn glyphIndex(self: Face, cp: u32) ?u32 {
|
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 fontBold = @embedFile("res/Inconsolata-Bold.ttf");
|
||||||
pub const fontEmoji = @embedFile("res/NotoColorEmoji.ttf");
|
pub const fontEmoji = @embedFile("res/NotoColorEmoji.ttf");
|
||||||
pub const fontEmojiText = @embedFile("res/NotoEmoji-Regular.ttf");
|
pub const fontEmojiText = @embedFile("res/NotoEmoji-Regular.ttf");
|
||||||
|
pub const fontVariable = @embedFile("res/Lilex-VF.ttf");
|
||||||
|
Reference in New Issue
Block a user