applib/graphics: add a minimal BiDi engine - #1972
Open
kaluaim wants to merge 1 commit into
Open
Conversation
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
force-pushed
the
minimal-bidi-engine
branch
from
September 2, 2026 07:22
672aac0 to
569a334
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Following the discussion in #1631 (comment) — mirroring paired brackets on its own does not cover mixed-direction text, so this does the reordering properly.
Non-RTL text never reaches the engine
bidi_is_needed()gates the whole path on a raw byte scan over the same range the previousutf8_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
(مرحبا)draws as)مرحبا(.mainhas no mirroring at all.مرحبا abc 123 defrenders the embedded phrase asdef 123 abc.\ninto the following paragraph.What it preserves
The weak-LTR digit and numeric-separator handling already in
rtl_support.cis 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.cbecomesbidi.c, rewritten. The run splitter intext_layout.ccollapses into a singlebidi_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 (
.text2012 B +.rodata90 B, cortex-m4-Os), no RAM, no allocation. It replacesrtl_support.cat 550 B andtext_layout.cis net 74 lines smaller, so the firmware grows by roughly 1.3–1.5 KB.Testing
test_rtl_support.cis replaced bytests/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,\nas a paragraph boundary, regional-indicator pairing, and the fast-path gate. All pass, clean under ASan and UBSan.