Skip to content

fix(rioterm): render RTL runs (Arabic/Farsi/Hebrew) correctly - #1877

Open
saeidakbari wants to merge 1 commit into
raphamorim:mainfrom
saeidakbari:fix/rtl-text-rendering
Open

saeidakbari wants to merge 1 commit into
raphamorim:mainfrom
saeidakbari:fix/rtl-text-rendering

Conversation

@saeidakbari

@saeidakbari saeidakbari commented Aug 16, 2026 •

Copy link
Copy Markdown

Summary

RTL text (Arabic, Farsi, Hebrew) rendered with every glyph crammed into one column. After a naive per-cell reversal, narrow glyphs (alef) still showed gaps inside words like آیا.

Root cause

build_row_fg's cluster→cell walk was forward-only:

while cell_starts[cell_idx + 1] <= g.cluster { cell_idx += 1 }

Shapers (CoreText, HarfBuzz/swash) return RTL runs in visual order with monotonically decreasing clusters — glyph 0 is the logically-last character. So the cursor jumped to the run's last cell on glyph 0 and stayed pinned: every glyph was emitted at the same grid_pos.

Additionally, snapping proportional Arabic glyphs to monospace cell origins leaves whitespace before narrow glyphs.

Fix

attribute_glyphs_to_cells() detects RTL via first.cluster > last.cluster (the documented shaper convention) and lays RTL runs out pen-relative:

  • a pen advances by each glyph's real shaped advance from the run's left edge
  • the glyph's cell is floor(pen / cell_w)
  • the sub-cell remainder goes into bearings[0] (renderers already add i16 pixel bearings to the cell origin — no renderer changes needed)

Tight packing eliminates the gaps. Fg colour still follows the cluster's logical cell so selection/cursor semantics are unchanged. LTR runs take an identical code path as before (extra_x = 0).

Testing

  • synthetic LTR / RTL / combining-mark attribution tests
  • end-to-end CoreText test shaping real Farsi (سلام) through the production shape_text_utf16 path, asserting each glyph reconstructs its pen position
  • full rioterm suite: 211/211 pass

Known limitation

Pure-RTL runs only. Mixed Latin-inside-RTL lines need full UAX#9 bidi reordering.

RTL text rendered with every glyph crammed into one column, and after a
naive reversal fix, with gaps before narrow glyphs (alef in آیا).

Root cause: build_row_fg's cluster-to-cell walk was forward-only.
Shapers (CoreText, HarfBuzz/swash) return RTL runs in visual order with
monotonically decreasing clusters - glyph 0 is the logically-last
character - so the cursor jumped to the run's last cell and pinned
every glyph there.

Fix: attribute_glyphs_to_cells() detects RTL via first.cluster >
last.cluster and lays RTL runs out pen-relative: a pen advances by each
glyph's real shaped advance from the run's left edge, the cell is
floor(pen / cell_w), and the sub-cell remainder goes into the x
bearing (renderers already add i16 pixel bearings to the cell origin).
Tight packing eliminates the gaps; fg colour still follows the
cluster's logical cell so selection/cursor semantics are unchanged.
LTR runs take the identical code path as before (extra_x = 0).

Tests: synthetic LTR/RTL/mark cases plus an end-to-end CoreText test
shaping real Farsi (سلام) through the production shaping path.
@saeidakbari
saeidakbari force-pushed the fix/rtl-text-rendering branch from 3167b50 to 05159dc Compare August 16, 2026 10:21
@saeidakbari saeidakbari changed the title fix(rioterm): render RTL runs (Arabic/Farsi/Hebrew) correctly — glyphs no longer crammed into one column fix(rioterm): render RTL runs (Arabic/Farsi/Hebrew) correctly Aug 16, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant