diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index b15bbcb27..e695e5a74 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -294,13 +294,14 @@ fn getIndexCodepointOverride( pub fn getPresentation( self: *CodepointResolver, index: Collection.Index, + glyph_index: u32, ) !Presentation { if (index.special()) |sp| return switch (sp) { .sprite => .text, }; const face = try self.collection.getFace(index); - return face.presentation; + return if (face.isColorGlyph(glyph_index)) .emoji else .text; } /// Render a glyph by glyph index into the given font atlas and return diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 38f4b92f8..86231b839 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -190,16 +190,23 @@ pub fn autoItalicize(self: *Collection, alloc: Allocator) !void { const list = self.faces.get(.regular); if (list.items.len == 0) return; - // Find our first font that is text. This will force - // loading any deferred faces but we only load them until - // we find a text face. A text face is almost always the - // first face in the list. + // Find our first regular face that has text glyphs. for (0..list.items.len) |i| { const face = try self.getFace(.{ .style = .regular, .idx = @intCast(i), }); - if (face.presentation == .text) break :regular face; + + // We have two conditionals here. The color check is obvious: + // we want to auto-italicize a normal text font. The second + // check is less obvious... for mixed color/non-color fonts, we + // accept the regular font if it has basic ASCII. This may not + // be strictly correct (especially with international fonts) but + // it's a reasonable heuristic and the first case will match 99% + // of the time. + if (!face.hasColor() or face.glyphIndex('A') != null) { + break :regular face; + } } // No regular text face found. @@ -344,7 +351,13 @@ pub const Entry = union(enum) { }, .loaded => |face| switch (p_mode) { - .explicit => |p| face.presentation == p and face.glyphIndex(cp) != null, + .explicit => |p| explicit: { + const index = face.glyphIndex(cp) orelse break :explicit false; + break :explicit switch (p) { + .text => !face.isColorGlyph(index), + .emoji => face.isColorGlyph(index), + }; + }, .default, .any => face.glyphIndex(cp) != null, }, @@ -357,7 +370,13 @@ pub const Entry = union(enum) { .fallback_loaded => |face| switch (p_mode) { .explicit, .default, - => |p| face.presentation == p and face.glyphIndex(cp) != null, + => |p| explicit: { + const index = face.glyphIndex(cp) orelse break :explicit false; + break :explicit switch (p) { + .text => !face.isColorGlyph(index), + .emoji => face.isColorGlyph(index), + }; + }, .any => face.glyphIndex(cp) != null, }, }; @@ -371,7 +390,7 @@ pub const PresentationMode = union(enum) { explicit: Presentation, /// The codepoint has no explicit presentation and we should use - /// the presentation from the UCd. + /// the presentation from the UCD. default: Presentation, /// The codepoint can be any presentation. diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 8051895a4..94fbab445 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -281,6 +281,7 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { => { // If we are using coretext, we check the loaded CT font. if (self.ct) |ct| { + // TODO(mixed-fonts): handle presentation on a glyph level if (p) |desired_p| { const traits = ct.font.getSymbolicTraits(); const actual_p: Presentation = if (traits.color_glyphs) .emoji else .text; diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index 9c0f5ca9c..e99d39078 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -253,7 +253,7 @@ pub fn renderGlyph( if (gop.found_existing) return gop.value_ptr.*; // Get the presentation to determine what atlas to use - const p = try self.resolver.getPresentation(index); + const p = try self.resolver.getPresentation(index, glyph_index); const atlas: *font.Atlas = switch (p) { .text => &self.atlas_greyscale, .emoji => &self.atlas_color, diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 306b6e5be..4618ad0b7 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -19,9 +19,6 @@ pub const Face = struct { /// if we're using Harfbuzz. hb_font: if (harfbuzz_shaper) harfbuzz.Font else void, - /// The presentation for this font. - presentation: font.Presentation, - /// Metrics for this font face. These are useful for renderers. metrics: font.face.Metrics, @@ -111,26 +108,11 @@ pub const Face = struct { var result: Face = .{ .font = ct_font, .hb_font = hb_font, - .presentation = if (traits.color_glyphs) .emoji else .text, .metrics = metrics, .color = color, }; result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); - // If our presentation is emoji, we also check for the presence of - // emoji codepoints. This forces fonts with colorized glyphs that aren't - // emoji font to be treated as text. Long term, this isn't what we want - // but this fixes some bugs in the short term. See: - // https://github.com/mitchellh/ghostty/issues/1768 - // - // Longer term, we'd like to detect mixed color/non-color fonts and - // handle them correctly by rendering the color glyphs as color and the - // non-color glyphs as text. - if (result.presentation == .emoji and result.glyphIndex('🥸') == null) { - log.warn("font has colorized glyphs but isn't emoji, treating as text", .{}); - result.presentation = .text; - } - // In debug mode, we output information about available variation axes, // if they exist. if (comptime builtin.mode == .Debug) { @@ -244,15 +226,15 @@ pub const Face = struct { /// Returns true if the face has any glyphs that are colorized. /// To determine if an individual glyph is colorized you must use - /// isColored. + /// isColorGlyph. pub fn hasColor(self: *const Face) bool { return self.color != null; } /// Returns true if the given glyph ID is colorized. - pub fn isColored(self: *const Face, glyph_id: u16) bool { + pub fn isColorGlyph(self: *const Face, glyph_id: u32) bool { const c = self.color orelse return false; - return c.isColored(glyph_id); + return c.isColorGlyph(glyph_id); } /// Returns the glyph index for the given Unicode code point. If this @@ -328,7 +310,7 @@ pub const Face = struct { depth: u32, space: *macos.graphics.ColorSpace, context_opts: c_uint, - } = if (self.presentation == .text) .{ + } = if (!self.isColorGlyph(glyph_index)) .{ .color = false, .depth = 1, .space = try macos.graphics.ColorSpace.createDeviceGray(), @@ -669,13 +651,18 @@ const ColorState = struct { } /// Returns true if the given glyph ID is colored. - pub fn isColored(self: *const ColorState, glyph_id: u16) bool { + pub fn isColorGlyph(self: *const ColorState, glyph_id: u32) bool { + // Our font system uses 32-bit glyph IDs for special values but + // actual fonts only contain 16-bit glyph IDs so if we can't cast + // into it it must be false. + const glyph_u16 = std.math.cast(u16, glyph_id) orelse return false; + // sbix is always true for now if (self.sbix) return true; // if we have svg data, check it if (self.svg) |svg| { - if (svg.hasGlyph(glyph_id)) return true; + if (svg.hasGlyph(glyph_u16)) return true; } return false; @@ -699,8 +686,6 @@ test { var face = try Face.initFontCopy(ct_font, .{ .size = .{ .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) { @@ -722,8 +707,6 @@ test "name" { var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 12 } }); defer face.deinit(); - try testing.expectEqual(font.Presentation.text, face.presentation); - var buf: [1024]u8 = undefined; const font_name = try face.name(&buf); try testing.expect(std.mem.eql(u8, font_name, "Menlo")); @@ -742,13 +725,10 @@ test "emoji" { var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 18 } }); defer face.deinit(); - // Presentation - try testing.expectEqual(font.Presentation.emoji, face.presentation); - // Glyph index check { const id = face.glyphIndex('🥸').?; - try testing.expect(face.isColored(id)); + try testing.expect(face.isColorGlyph(id)); } } @@ -766,8 +746,6 @@ test "in-memory" { var face = try Face.init(lib, testFont, .{ .size = .{ .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) { @@ -790,8 +768,6 @@ test "variable" { var face = try Face.init(lib, testFont, .{ .size = .{ .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) { @@ -814,8 +790,6 @@ test "variable set variation" { var face = try Face.init(lib, testFont, .{ .size = .{ .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 }, }, .{ .size = .{ .points = 12 } }); @@ -828,19 +802,6 @@ test "variable set variation" { } } -test "mixed color/non-color font treated as text" { - const testing = std.testing; - const testFont = @import("../test.zig").fontJuliaMono; - - var lib = try font.Library.init(); - defer lib.deinit(); - - var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); - defer face.deinit(); - - try testing.expect(face.presentation == .text); -} - test "svg font table" { const testing = std.testing; const alloc = testing.allocator; @@ -871,12 +832,12 @@ test "glyphIndex colored vs text" { { const glyph = face.glyphIndex('A').?; try testing.expectEqual(4, glyph); - try testing.expect(!face.isColored(glyph)); + try testing.expect(!face.isColorGlyph(glyph)); } { const glyph = face.glyphIndex(0xE800).?; try testing.expectEqual(11482, glyph); - try testing.expect(face.isColored(glyph)); + try testing.expect(face.isColorGlyph(glyph)); } }