---
title: "RTL コンポーネント — zenit Zig UI"
description: "アラビア文字、ヘブライ文字、ラテン文字の混在テキスト。UAX #9 が段落の方向と表示順を解決し、CoreText が shaping を担当します。"
url: https://zenit.z.express/ja/components/rtl
language: ja
alternate_en: https://zenit.z.express/components/rtl.md
alternate_zh: https://zenit.z.express/zh/components/rtl.md
alternate_es: https://zenit.z.express/es/components/rtl.md
alternate_ko: https://zenit.z.express/ko/components/rtl.md
alternate_fr: https://zenit.z.express/fr/components/rtl.md
alternate_de: https://zenit.z.express/de/components/rtl.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# RTL

`ui.text`

アラビア文字、ヘブライ文字、ラテン文字の混在テキスト。

[Video](https://zenit.z.express/media/stories/rtl.mp4?v=bc200dee27)

**BEHAVIOR CONTRACT**

UAX #9 が段落の方向と表示順を解決し、CoreText が shaping を担当します。

**RECORDED INTERACTION**

RTL と混在テキストのサンプルを一巡します。

`examples/storybook/stories.zig:3467`

```zig
// 除了纯 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;
}
```

[GitHub でソースを見るexamples/storybook/stories.zig:3467](https://github.com/version-next/zenit/blob/HEAD/examples/storybook/stories.zig#L3467)

Storybook にあるこの story のコードそのままです（zenit 5f9add5+wip 2026-09-30）。`W` は `ui.widgets` のことで、`col` / `row` / `label` は Storybook 内の小さなレイアウトヘルパーです。
