From 712cc9e55c4dfc006c8e9767c07aee3af96dcbb3 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 14 Nov 2025 16:50:18 -0700 Subject: [PATCH] fix(shaper/coretext): handle non-monotonic runs by sorting --- src/font/shaper/coretext.zig | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index d73b191b8..45844d3e2 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -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; }