diff --git a/pkg/macos/text/font.zig b/pkg/macos/text/font.zig index cf24bc5d0..40238bfdd 100644 --- a/pkg/macos/text/font.zig +++ b/pkg/macos/text/font.zig @@ -18,13 +18,18 @@ pub const Font = opaque { ) orelse Allocator.Error.OutOfMemory; } - pub fn copyWithAttributes(self: *Font, size: f32, attrs: ?*text.FontDescriptor) Allocator.Error!*Font { + pub fn copyWithAttributes( + self: *Font, + size: f32, + matrix: ?*const graphics.AffineTransform, + attrs: ?*text.FontDescriptor, + ) Allocator.Error!*Font { return @as( ?*Font, @ptrFromInt(@intFromPtr(c.CTFontCreateCopyWithAttributes( @ptrCast(self), size, - null, + @ptrCast(matrix), @ptrCast(attrs), ))), ) orelse Allocator.Error.OutOfMemory; @@ -217,6 +222,6 @@ test "copy" { const font = try Font.createWithFontDescriptor(desc, 12); defer font.release(); - const f2 = try font.copyWithAttributes(14, null); + const f2 = try font.copyWithAttributes(14, null, null); defer f2.release(); } diff --git a/src/Surface.zig b/src/Surface.zig index 85e462c87..3393d8d31 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -231,7 +231,7 @@ pub fn init( if (try disco_it.next()) |face| { log.info("font regular: {s}", .{try face.name()}); try group.addFace(alloc, .regular, face); - } else std.log.warn("font-family not found: {s}", .{family}); + } else log.warn("font-family not found: {s}", .{family}); } if (config.@"font-family-bold") |family| { var disco_it = try disco.discover(.{ @@ -243,7 +243,7 @@ pub fn init( if (try disco_it.next()) |face| { log.info("font bold: {s}", .{try face.name()}); try group.addFace(alloc, .bold, face); - } else std.log.warn("font-family-bold not found: {s}", .{family}); + } else log.warn("font-family-bold not found: {s}", .{family}); } if (config.@"font-family-italic") |family| { var disco_it = try disco.discover(.{ @@ -255,7 +255,7 @@ pub fn init( if (try disco_it.next()) |face| { log.info("font italic: {s}", .{try face.name()}); try group.addFace(alloc, .italic, face); - } else std.log.warn("font-family-italic not found: {s}", .{family}); + } else log.warn("font-family-italic not found: {s}", .{family}); } if (config.@"font-family-bold-italic") |family| { var disco_it = try disco.discover(.{ @@ -268,7 +268,7 @@ pub fn init( if (try disco_it.next()) |face| { log.info("font bold+italic: {s}", .{try face.name()}); try group.addFace(alloc, .bold_italic, face); - } else std.log.warn("font-family-bold-italic not found: {s}", .{family}); + } else log.warn("font-family-bold-italic not found: {s}", .{family}); } } @@ -284,6 +284,26 @@ pub fn init( font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_bold_ttf, font_size)), ); + // If we support auto-italicization and we don't have an italic face, + // then we can try to auto-italicize our regular face. + if (comptime font.DeferredFace.canItalicize()) { + if (group.getFace(.italic) == null) { + if (group.getFace(.regular)) |regular| { + if (try regular.italicize()) |face| { + log.info("font auto-italicized: {s}", .{try face.name()}); + try group.addFace(alloc, .italic, face); + } + } + } + } else { + // We don't support auto-italics. If we don't have an italic font + // face let the user know so they aren't surprised (if they look + // at logs). + if (group.getFace(.italic) == null) { + log.warn("no italic font face available, italics will not render", .{}); + } + } + // Emoji fallback. We don't include this on Mac since Mac is expected // to always have the Apple Emoji available. if (builtin.os.tag != .macos or font.Discover == void) { diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 7625876e2..f5f43c48e 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -19,6 +19,17 @@ const Presentation = @import("main.zig").Presentation; const log = std.log.scoped(.deferred_face); +/// The struct used for deferred face state. +/// +/// TODO: Change the "fc", "ct", "wc" fields in @This to just use one field +/// with the state since there should be no world in which multiple are used. +const FaceState = switch (options.backend) { + .freetype => void, + .fontconfig_freetype => Fontconfig, + .coretext_freetype, .coretext => CoreText, + .web_canvas => WebCanvas, +}; + /// The loaded face (once loaded). face: ?Face = null, @@ -61,6 +72,13 @@ pub const CoreText = struct { self.font.release(); self.* = undefined; } + + /// Auto-italicize the font by applying a skew. + pub fn italicize(self: *const CoreText) !CoreText { + const ct_font = try self.font.copyWithAttributes(0.0, &Face.italic_skew, null); + errdefer ct_font.release(); + return .{ .font = ct_font }; + } }; /// WebCanvas specific data. This is only present when building with canvas. @@ -333,6 +351,40 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { unreachable; } +/// Returns true if our deferred font implementation supports auto-itacilization. +pub fn canItalicize() bool { + return @hasDecl(FaceState, "italicize") and @hasDecl(Face, "italicize"); +} + +/// Returns a new deferred face with the italicized version of this face +/// by applying a skew. This is NOT TRUE italics. You should use the discovery +/// mechanism to try to find an italic font. This is a fallback for when +/// that fails. +pub fn italicize(self: *const DeferredFace) !?DeferredFace { + if (comptime !canItalicize()) return null; + + var result: DeferredFace = .{}; + + if (self.face) |face| { + result.face = try face.italicize(); + } + + switch (options.backend) { + .freetype => {}, + .fontconfig_freetype => if (self.fc) |*fc| { + result.fc = try fc.italicize(); + }, + .coretext, .coretext_freetype => if (self.ct) |*ct| { + result.ct = try ct.italicize(); + }, + .web_canvas => if (self.wc) |*wc| { + result.wc = try wc.italicize(); + }, + } + + return result; +} + /// The wasm-compatible API. pub const Wasm = struct { const wasm = @import("../os/wasm.zig"); diff --git a/src/font/Group.zig b/src/font/Group.zig index 11f4f94be..251827af7 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -99,6 +99,15 @@ pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: DeferredFace) try list.append(alloc, face); } +/// Get the face for the given style. This will always return the first +/// face (if it exists). The returned pointer is only valid as long as +/// the faces do not change. +pub fn getFace(self: *Group, style: Style) ?*DeferredFace { + const list = self.faces.getPtr(style); + if (list.items.len == 0) return null; + return &list.items[0]; +} + /// Resize the fonts to the desired size. pub fn setSize(self: *Group, size: font.face.DesiredSize) !void { // Note: there are some issues here with partial failure. We don't @@ -189,8 +198,15 @@ pub fn indexForCodepoint( // If we can find the exact value, then return that. if (self.indexForCodepointExact(cp, style, p)) |value| return value; - // Try looking for another font that will satisfy this request. - if (font.Discover != void) { + // If we're not a regular font style, try looking for a regular font + // that will satisfy this request. Blindly looking for unmatched styled + // fonts to satisfy one codepoint results in some ugly rendering. + if (style != .regular) { + if (self.indexForCodepoint(cp, .regular, p)) |value| return value; + } + + // If we are regular, try looking for a fallback using discovery. + if (style == .regular and font.Discover != void) { if (self.discover) |*disco| discover: { var disco_it = disco.discover(.{ .codepoint = cp, diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index efb1ebab8..b4db4617a 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -20,6 +20,16 @@ pub const Face = struct { /// Metrics for this font face. These are useful for renderers. metrics: font.face.Metrics, + /// The matrix applied to a regular font to auto-italicize it. + pub const italic_skew = macos.graphics.AffineTransform{ + .a = 1, + .b = 0, + .c = 0.267949, // approx. tan(15) + .d = 1, + .tx = 0, + .ty = 0, + }; + /// Initialize a CoreText-based font from a TTF/TTC in memory. pub fn init(lib: font.Library, source: [:0]const u8, size: font.face.DesiredSize) !Face { _ = lib; @@ -47,7 +57,7 @@ pub const Face = struct { // Create a copy. The copyWithAttributes docs say the size is in points, // but we need to scale the points by the DPI and to do that we use our // function called "pixels". - const ct_font = try base.copyWithAttributes(@floatFromInt(size.pixels()), null); + const ct_font = try base.copyWithAttributes(@floatFromInt(size.pixels()), null, null); errdefer ct_font.release(); var hb_font = try harfbuzz.coretext.createFont(ct_font); @@ -69,6 +79,14 @@ pub const Face = struct { self.* = undefined; } + /// Return a new face that is the same as this but has a transformation + /// matrix applied to italicize it. + pub fn italicize(self: *const Face) !Face { + const ct_font = try self.font.copyWithAttributes(0.0, &italic_skew, null); + defer ct_font.release(); + return try initFontCopy(ct_font, .{ .points = 0 }); + } + /// Resize the font in-place. If this succeeds, the caller is responsible /// for clearing any glyph caches, font atlas data, etc. pub fn setSize(self: *Face, size: font.face.DesiredSize) !void {