---
title: "Text, Bidi & IME — zenit Zig UI Doku"
description: "Von Bytes über Graphem-Cluster und bidirektionale Absätze bis zur IME-Komposition: zenit behandelt Text als Systemfähigkeit, nicht als Reihe von Glyphen, die…"
url: https://zenit.z.express/de/docs/guide/text-engine
language: de
alternate_en: https://zenit.z.express/docs/guide/text-engine.md
alternate_zh: https://zenit.z.express/zh/docs/guide/text-engine.md
alternate_es: https://zenit.z.express/es/docs/guide/text-engine.md
alternate_ja: https://zenit.z.express/ja/docs/guide/text-engine.md
alternate_ko: https://zenit.z.express/ko/docs/guide/text-engine.md
alternate_fr: https://zenit.z.express/fr/docs/guide/text-engine.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Text, Bidi und IME

Von Bytes über Graphem-Cluster und bidirektionale Absätze bis zur IME-Komposition: zenit behandelt Text als Systemfähigkeit, nicht als Reihe von Glyphen, die pro Codepoint ausgelegt werden.

## Eine Zeile, vier Schichten

Speicherung, Bearbeitungsgrenzen, Richtungsauflösung und Plattform-Shaping haben jeweils genau eine Autorität und ergeben zusammen visuelle Zeilen, die sich per Hit-Test treffen und auswählen lassen. Keine Schicht entscheidet für eine andere.

TEXT STACK

Die ersten beiden Schichten liegen in src/text\_core, der Bidi-Algorithmus in src/i18n, und das Shaping übernimmt unter macOS CoreText.

| Schicht | Zuständig für | Ort |
| --- | --- | --- |
| UTF-8-Dokumentkoordinaten | Stabile Byte-Offsets, an jedem Bearbeitungseinstieg auf Grenzen normalisiert | `src/text_core` |
| UAX-#29-Grapheme | Cursor, Löschen und Auswahl zerteilen nie ZWJ-Emoji, kombinierende Zeichen oder Flaggen | `src/text_core/grapheme.zig` |
| UAX-#9-Bidi | Absatzrichtung, Isolates, Embeddings, Klammerpaare, Zuordnung logisch ↔ visuell | `src/i18n/bidi.zig` |
| CoreText-Shaping | Font-Fallback, Ligaturen, Glyph-Runs, visuelle Platzierung und Caret-x | `src/render/text_renderer.zig` |

## Grapheme statt Codepoints

Ein Familien-Emoji besteht aus sieben Unicode-Scalars (vier Personen und drei ZWJs), `é` kann `e` plus ein kombinierender Akut sein, und eine Flagge besteht aus zwei regionalen Indikatoren. Für Nutzer ist jedes davon ein einziges Zeichen. zenit verwendet erweiterte Graphem-Cluster als Atom für Cursorbewegung, Löschen, Auswahl und Kürzen an der Kapazitätsgrenze.

UAX #29

cursor\_pos ist ein UTF-8-Byte-Offset und landet immer nur auf einer Clustergrenze. Selbst das Kürzen an der 2048-Byte-Kapazität des einzeiligen Input hinterlässt nie ein halbes Graphem.

