docs: document Focusable interface propagation pattern for IME support

Container components with embedded Input/Editor children must implement
Focusable and propagate focus state to enable correct IME candidate
window positioning.

Related to #827
This commit is contained in:
Mario Zechner 2026-01-18 17:27:16 +01:00
parent b4f833c259
commit ad337e30be
2 changed files with 82 additions and 0 deletions

View file

@ -52,6 +52,36 @@ When a `Focusable` component has focus, TUI:
This enables IME candidate windows to appear at the correct position for CJK input methods. The `Editor` and `Input` built-in components already implement this interface.
### Container Components with Embedded Inputs
When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child. Otherwise, the hardware cursor won't be positioned correctly for IME input.
```typescript
import { Container, type Focusable, Input } from "@mariozechner/pi-tui";
class SearchDialog extends Container implements Focusable {
private searchInput: Input;
// Focusable implementation - propagate to child input for IME cursor positioning
private _focused = false;
get focused(): boolean {
return this._focused;
}
set focused(value: boolean) {
this._focused = value;
this.searchInput.focused = value;
}
constructor() {
super();
this.searchInput = new Input();
this.addChild(this.searchInput);
}
}
```
Without this propagation, typing with an IME (Chinese, Japanese, Korean, etc.) will show the candidate window in the wrong position on screen.
## Using Components
**In hooks** via `ctx.ui.custom()`:

View file

@ -136,6 +136,58 @@ interface Component {
The TUI appends a full SGR reset and OSC 8 reset at the end of each rendered line. Styles do not carry across lines. If you emit multi-line text with styling, reapply styles per line or use `wrapTextWithAnsi()` so styles are preserved for each wrapped line.
### Focusable Interface (IME Support)
Components that display a text cursor and need IME (Input Method Editor) support should implement the `Focusable` interface:
```typescript
import { CURSOR_MARKER, type Component, type Focusable } from "@mariozechner/pi-tui";
class MyInput implements Component, Focusable {
focused: boolean = false; // Set by TUI when focus changes
render(width: number): string[] {
const marker = this.focused ? CURSOR_MARKER : "";
// Emit marker right before the fake cursor
return [`> ${beforeCursor}${marker}\x1b[7m${atCursor}\x1b[27m${afterCursor}`];
}
}
```
When a `Focusable` component has focus, TUI:
1. Sets `focused = true` on the component
2. Scans rendered output for `CURSOR_MARKER` (a zero-width APC escape sequence)
3. Positions the hardware terminal cursor at that location
4. Shows the hardware cursor
This enables IME candidate windows to appear at the correct position for CJK input methods. The `Editor` and `Input` built-in components already implement this interface.
**Container components with embedded inputs:** When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child:
```typescript
import { Container, type Focusable, Input } from "@mariozechner/pi-tui";
class SearchDialog extends Container implements Focusable {
private searchInput: Input;
// Propagate focus to child input for IME cursor positioning
private _focused = false;
get focused(): boolean { return this._focused; }
set focused(value: boolean) {
this._focused = value;
this.searchInput.focused = value;
}
constructor() {
super();
this.searchInput = new Input();
this.addChild(this.searchInput);
}
}
```
Without this propagation, typing with an IME (Chinese, Japanese, Korean, etc.) will show the candidate window in the wrong position.
## Built-in Components
### Container