RTL
ui.textTexto mixto en árabe, hebreo y latín.
BEHAVIOR CONTRACT
UAX #9 resuelve la dirección y el orden visual; CoreText se encarga del shaping.
RECORDED INTERACTION
Recorre las muestras RTL y mixtas.
// 除了纯 RTL,还覆盖数字、中性/镜像标点、combining marks、URL/邮箱与
// 连续方向切换。最后的窄 Textarea 刻意触发 soft wrap,供 e2e 实际验证
// caret、selection rect 与 pointer hit-testing 使用同一套视觉坐标。
pub fn buildRtl(scope: *ui.Scope, cx: *ui.Cx) anyerror!*ui.Node {
const a = cx.allocator;
const c = try col(cx, 10);
try c.appendChild(a, try label(cx, "RTL bidi text (CoreText 已做 bidi,glyph 数组为视觉序)"));
const matrix = try ui.box(cx, .{ .direction = .row, .gap = 28, .align_items = .start }, .{});
const left = try col(cx, 6);
left.style.width = .{ .px = 350 };
const right = try col(cx, 6);
right.style.width = .{ .px = 390 };
// 纯 RTL:阿拉伯语 "مرحبا"(marhaba)5 个字符,连写后仍应铺开成一条。
try left.appendChild(a, try label(cx, "plain shaping controls"));
const arabic = try ui.text(cx, "\u{645}\u{631}\u{62D}\u{628}\u{627}", .{
.font_size = 36,
.color = ui.Color.rgb(0, 0, 0),
});
arabic.meta.ownership.meta.test_id = "story.rtl.arabic";
try left.appendChild(a, arabic);
// 纯 RTL:希伯来语 "שלום"(shalom)4 个字符,无连写,字形间隔清晰。
const hebrew = try ui.text(cx, "\u{5E9}\u{5DC}\u{5D5}\u{5DD}", .{
.font_size = 36,
.color = ui.Color.rgb(0, 0, 0),
});
hebrew.meta.ownership.meta.test_id = "story.rtl.hebrew";
try left.appendChild(a, hebrew);
// 对照组:等长 LTR 文本。证明"墨迹跨度"这个判据本身在已知正确的
// 情况下确实成立(否则下界断言可能只是碰巧过)。
const ltr = try ui.text(cx, "abcde", .{
.font_size = 36,
.color = ui.Color.rgb(0, 0, 0),
});
ltr.meta.ownership.meta.test_id = "story.rtl.ltr_control";
try left.appendChild(a, ltr);
try left.appendChild(a, try label(cx, "numbers + RTL / repeated direction switches"));
const numbers = try ui.text(cx, "abc \u{645}\u{631}\u{62D}\u{628}\u{627} 123 xyz", .{
.font_size = 24,
.color = ui.Color.rgb(0, 0, 0),
});
numbers.meta.ownership.meta.test_id = "story.rtl.numbers";
try left.appendChild(a, numbers);
// 拉丁 → 阿拉伯 → 拉丁 → 希伯来 → 数字,连续跨越多个 bidi run。
const mixed = try ui.text(cx, "ab\u{645}\u{631}\u{62D}\u{628}\u{627}cd", .{
.font_size = 36,
.color = ui.Color.rgb(0, 0, 0),
});
mixed.meta.ownership.meta.test_id = "story.rtl.mixed";
try left.appendChild(a, mixed);
const switches = try ui.text(cx, "abc \u{645}\u{631}\u{62D}\u{628}\u{627} xyz \u{5E9}\u{5DC}\u{5D5}\u{5DD} 123", .{
.font_size = 24,
.color = ui.Color.rgb(0, 0, 0),
});
switches.meta.ownership.meta.test_id = "story.rtl.switches";
try left.appendChild(a, switches);
// Paired-bracket resolution (UAX #9 N0) and mirrored punctuation.
try right.appendChild(a, try label(cx, "mirrored punctuation — parentheses / brackets"));
const parens = try ui.text(cx, "\u{645}\u{631}\u{62D}\u{628}\u{627} (abc) \u{5E9}\u{5DC}\u{5D5}\u{5DD}", .{
.font_size = 24,
.color = ui.Color.rgb(0, 0, 0),
});
parens.meta.ownership.meta.test_id = "story.rtl.mirrored_parens";
try right.appendChild(a, parens);
const brackets = try ui.text(cx, "\u{645}\u{631}\u{62D}\u{628}\u{627} [abc] \u{5E9}\u{5DC}\u{5D5}\u{5DD}", .{
.font_size = 24,
.color = ui.Color.rgb(0, 0, 0),
});
brackets.meta.ownership.meta.test_id = "story.rtl.mirrored_brackets";
try right.appendChild(a, brackets);
// Tashkeel 是 combining marks:有无变音符号应保持同一组基字符的推进宽度,
// 但墨迹必须包含上/下附标。
try right.appendChild(a, try label(cx, "Arabic tashkeel / combining marks"));
const tashkeel_base = try ui.text(cx, "\u{645}\u{631}\u{62D}\u{628}\u{627}", .{
.font_size = 30,
.color = ui.Color.rgb(0, 0, 0),
});
tashkeel_base.meta.ownership.meta.test_id = "story.rtl.tashkeel_base";
try right.appendChild(a, tashkeel_base);
const tashkeel = try ui.text(cx, "\u{645}\u{64E}\u{631}\u{652}\u{62D}\u{64E}\u{628}\u{64B}\u{627}", .{
.font_size = 30,
.color = ui.Color.rgb(0, 0, 0),
});
tashkeel.meta.ownership.meta.test_id = "story.rtl.tashkeel";
try right.appendChild(a, tashkeel);
try matrix.appendChild(a, left);
try matrix.appendChild(a, right);
try c.appendChild(a, matrix);
// URL、邮箱以及 ES/CS/ON 类符号经常把相邻数字或 Latin run 粘到错误一侧。
try c.appendChild(a, try label(cx, "RTL paragraph with URL / email / + - / : symbols"));
const symbols = try ui.text(cx, "\u{645}\u{631}\u{62D}\u{628}\u{627} https://example.com/a-b?q=1:2 test@example.com +12/34 - \u{5E9}\u{5DC}\u{5D5}\u{5DD}", .{
.font_size = 17,
.color = ui.Color.rgb(0, 0, 0),
});
symbols.meta.ownership.meta.test_id = "story.rtl.symbols";
try c.appendChild(a, symbols);
// 无硬换行的长 RTL 段落,在窄编辑器内产生多条 display line。e2e 会对
// 两条视觉行的左右位置点击,并跨行拖选,核对 caret/selection 几何。
try c.appendChild(a, try label(cx, "wrapped editable RTL — caret / selection rect / hit-testing"));
const editor_text =
"\u{645}\u{631}\u{62D}\u{628}\u{627} \u{628}\u{627}\u{644}\u{639}\u{627}\u{644}\u{645} abc 123 xyz \u{5E9}\u{5DC}\u{5D5}\u{5DD} " ++
"https://example.com/a-b?q=1:2 test@example.com +12/34 - 56:78 / " ++
"abc \u{645}\u{64E}\u{631}\u{652}\u{62D}\u{64E}\u{628}\u{64B}\u{627} xyz \u{5E9}\u{5DC}\u{5D5}\u{5DD} 123 \u{646}\u{647}\u{627}\u{64A}\u{629} \u{627}\u{644}\u{633}\u{637}\u{631}";
const editor = try ui.widgets.Textarea(.{ .value = editor_text, .rows = 5, .width = 520 }).mount(scope, cx);
editor.meta.ownership.meta.test_id = "story.rtl.editor";
try c.appendChild(a, editor);
return c;
}El código literal de la story en el Storybook (zenit 5f9add5+wip 2026-09-30). W es ui.widgets; col / row / label son pequeños helpers de layout del Storybook.