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 2c1da2b6a..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); } diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 67bde3b0d..fb3c8322e 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; } }; 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..ac14aaef8 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -64,11 +64,33 @@ 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(); const traits = ct_font.getSymbolicTraits(); + // Get variation axes + // if (ct_font.copyAttribute(.variation_axes)) |axes| { + // defer axes.release(); + // const len = axes.getCount(); + // for (0..len) |i| { + // const dict = axes.getValueAtIndex(macos.foundation.Dictionary, i); + // const Key = macos.text.FontVariationAxisKey; + // const name_ = dict.getValue(Key.name.Value(), Key.name.key()); + // if (name_) |name_val| { + // var buf: [1024]u8 = undefined; + // const namestr = name_val.cstring(&buf, .utf8) orelse ""; + // log.warn("AXES: {s}", .{namestr}); + // } + // } + // } + var result: Face = .{ .font = ct_font, .hb_font = hb_font, @@ -89,8 +111,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 +137,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 +539,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/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");