diff --git a/pkg/freetype/Library.zig b/pkg/freetype/Library.zig index ab3be153a..f5dc606ef 100644 --- a/pkg/freetype/Library.zig +++ b/pkg/freetype/Library.zig @@ -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, diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index fd7514d8f..2bd4a33c7 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -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 diff --git a/pkg/freetype/freetype-zig.h b/pkg/freetype/freetype-zig.h index ddb242be8..e430aef52 100644 --- a/pkg/freetype/freetype-zig.h +++ b/pkg/freetype/freetype-zig.h @@ -1,5 +1,6 @@ #include #include FT_FREETYPE_H #include FT_TRUETYPE_TABLES_H +#include #include #include diff --git a/pkg/macos/foundation/array.zig b/pkg/macos/foundation/array.zig index 35d3a0aba..4524cda6b 100644 --- a/pkg/macos/foundation/array.zig +++ b/pkg/macos/foundation/array.zig @@ -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( diff --git a/pkg/macos/text/font.zig b/pkg/macos/text/font.zig index 40238bfdd..0cef4d8a6 100644 --- a/pkg/macos/text/font.zig +++ b/pkg/macos/text/font.zig @@ -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( diff --git a/pkg/macos/text/font_descriptor.zig b/pkg/macos/text/font_descriptor.zig index f7f8097a1..c39e03a42 100644 --- a/pkg/macos/text/font_descriptor.zig +++ b/pkg/macos/text/font_descriptor.zig @@ -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, diff --git a/src/Surface.zig b/src/Surface.zig index 8bacd4886..85aa6c78b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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| { diff --git a/src/config.zig b/src/config.zig index d91e96689..6f4544124 100644 --- a/src/config.zig +++ b/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 = .{}, diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 6d0d6fe31..acf5ba93f 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -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( diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 67bde3b0d..d1dc6a601 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -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, }, }; } diff --git a/src/font/face.zig b/src/font/face.zig index 458a49f62..765b7dfe2 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -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())); +} diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 99d074453..7b738a322 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -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).?, .{}); + } +} diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 7b104eb01..9ae861c7d 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -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 { diff --git a/src/font/res/Lilex-VF.ttf b/src/font/res/Lilex-VF.ttf new file mode 100644 index 000000000..ad0c1aca5 Binary files /dev/null and b/src/font/res/Lilex-VF.ttf differ diff --git a/src/font/test.zig b/src/font/test.zig index c8f2d90e5..f07cefcd5 100644 --- a/src/font/test.zig +++ b/src/font/test.zig @@ -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");