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.
This commit is contained in:
Qwerasd
2025-03-19 14:50:42 -06:00
parent 6f84a5d682
commit f0080529c4
3 changed files with 49 additions and 24 deletions

View File

@@ -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,
);

View File

@@ -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,
);

View File

@@ -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.