44 lines
7.9 KiB
Markdown
44 lines
7.9 KiB
Markdown
|
|
# kiln-ui
|
||
|
|
|
||
|
|
Module reference for the `kiln-ui` crate — reusable, **game-agnostic** terminal UI
|
||
|
|
widgets on **ratatui**. This is a standalone crate (its own git repo); it has **no
|
||
|
|
dependency on any game engine** and is intended to be reused by any ratatui front-end
|
||
|
|
(e.g. a text editor).
|
||
|
|
|
||
|
|
## Code style
|
||
|
|
|
||
|
|
- Add `///` rustdoc comments to every `pub` type, field, and function.
|
||
|
|
- Add inline `//` comments inside non-trivial function bodies to explain the *why* of
|
||
|
|
each logical step.
|
||
|
|
- Keep comments accurate: update them when the code they describe changes.
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
A widget owns its presentation + input handling and is generic over a host context `Ctx` it mutates *only through callbacks*, so a front-end decides what a choice does without the widget knowing about app/domain types. The front-end owns the event loop and routes input to the widget while it is focused. The **syntect** (highlighting) and **arboard** (clipboard) deps make this crate desktop/terminal-only (not WASM), unlike its ratatui core.
|
||
|
|
|
||
|
|
Consumers exchange ratatui types with these widgets (`Dialog` is a `Widget`, `CursorOverlay` uses `Buffer`/`Rect`/`Position`), so a consuming crate's `ratatui` must unify with this crate's (pinned `0.30.x`).
|
||
|
|
|
||
|
|
**`src/lib.rs`** — crate root:
|
||
|
|
- `dim_area(area, buf)` (`pub`) — dims every cell in `area` (background tint for a modal overlay). Shared scaffolding: used by [`dialog`] here and re-borrowable by a host's own overlay renderer so modal styles dim identically.
|
||
|
|
- `CursorOverlay` (`pub` trait) + `render_overlay(frame, &impl CursorOverlay)` (`pub`) — a modal overlay paints into a `&mut Buffer` and *returns* `Option<Position>` (where the terminal caret goes) from `render(area, buf)`, instead of placing the cursor itself. `render_overlay` is the one place that needs a `Frame`: it calls `render` over `frame.area()` then applies the returned position via `set_cursor_position`. The split exists because a ratatui `Widget` only sees a buffer (can't move the hardware cursor), and it makes `render` unit-testable headless. Implemented by `Dialog`, which *also* `impl Widget for &Dialog` (forwarding to `CursorOverlay::render` and discarding the caret) so it can be drawn with `frame.render_widget` where the cursor isn't needed.
|
||
|
|
- Modules: `pub mod dialog; pub mod code_editor; pub mod text_field;` (widgets) plus `mod clipboard; mod text;` (`pub(crate)` shared internals — see below).
|
||
|
|
|
||
|
|
**`src/clipboard.rs` / `text.rs`** — `pub(crate)` shared internals for the text widgets:
|
||
|
|
- `clipboard::Clipboard` — system clipboard via `arboard` when available, always backed by an internal buffer (so copy/paste work headless / on the web); `set`/`get` write-through and prefer-system-on-read. Used by both `code_editor` and `text_field`.
|
||
|
|
- `text::{byte_of, char_cells, display_width, next_word, prev_word, super_to_ctrl, TAB_WIDTH}` — char↔byte index, display-width (tab stops + `unicode-width` wide chars), within-line word boundaries, and the `⌘`→`ctrl` key rewrite, shared by both editors.
|
||
|
|
|
||
|
|
**`src/dialog.rs`** — modal dialog widgets:
|
||
|
|
- `Dialog<Ctx>` — an open dialog generic over the host context its callback mutates. Built via `Dialog::text(title, initial, on_done)` (a single free-text field; `on_done: FnOnce(Option<String>, &mut Ctx)`, `None` on cancel) or `Dialog::list(title, items, allow_create, on_done)` (a filter field over a selectable list; `on_done: FnOnce(ListDialogResponse, &mut Ctx)`). Holds a `DialogBody` (`Text`/`List`) plus the boxed callback. Each field is a [`TextField`].
|
||
|
|
- `ListDialogResponse { Select(String), Create(String), Cancel }` — list outcome: an existing entry, a newly-typed value (only when `allow_create`), or cancel. `DialogResult { Continue, Submit, Cancel }` — what a key event did.
|
||
|
|
- `handle_event(&Event) -> DialogResult` — `Esc`→`Cancel`; `Enter`→`Submit` only when OK is enabled (always for text; for a list, a valid entry is selected *or* `allow_create` + non-empty); Up/Down move a list's selection; other keys edit the field (and re-clamp the list selection to the new prefix filter). `finish(result, &mut Ctx)` consumes the dialog and fires its callback. The host pattern: `take()` the dialog out of its slot on `Submit`/`Cancel`, then call `finish` so the callback gets an un-borrowed `&mut Ctx`.
|
||
|
|
- `Dialog` draws via its `CursorOverlay` impl (`render(area, buf) -> Option<Position>`; host renders it through `render_overlay`) — dims the screen, draws a centered bordered box. A **text** dialog centers its field box (h+v) with a distinct `FIELD_BG` background so it reads as an input box; a **list** dialog puts the filter field atop the scrolling, selection-highlighted list. Both show an `[enter] OK [esc] Cancel` footer (OK dimmed when disabled). `render` returns the active field's caret position (the host places the real terminal cursor). Unit-tested with a dummy `Ctx`.
|
||
|
|
|
||
|
|
**`src/text_field.rs`** — a single-line editable text field (a one-line sibling of `code_editor`, used by `dialog`'s text/filter inputs):
|
||
|
|
- `TextField` — `text: String` + a char-indexed `cursor` + a `Clipboard` + an optional `max_length`. `new(initial)` (unbounded) or `sized(initial, max_length)` (truncates the seed and caps typed input); `value()`, `display_cursor()` (caret's display column), `set(text)` (replace contents, truncated to `max_length`), and `handle_key(KeyEvent) -> bool` (`true` if consumed). Handles printable insert, Backspace/Delete, Left/Right (+ ctrl word-moves), Home/End, and `ctrl`/`⌘`+`v` paste (newlines stripped); leaves Esc/Enter/Up/Down to the caller. Presentation is the caller's job (it reads `value()`/`display_cursor()`). No selection yet. Unit-tested.
|
||
|
|
|
||
|
|
**`src/code_editor.rs`** — a hand-written **modeless, keyboard-only** code editor (hand-written; `edtui` was rejected for panicking on bare Shift and lacking line-wrap):
|
||
|
|
- `CodeEditor` — edits one script's text: `lines: Vec<String>`, a char-indexed `cursor`/`anchor` (selection), `desired_col` (vertical-move column), `scroll` (top row + left display-col, cursor-following), undo/redo `Snapshot` stacks, a cached `HighlightParts`, and a `Clipboard` (system via `arboard` when available, always backed by an internal buffer so copy/paste work headless / on the web). Public API: `new(name, text)`, `name()`, `text()` (lines joined with `\n`), `handle_event(&Event) -> CodeEditorOutcome { Continue, Exit }`, `draw(&mut self, frame, area)`.
|
||
|
|
- Input (`handle_event`): `⌘` is mapped to `ctrl` up front; then printable keys insert, Enter/Backspace/Delete edit (Backspace/Delete at a line edge join lines, Left/Right wrap across line ends), arrows move (`shift` extends the selection), **`ctrl`+Left/Right jump by word** (`text::{prev,next}_word`, wrapping at line ends) and **`ctrl`+Up/Down jump by paragraph** (to the nearest blank/whitespace-only line, via `is_blank_line`), Home/End/PageUp/PageDown, `ctrl`+`c`/`x`/`v` (system clipboard), `ctrl`+`a` (select all), `ctrl`+`z`/`y` (undo/redo), `Esc` → `Exit`. Bracketed `Event::Paste` inserts text. **Unknown keycodes are ignored** (the whole point — no panic on Shift/F-keys). Undo coalesces a run of typing (or deletes) into one step; movement/structural edits break the run.
|
||
|
|
- Rendering (`draw`): a titled border, a line-number gutter, and per-line styled text. One width model (`char_cells`: tabs → next 4-stop, wide chars via `unicode-width`) drives the cursor X, selection background, and horizontal scroll together. Highlighting: `build_highlight_parts()` parses the embedded `resources/rhai.sublime-syntax` into a one-syntax `SyntaxSet` + `base16-ocean.dark` theme (cached); `draw` runs `syntect::easy::HighlightLines` from line 0 to the viewport bottom each frame (small scripts), mapping syntect colors to `Color::Rgb`. Long lines clip with the cursor-following horizontal scroll (no soft-wrap). Mouse is out of scope for now. **Note:** highlighting is currently hardcoded to Rhai; making the syntax/theme pluggable is a known follow-up for non-Rhai hosts.
|
||
|
|
- Unit-tested (pure logic, no TTY).
|