fix(shaper/coretext): handle non-monotonic runs by sorting

pull/9609/head
Qwerasd 2025-11-14 16:50:18 -07:00
parent 0a7da32c71
commit 712cc9e55c
1 changed files with 28 additions and 0 deletions

View File

@ -392,6 +392,12 @@ pub const Shaper = struct {
self.cell_buf.clearRetainingCapacity();
try self.cell_buf.ensureTotalCapacity(self.alloc, line.getGlyphCount());
// CoreText, despite our insistence with an enforced embedding level,
// may sometimes output runs that are non-monotonic. In order to fix
// this, we check the run status for each run and if any aren't ltr
// we set this to true, which indicates that we must sort our buffer.
var non_ltr: bool = false;
// CoreText may generate multiple runs even though our input to
// CoreText is already split into runs by our own run iterator.
// The runs as far as I can tell are always sequential to each
@ -401,6 +407,9 @@ pub const Shaper = struct {
for (0..runs.getCount()) |i| {
const ctrun = runs.getValueAtIndex(macos.text.Run, i);
const status = ctrun.getStatus();
if (status.non_monotonic or status.right_to_left) non_ltr = true;
// Get our glyphs and positions
const glyphs = ctrun.getGlyphsPtr() orelse try ctrun.getGlyphs(alloc);
const advances = ctrun.getAdvancesPtr() orelse try ctrun.getAdvances(alloc);
@ -441,6 +450,25 @@ pub const Shaper = struct {
}
}
// If our buffer contains some non-ltr sections we need to sort it :/
if (non_ltr) {
// This is EXCEPTIONALLY rare. Only happens for languages with
// complex shaping which we don't even really support properly
// right now, so are very unlikely to be used heavily by users
// of Ghostty.
@branchHint(.cold);
std.mem.sort(
font.shape.Cell,
self.cell_buf.items,
{},
struct {
fn lt(_: void, a: font.shape.Cell, b: font.shape.Cell) bool {
return a.x < b.x;
}
}.lt,
);
}
return self.cell_buf.items;
}