Skip to content

applib/graphics: add a minimal BiDi engine - #1972

Open
kaluaim wants to merge 1 commit into
coredevices:mainfrom
kaluaim:minimal-bidi-engine
Open

applib/graphics: add a minimal BiDi engine#1972
kaluaim wants to merge 1 commit into
coredevices:mainfrom
kaluaim:minimal-bidi-engine

Conversation

@kaluaim

@kaluaim kaluaim commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Following the discussion in #1631 (comment) — mirroring paired brackets on its own does not cover mixed-direction text, so this does the reordering properly.

before / after

Non-RTL text never reaches the engine

bidi_is_needed() gates the whole path on a raw byte scan over the same range the previous utf8_contains_rtl() covered. Text with none of those bytes costs one comparison per byte, never decodes UTF-8, and keeps taking the existing left-to-right path. No behaviour change for Latin-only text.

What it fixes

  • Bracket mirroring (L4). RTL runs are reversed but the glyphs are never mirrored, so (مرحبا) draws as )مرحبا(. main has no mirroring at all.
  • Neutral resolution (N1/N2). Spaces and punctuation join whichever run happens to be open instead of resolving against their neighbours, so the two halves of a bracket pair can land in different runs and be treated inconsistently.
  • Combining marks (W1). A mark is reversed away from the letter it attaches to, and can resolve into a run of its own.
  • European numbers (W7). A number after a Latin word is treated as right-to-left, so مرحبا abc 123 def renders the embedded phrase as def 123 abc.
  • Paragraph separators (class B). Direction resolution runs past \n into the following paragraph.

What it preserves

The weak-LTR digit and numeric-separator handling already in rtl_support.c is kept, not added. It moves from a special case inside the reversal into the run classification: European and Arabic-Indic numbers resolve to their own left-to-right run, which is the level UAX 9 assigns them in either paragraph direction. ٢٠٢٦ and ١٢:٣٤ behave as they do today.

Resolving neutrals properly also removes the need to peel trailing spaces into a separate segment, so rtl_segment_content_end() goes away with it. Lam-alef ligature shaping and the segment cap are untouched.

Scope

Implemented: P2/P3 (paragraph direction), W1 (marks), W4–W7 (numbers keep their separators and terminators), N1/N2 (neutrals), L4 (mirroring, 10 pairs), class B (paragraph separators).

Not implemented: explicit directional controls (LRM/RLM/RLE/PDF), isolates, embedding levels above two, the full ~380-character mirror set. N0 is not implemented — N1/N2 already give both halves of a bracket pair the same direction in the cases that arise in practice.

Changes

rtl_support.c becomes bidi.c, rewritten. The run splitter in text_layout.c collapses into a single bidi_next_run() call, and mirroring is applied in both the width pass and the draw pass so the two agree.

About 2.1 KB of flash (.text 2012 B + .rodata 90 B, cortex-m4 -Os), no RAM, no allocation. It replaces rtl_support.c at 550 B and text_layout.c is net 74 lines smaller, so the firmware grows by roughly 1.3–1.5 KB.

Testing

test_rtl_support.c is replaced by tests/fw/test_bidi.c — 29 cases. The digit and separator coverage from the old file is kept, re-expressed as run boundaries rather than reversal output, alongside bracket mirroring in both paragraph directions, combining marks across every block that carries them, \n as a paragraph boundary, regional-indicator pairing, and the fast-path gate. All pass, clean under ASan and UBSan.

@kaluaim
kaluaim marked this pull request as ready for review August 31, 2026 05:19
RTL rendering reverses the codepoints of each right-to-left run but never
mirrors the glyphs that call for it, so a bracket keeps facing the same
way after the reversal and "(مرحبا)" draws as ")مرحبا(". Neutral
characters are attached to whichever run happens to be open rather than
resolved against their neighbours, so the brackets around an embedded
Latin word can land in different runs and be treated inconsistently. A
combining mark is reversed away from the letter it attaches to.

Replace rtl_support.c with bidi.c, a subset of Unicode UAX 9 sized for a
single line of watch text: P2/P3 for the paragraph direction, W1 so a
mark takes the class of the character it follows, W4-W6 so a number keeps
the separators and terminators that belong to it, N1/N2 for neutrals and
L4 for mirrored glyphs. Explicit directional controls, isolates and
embedding levels above two are out of scope. N0 is not implemented;
N1/N2 already give both halves of a bracket pair the same direction in
every case that turns up in practice.

The weak-LTR digit and numeric separator behaviour already in
rtl_support.c is preserved, but moves from a special case inside the
reversal into the run classification: European and Arabic-Indic numbers
resolve to their own left-to-right run, which is the level UAX 9 assigns
them in either paragraph direction. Resolving neutrals also removes the
need to peel trailing spaces into their own segment, so
rtl_segment_content_end() goes away with it.

bidi_is_needed() gates the whole path on a raw byte scan over the same
range the old utf8_contains_rtl() covered. Text with no Hebrew or Arabic
in it costs one comparison per byte and keeps taking the existing
left-to-right path, unchanged.

The run splitter in text_layout.c collapses into one bidi_next_run() call
and mirroring is applied in both the width pass and the draw pass so the
two agree. Lam-alef ligature shaping and the segment cap are untouched.

test_rtl_support.c is replaced by test_bidi.c, which keeps its digit and
separator coverage expressed as run boundaries rather than as reversal
output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Khalid Nuaim <i@kalua.im>
@kaluaim
kaluaim force-pushed the minimal-bidi-engine branch from 672aac0 to 569a334 Compare September 2, 2026 07:22
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