From f0080529c42ccccbeb0340a7e36534695c621807 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 19 Mar 2025 14:50:42 -0600 Subject: [PATCH] fix(font/shape): don't require emoji presentation for grapheme parts Also update shaper test that fails because the run iterator can't apply that logic since `testWriteString` doesn't do proper grpaheme clustering so the parts are actually split across multiple cells. Several other tests are technically incorrect for the same reason but still pass, so I've decided not to fix them here. --- src/font/shaper/coretext.zig | 32 +++++++++++++++++++++----------- src/font/shaper/harfbuzz.zig | 32 +++++++++++++++++++++----------- src/font/shaper/run.zig | 9 +++++++-- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index e084a68c9..ec64fe6eb 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -1015,25 +1015,35 @@ test "shape emoji width long" { var testdata = try testShaper(alloc); defer testdata.deinit(); - var buf: [32]u8 = undefined; - var buf_idx: usize = 0; - buf_idx += try std.unicode.utf8Encode(0x1F9D4, buf[buf_idx..]); // man: beard - buf_idx += try std.unicode.utf8Encode(0x1F3FB, buf[buf_idx..]); // light skin tone (Fitz 1-2) - buf_idx += try std.unicode.utf8Encode(0x200D, buf[buf_idx..]); // ZWJ - buf_idx += try std.unicode.utf8Encode(0x2642, buf[buf_idx..]); // male sign - buf_idx += try std.unicode.utf8Encode(0xFE0F, buf[buf_idx..]); // emoji representation - - // Make a screen with some data + // Make a screen and add a long emoji sequence to it. var screen = try terminal.Screen.init(alloc, 30, 3, 0); defer screen.deinit(); - try screen.testWriteString(buf[0..buf_idx]); + + var page = screen.pages.pages.first.?.data; + var row = page.getRow(1); + const cell = &row.cells.ptr(page.memory)[0]; + cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 0x1F9D4 }, // Person with beard + }; + var graphemes = [_]u21{ + 0x1F3FB, // Light skin tone (Fitz 1-2) + 0x200D, // ZWJ + 0x2642, // Male sign + 0xFE0F, // Emoji presentation selector + }; + try page.setGraphemes( + row, + cell, + graphemes[0..], + ); // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( testdata.grid, &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, null, null, ); diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 97292b9b0..b284dc140 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -540,25 +540,35 @@ test "shape emoji width long" { var testdata = try testShaper(alloc); defer testdata.deinit(); - var buf: [32]u8 = undefined; - var buf_idx: usize = 0; - buf_idx += try std.unicode.utf8Encode(0x1F9D4, buf[buf_idx..]); // man: beard - buf_idx += try std.unicode.utf8Encode(0x1F3FB, buf[buf_idx..]); // light skin tone (Fitz 1-2) - buf_idx += try std.unicode.utf8Encode(0x200D, buf[buf_idx..]); // ZWJ - buf_idx += try std.unicode.utf8Encode(0x2642, buf[buf_idx..]); // male sign - buf_idx += try std.unicode.utf8Encode(0xFE0F, buf[buf_idx..]); // emoji representation - - // Make a screen with some data + // Make a screen and add a long emoji sequence to it. var screen = try terminal.Screen.init(alloc, 30, 3, 0); defer screen.deinit(); - try screen.testWriteString(buf[0..buf_idx]); + + var page = screen.pages.pages.first.?.data; + var row = page.getRow(1); + const cell = &row.cells.ptr(page.memory)[0]; + cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 0x1F9D4 }, // Person with beard + }; + var graphemes = [_]u21{ + 0x1F3FB, // Light skin tone (Fitz 1-2) + 0x200D, // ZWJ + 0x2642, // Male sign + 0xFE0F, // Emoji presentation selector + }; + try page.setGraphemes( + row, + cell, + graphemes[0..], + ); // Get our run iterator var shaper = &testdata.shaper; var it = shaper.runIterator( testdata.grid, &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, null, null, ); diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 22d19979e..18ddd4b56 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -360,11 +360,16 @@ pub const RunIterator = struct { // Find a font that supports this codepoint. If none support this // then the whole grapheme can't be rendered so we return null. + // + // We explicitly do not require the additional grapheme components + // to support the base presentation, since it is common for emoji + // fonts to support the base emoji with emoji presentation but not + // certain ZWJ-combined characters like the male and female signs. const idx = try self.grid.getIndex( alloc, cp, style, - presentation, + null, ) orelse return null; candidates.appendAssumeCapacity(idx); } @@ -375,7 +380,7 @@ pub const RunIterator = struct { for (cps) |cp| { // Ignore Emoji ZWJs if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue; - if (!self.grid.hasCodepoint(idx, cp, presentation)) break; + if (!self.grid.hasCodepoint(idx, cp, null)) break; } else { // If the while completed, then we have a candidate that // supports all of our codepoints.