From 6addccdeeb450fbee6661c36e3ccdbecd94e94d4 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Thu, 11 Dec 2025 10:48:28 -0500 Subject: [PATCH] Add shape Tai Tham vowels test --- src/font/shaper/coretext.zig | 149 +++++++++++++++++++++++++---------- src/terminal/Terminal.zig | 2 +- 2 files changed, 108 insertions(+), 43 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 498b45799..32b7ab77b 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -390,8 +390,8 @@ pub const Shaper = struct { y: f64 = 0, // For debugging positions, turn this on: - start_index: usize = 0, - end_index: usize = 0, + //start_index: usize = 0, + //end_index: usize = 0, } = .{}; // Clear our cell buf and make sure we have enough room for the whole @@ -450,53 +450,56 @@ pub const Shaper = struct { .y = run_offset.y, // For debugging positions, turn this on: - .start_index = index, - .end_index = index, + //.start_index = index, + //.end_index = index, }; - } else { - if (index < cell_offset.start_index) { - cell_offset.start_index = index; - } - if (index > cell_offset.end_index) { - cell_offset.end_index = index; - } + + // For debugging positions, turn this on: + //} else { + // if (index < cell_offset.start_index) { + // cell_offset.start_index = index; + // } + // if (index > cell_offset.end_index) { + // cell_offset.end_index = index; + // } } const x_offset = position.x - cell_offset.x; const y_offset = position.y - cell_offset.y; - const advance_x_offset = run_offset.x - cell_offset.x; - const advance_y_offset = run_offset.y - cell_offset.y; - const x_offset_diff = x_offset - advance_x_offset; - const y_offset_diff = y_offset - advance_y_offset; + // Ford debugging positions, turn this on: + //const advance_x_offset = run_offset.x - cell_offset.x; + //const advance_y_offset = run_offset.y - cell_offset.y; + //const x_offset_diff = x_offset - advance_x_offset; + //const y_offset_diff = y_offset - advance_y_offset; - if (@abs(x_offset_diff) > 0.0001 or @abs(y_offset_diff) > 0.0001) { - var allocating = std.Io.Writer.Allocating.init(alloc); - const writer = &allocating.writer; - const codepoints = state.codepoints.items[cell_offset.start_index .. cell_offset.end_index + 1]; - for (codepoints) |cp| { - if (cp.codepoint == 0) continue; // Skip surrogate pair padding - try writer.print("\\u{{{x}}}", .{cp.codepoint}); - } - try writer.writeAll(" → "); - for (codepoints) |cp| { - if (cp.codepoint == 0) continue; // Skip surrogate pair padding - try writer.print("{u}", .{@as(u21, @intCast(cp.codepoint))}); - } - const formatted_cps = try allocating.toOwnedSlice(); + //if (@abs(x_offset_diff) > 0.0001 or @abs(y_offset_diff) > 0.0001) { + // var allocating = std.Io.Writer.Allocating.init(alloc); + // const writer = &allocating.writer; + // const codepoints = state.codepoints.items[cell_offset.start_index .. cell_offset.end_index + 1]; + // for (codepoints) |cp| { + // if (cp.codepoint == 0) continue; // Skip surrogate pair padding + // try writer.print("\\u{{{x}}}", .{cp.codepoint}); + // } + // try writer.writeAll(" → "); + // for (codepoints) |cp| { + // if (cp.codepoint == 0) continue; // Skip surrogate pair padding + // try writer.print("{u}", .{@as(u21, @intCast(cp.codepoint))}); + // } + // const formatted_cps = try allocating.toOwnedSlice(); - log.warn("position differs from advance: cluster={d} pos=({d:.2},{d:.2}) adv=({d:.2},{d:.2}) diff=({d:.2},{d:.2}) current cp={x}, cps={s}", .{ - cluster, - x_offset, - y_offset, - advance_x_offset, - advance_y_offset, - x_offset_diff, - y_offset_diff, - state.codepoints.items[index].codepoint, - formatted_cps, - }); - } + // log.warn("position differs from advance: cluster={d} pos=({d:.2},{d:.2}) adv=({d:.2},{d:.2}) diff=({d:.2},{d:.2}) current cp={x}, cps={s}", .{ + // cluster, + // x_offset, + // y_offset, + // advance_x_offset, + // advance_y_offset, + // x_offset_diff, + // y_offset_diff, + // state.codepoints.items[index].codepoint, + // formatted_cps, + // }); + //} self.cell_buf.appendAssumeCapacity(.{ .x = @intCast(cluster), @@ -1332,7 +1335,7 @@ test "shape with empty cells in between" { } } -test "shape Chinese characters" { +test "shape Combining characters" { const testing = std.testing; const alloc = testing.allocator; @@ -1350,6 +1353,9 @@ test "shape Chinese characters" { var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); defer t.deinit(alloc); + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + var s = t.vtStream(); defer s.deinit(); try s.nextSlice(buf[0..buf_idx]); @@ -1397,6 +1403,9 @@ test "shape Devanagari string" { var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); defer t.deinit(alloc); + // Disable grapheme clustering + t.modes.set(.grapheme_cluster, false); + var s = t.vtStream(); defer s.deinit(); try s.nextSlice("अपार्टमेंट"); @@ -1429,6 +1438,62 @@ test "shape Devanagari string" { try testing.expect(try it.next(alloc) == null); } +test "shape Tai Tham vowels (position differs from advance)" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Tai Tham for this to work, if we can't find + // Noto Sans Tai Tham, which is a system font on macOS, we just skip the + // test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Noto Sans Tai Tham", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + buf_idx += try std.unicode.utf8Encode(0x1a2F, buf[buf_idx..]); // ᨯ + buf_idx += try std.unicode.utf8Encode(0x1a70, buf[buf_idx..]); // ᩰ + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + const cell_width = run.grid.metrics.cell_width; + try testing.expectEqual(@as(usize, 2), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 0), cells[1].x); + + // The first glyph renders in the next cell + try testing.expectEqual(@as(i16, @intCast(cell_width)), cells[0].x_offset); + try testing.expectEqual(@as(i16, 0), cells[1].x_offset); + } + try testing.expectEqual(@as(usize, 1), count); +} + test "shape box glyphs" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 6c9db6a8d..b0d43c192 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -8893,7 +8893,7 @@ test "Terminal: insertBlanks shift graphemes" { var t = try init(alloc, .{ .rows = 5, .cols = 5 }); defer t.deinit(alloc); - // Disable grapheme clustering + // Enable grapheme clustering t.modes.set(.grapheme_cluster, true); try t.printString("A");