diff --git a/pkg/macos/text/font.zig b/pkg/macos/text/font.zig index c26a0f32f..e0c1f4891 100644 --- a/pkg/macos/text/font.zig +++ b/pkg/macos/text/font.zig @@ -18,6 +18,18 @@ pub const Font = opaque { ) orelse Allocator.Error.OutOfMemory; } + pub fn createForString( + self: *Font, + str: *foundation.String, + range: foundation.Range, + ) ?*Font { + return @ptrCast(@constCast(c.CTFontCreateForString( + @ptrCast(self), + @ptrCast(str), + @bitCast(range), + ))); + } + pub fn copyWithAttributes( self: *Font, size: f32, @@ -51,6 +63,10 @@ pub const Font = opaque { return @ptrCast(@constCast(c.CTFontCopyFeatures(@ptrCast(self)))); } + pub fn copyDefaultCascadeListForLanguages(self: *Font) *foundation.Array { + return @ptrCast(@constCast(c.CTFontCopyDefaultCascadeListForLanguages(@ptrCast(self), null))); + } + pub fn getGlyphCount(self: *Font) usize { return @intCast(c.CTFontGetGlyphCount(@ptrCast(self))); } diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index 370d933cb..a0073f9ad 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -172,7 +172,7 @@ pub fn getIndex( if (self.discover) |disco| discover: { const load_opts = self.collection.load_options orelse break :discover; - var disco_it = disco.discover(alloc, .{ + var disco_it = disco.discoverFallback(alloc, &self.collection, .{ .codepoint = cp, .size = load_opts.size.points, .bold = style == .bold or style == .bold_italic, diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 0259200c9..9cfbe4a05 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -5,6 +5,7 @@ const assert = std.debug.assert; const fontconfig = @import("fontconfig"); const macos = @import("macos"); const options = @import("main.zig").options; +const Collection = @import("main.zig").Collection; const DeferredFace = @import("main.zig").DeferredFace; const Variation = @import("main.zig").face.Variation; @@ -258,7 +259,11 @@ pub const Fontconfig = struct { /// Discover fonts from a descriptor. This returns an iterator that can /// be used to build up the deferred fonts. - pub fn discover(self: *const Fontconfig, alloc: Allocator, desc: Descriptor) !DiscoverIterator { + pub fn discover( + self: *const Fontconfig, + alloc: Allocator, + desc: Descriptor, + ) !DiscoverIterator { _ = alloc; // Build our pattern that we'll search for @@ -282,6 +287,16 @@ pub const Fontconfig = struct { }; } + pub fn discoverFallback( + self: *const Fontconfig, + alloc: Allocator, + collection: *Collection, + desc: Descriptor, + ) !DiscoverIterator { + _ = collection; + return try self.discover(alloc, desc); + } + pub const DiscoverIterator = struct { config: *fontconfig.Config, pattern: *fontconfig.Pattern, @@ -364,6 +379,87 @@ pub const CoreText = struct { }; } + pub fn discoverFallback( + self: *const CoreText, + alloc: Allocator, + collection: *Collection, + desc: Descriptor, + ) !DiscoverIterator { + // If we have a codepoint within the CJK unified ideographs block + // then we fallback to macOS to find a font that supports it because + // there isn't a better way manually with CoreText that I can find that + // properly takes into account system locale. + // + // References: + // - http://unicode.org/charts/PDF/U4E00.pdf + // - https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/platform/fonts/LocaleInFonts.md#unified-han-ideographs + if (desc.codepoint >= 0x4E00 and + desc.codepoint <= 0x9FFF) + han: { + const han = try self.discoverCodepoint( + collection, + desc, + ) orelse break :han; + + // This is silly but our discover iterator needs a slice so + // we allocate here. This isn't a performance bottleneck but + // this is something we can optimize very easily... + const list = try alloc.alloc(*macos.text.FontDescriptor, 1); + errdefer alloc.free(list); + list[0] = han; + + return DiscoverIterator{ + .alloc = alloc, + .list = list, + .i = 0, + }; + } + + return try self.discover(alloc, desc); + } + + /// Discover a font for a specific codepoint using the CoreText + /// CTFontCreateForString API. + fn discoverCodepoint( + self: *const CoreText, + collection: *Collection, + desc: Descriptor, + ) !?*macos.text.FontDescriptor { + _ = self; + + assert(desc.codepoint > 0); + + // Get our original font. This is dependent on the requestd style + // from the descriptor. + const original = original: { + break :original try collection.getFace(.{ .style = .regular }); + }; + + // We need it in utf8 format + var buf: [4]u8 = undefined; + const len = try std.unicode.utf8Encode( + @intCast(desc.codepoint), + &buf, + ); + + // We need a CFString + const str = try macos.foundation.String.createWithBytes( + buf[0..len], + .utf8, + false, + ); + defer str.release(); + + // Get our font + const font = original.font.createForString( + str, + macos.foundation.Range.init(0, 1), + ) orelse return null; + defer font.release(); + + // Get the descriptor + return font.copyDescriptor(); + } fn copyMatchingDescriptors( alloc: Allocator, list: *macos.foundation.Array,