[Video](https://zenit.z.express/media/stories/emoji.mp4?v=5f65f98444)

Farbige Emoji laufen über eine BGRA-Atlasseite und den Farbzweig des Shaders; die Graustufen-Kontrollgruppe bleibt grau, und gemischte Runs teilen die normale Text-Pipeline.

[Video](https://zenit.z.express/media/stories/wordnav.mp4?v=1fd0aa8709)

Alt+← / → springt über ganze griechische und kyrillische Wörter, Doppelklick wählt ein Wort aus, und das Familien-Emoji bleibt ein Cluster.

## Bidirektionaler Text

Text wird in logischer Reihenfolge gespeichert und in visueller Reihenfolge angezeigt. UAX #9 löst zunächst für jedes Zeichen eine Einbettungsebene auf, dann kehrt Regel L2 die Runs von der höchsten Ebene abwärts um. Unten steht `abc אבג 123` in einem LTR-Absatz: Das Hebräische wird zu Ebene 1 aufgelöst, die folgenden Ziffern zu Ebene 2.

UAX #9

Die Ziffern werden weiterhin von links nach rechts gelesen, aber der gesamte RTL-Abschnitt – Ziffern eingeschlossen – wird als Block von rechts nach links ausgelegt.

Der UAX-#9-Resolver ist zenits eigene portable Implementierung in `src/i18n/bidi.zig`, mit Eigenschaftstabellen, die aus fest gepinnten Unicode-17.0.0-Daten generiert werden. Unter macOS werden das finale Glyph-Shaping und die RTL-Verbindung weiterhin run-weise an CoreText übergeben – der Text-Renderer zerlegt einen RTL-Run nie pro Codepoint.

[![zenit Storybook showing mixed Arabic, Hebrew and Latin text](https://zenit.z.express/media/unicode-bidi.png?v=d1e0e89e51)](https://zenit.z.express/media/unicode-bidi.png?v=d1e0e89e51)

Das echte Storybook: Arabisch, Hebräisch und Mischungen aus latin + arabic + latin, ausgelegt von einer einzigen Text-Pipeline; der virtuelle Cursor ruht auf der RTL-Story.

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

Cursorbewegung und Auswahl in RTL-Absätzen und Absätzen mit gemischter Richtung.

## Unicode-17-Konformität

Die Laufzeit-Eigenschaftstabellen werden aus im Repo gepinnten Unicode-17.0.0-Daten generiert; die offiziellen Testdateien liegen unverändert unter `vendor/unicode/17.0.0` und sind per SHA-256 in `SHA256SUMS` fixiert. „Unterstützt“ heißt hier nicht eine Handvoll Emoji-Unit-Tests – es heißt, die vollständigen offiziellen Suites auszuführen.

**490,846**BidiTest-Typsequenzen

**770,241**Auswertungen der Absatzrichtung

**91,707**BidiCharacterTest-Sequenzen

**766**GraphemeBreakTest-Fälle

```sh
python3 tools/generate_grapheme_data.py --check
python3 tools/generate_bidi_data.py --check
(cd vendor/unicode/17.0.0 && shasum -a 256 -c SHA256SUMS)
zig build test-text-core
zig build test-bidi-conformance
```

`--check` bestätigt, dass die generierten Zig-Tabellen zu den Daten passen; `test-text-core` führt jeden GraphemeBreakTest-Fall aus; `test-bidi-conformance` führt beide vollständigen offiziellen Bidi-Dateien aus (bis UAX #9 Regel L2) und ist auch Teil von `zig build test-headless`.

## IME als Bürger erster Klasse

IME ist nicht das fertige Zeichen, getarnt als Tastendruck. Plattform-Events behalten die Phasen: `ime_preedit` trägt den Kompositionstext plus einen Cursor-Offset darin, `ime_commit` trägt den finalen Text. Während der Komposition rendern [Input](https://zenit.z.express/de/components/input) und Textarea unterstrichenen Marked Text, doch der kanonische Buffer ändert sich erst beim Commit. Es gibt kein separates „Verwerfen“-Event – ein leeres Preedit bricht die Komposition ab.

IME LIFECYCLE

Preedit und Commit sind zwei Arten von Events, keine Momentaufnahmen eines Strings. Nach einem Commit verharrt das Input kurz in commit\_pending\_end, um ein Duplikat desselben Texts zu schlucken, das die Plattform eventuell nachschickt, und kehrt dann zu idle zurück.

| Was „erster Klasse“ bedeutet | Konkreter Vertrag |
| --- | --- |
| Getrennte Events | `ime_preedit` / `ime_commit` behalten die Phase; ein leeres Preedit bricht ab |
| Rekonvertierung | Beide Events können eine Replacement Range (UTF-8-Bytebereich) tragen, um bestätigten Text zurück in die Komposition zu holen oder an Ort und Stelle zu ersetzen |
| Visuelle Komposition | Unterstreichung und Hervorhebung der Marked Range, Kompositions-Cursor und Position des Kandidatenfensters folgen der visuellen Zeile |
| Sichere Bearbeitung | Cursor, Löschen, Auswahl und Kapazitätskürzung landen stets auf erweiterten Graphemgrenzen |
| Testbar | Der Harness injiziert Preedit / Commit getrennt und liest `ime_phase`, `ime_preedit_len`, `buffer`, `cursor_pos`, `anchor` zurück |
| Gate auf echtem System | `scripts/verify_ime.sh` durchläuft den vollständigen NSTextInputClient-Pfad mit den System-Eingabequellen Pinyin / Japanisch |

[![Input showing underlined nihongo preedit text](https://zenit.z.express/media/ime-preedit.png?v=6c12ff351b)](https://zenit.z.express/media/ime-preedit.png?v=6c12ff351b)

Preedit: Unterstrichener Marked Text und der I-Beam sind sichtbar; der kanonische Buffer hat sich nicht geändert.

[![Input showing committed 日本語](https://zenit.z.express/media/ime-commit.png?v=f665179c69)](https://zenit.z.express/media/ime-commit.png?v=f665179c69)

Commit: 日本語 gelangt in den kanonischen Buffer, das Preedit wird geleert.

## IME testen

Der E2E-Client stellt beide Phasen als getrennte RPCs bereit, und `inputState` liest den echten Zustand eines Eingabefelds per Test-ID zurück.

`ime.test.ts`

```ts
import { imeCommit, imePreedit, inputState } from "./client";

// The input must already have focus (e.g. click it first).

await imePreedit("nihongo");
let s = await inputState("story.input.name");
// s.ime_phase === "composing", s.ime_preedit_len === 7, s.buffer === ""

await imeCommit("日本語");
s = await inputState("story.input.name");
// s.buffer === "日本語", s.ime_preedit_len === 0

await imePreedit("");   // empty preedit cancels an active composition
```

[Video](https://zenit.z.express/media/e2e-text-ime.mp4?v=efa93e6b3f)

Der Harness fokussiert ein Input in einem echten Retina-Fenster, injiziert Preedit und Commit getrennt und wechselt dann zu RTL und Checkbox; I-Beam, Hand-Cursor und Klickpuls zeichnet der virtuelle Cursor in das Drawable.

> TIP
> 
> **Injektion und echte IMEs testen Unterschiedliches.** Die Harness-Injektion macht Assertions zu Phase, Buffer und Pixeln wiederholbar; Kandidatenfenster, Wechsel der Eingabequelle und NSTextInputClient-Integration werden weiterhin über den echten Systempfad in `scripts/verify_ime.sh` geprüft (benötigt eine GUI-Sitzung und die Bedienungshilfen-Berechtigung; etwa 8 Sekunden lang die Tastatur nicht berühren).

Den vollständigen Harness-Workflow finden Sie unter [E2E-Harness](https://zenit.z.express/de/docs/advanced/e2e).
