Skip to content

Commit dda08dd

Browse files
kaluaimclaude
authored andcommitted
applib/graphics: add a minimal BiDi engine
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-W7 so a number keeps the separators and terminators that belong to it, N1/N2 for neutrals, I1/I2 and L2 so runs carry an embedding level and reorder by it, and L4 for mirrored glyphs. Arabic letters are AL, so a European number after one becomes an Arabic number (W2) and terminators and signs no longer bind to it, matching what the phone renders. Bracket pairs (N0), explicit directional controls, isolates and embedding levels above two are out of scope; N0 and a linear neutral resolver are tracked as a follow-up. 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 covering every block the classifier calls strong right-to-left, so the gate and the class table agree and nothing is right-aligned while still being drawn in logical order. Text with none of those bytes costs one comparison per byte and keeps taking the existing left-to-right path, unchanged. Runs carry an embedding level rather than a direction bit, because a number following a right-to-left run sits one level deeper than the text around it: without the level, "Total <arabic> 123" cannot be told apart from ordinary Latin and L2 leaves the digits on the wrong side of the Arabic. Paragraph direction is resolved from the start of the paragraph rather than the start of the visual line, so a wrapped paragraph does not change base direction part way down. 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>
1 parent bb02ebe commit dda08dd

18 files changed

Lines changed: 1445 additions & 577 deletions

File tree

src/fw/applib/graphics/bidi.c

Lines changed: 714 additions & 0 deletions
Large diffs are not rendered by default.

src/fw/applib/graphics/bidi.h

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* SPDX-FileCopyrightText: 2026 Ahmed Hussein */
2+
/* SPDX-FileCopyrightText: 2026 Khalid Nuaim (kaluaim) */
3+
/* SPDX-License-Identifier: Apache-2.0 */
4+
5+
#pragma once
6+
7+
#include "utf8.h"
8+
9+
#include <stdbool.h>
10+
#include <stddef.h>
11+
#include <stdint.h>
12+
13+
//! Minimal bidirectional text engine, a subset of Unicode UAX 9.
14+
//!
15+
//! Implements the rules that matter for a single line of watch text:
16+
//! P2/P3 (paragraph direction), W1 (marks follow their base), W2 and W4-W7
17+
//! (numbers take their class from the letter before them and absorb their
18+
//! separators and terminators), N1/N2 (neutrals take the surrounding direction,
19+
//! otherwise the paragraph direction), I1/I2 (embedding levels 0-2) and L4
20+
//! (mirrored glyphs). The caller reorders runs by level (L2). Bracket pairs
21+
//! (N0), explicit directional controls, isolates and levels above two are not
22+
//! implemented.
23+
24+
//! Check whether a UTF-8 range needs bidirectional processing at all.
25+
//! Scans raw bytes over every block the class table calls strong right-to-left,
26+
//! so pure-ASCII text costs one comparison per byte and stays on the LTR path.
27+
//! @param start Pointer to start of UTF-8 string
28+
//! @param end Pointer to end of UTF-8 string (exclusive)
29+
//! @return true if the range contains at least one right-to-left character
30+
bool bidi_is_needed(const utf8_t *start, const utf8_t *end);
31+
32+
//! Resolve the paragraph direction of a UTF-8 range (UAX 9 P2/P3).
33+
//! The first strong character wins; ranges without one are left-to-right.
34+
//! @param start Pointer to start of UTF-8 string
35+
//! @param end Pointer to end of UTF-8 string (exclusive)
36+
//! @return true if the paragraph reads right-to-left
37+
bool bidi_paragraph_is_rtl(const utf8_t *start, const utf8_t *end);
38+
39+
//! Find the extent of the directional run starting at @p pos.
40+
//!
41+
//! Weak types are folded into the neighbouring number and neutrals are
42+
//! resolved against the surrounding runs, so the returned range is a maximal
43+
//! stretch at one embedding level. Levels follow UAX 9 I1/I2 and stay within
44+
//! 0-2: even levels read left-to-right, odd levels right-to-left.
45+
//!
46+
//! @param line_start Start of the line, used to look behind @p pos
47+
//! @param pos Position to start the run at, must be within the line
48+
//! @param end End of the line (exclusive)
49+
//! @param para_is_rtl Paragraph direction, from \ref bidi_paragraph_is_rtl
50+
//! @param[out] run_level Embedding level of the returned run
51+
//! @return Pointer to the first codepoint after the run, or @p pos on failure
52+
utf8_t *bidi_next_run(const utf8_t *line_start, utf8_t *pos, const utf8_t *end, bool para_is_rtl,
53+
uint8_t *run_level);
54+
55+
//! Return the mirrored form of a codepoint (UAX 9 L4).
56+
//! Glyphs such as brackets are drawn mirrored inside a right-to-left run.
57+
//! Codepoints without a mirrored form are returned unchanged.
58+
Codepoint bidi_mirror_codepoint(Codepoint cp);
59+
60+
//! Reverse the codepoints of a run for right-to-left display.
61+
//! Combining marks stay behind the base character they attach to.
62+
//! @param src Source UTF-8 string
63+
//! @param src_len Length of source string in bytes
64+
//! @param dest Destination buffer for the reversed string
65+
//! @param dest_size Size of destination buffer in bytes
66+
//! @return Number of bytes written to dest (excluding null terminator), or 0 on failure
67+
size_t bidi_reverse_run(const utf8_t *src, size_t src_len, utf8_t *dest, size_t dest_size);
68+
69+
//! Check if a UTF-8 string range contains any shapeable Arabic letters.
70+
//! @param start Pointer to start of UTF-8 string
71+
//! @param end Pointer to end of UTF-8 string (exclusive)
72+
//! @return true if the range contains at least one shapeable Arabic letter
73+
bool bidi_contains_arabic(const utf8_t *start, const utf8_t *end);

src/fw/applib/graphics/rtl_support.c

Lines changed: 0 additions & 208 deletions
This file was deleted.

src/fw/applib/graphics/rtl_support.h

Lines changed: 0 additions & 42 deletions
This file was deleted.

0 commit comments

Comments
 (0)