Maintains a Vector[String] of lines plus a (row, col) cursor. Editing keys (CharKey, Enter, Backspace, Delete, arrow keys, Home/End) compose to a small fixed-pitch text editor — enough for a comments box, a description field, or the body of a chat message. For single-line input use termflow.tui.Prompt instead.
Cursor navigation is grapheme-aware within a line: arrow keys step over surrogate pairs and combining marks as one unit, mirroring the single-line Prompt. The cursor is stored in UTF-16 code-unit offsets to match the underlying String model — apps composing with other text APIs can pass it straight through.
The widget renders one TextNode per visible row. The cursor row uses a reverse-video cell so the caret is visible without having to commandeer the hardware cursor — same approach as termflow.tui.widgets.TextField. Lines too wide for the viewport are clipped with a trailing …; vertical scrolling lifts the top row off-screen when the cursor moves below the visible window.
The widget is purely presentational. State lives in the calling app:
given Theme = Theme.dark
MultiLineInput.render(
state = m.editor,
width = 60,
height = 8,
at = Coord(2.x, 5.y)
)
Apps wire the keyboard with MultiLineInput.handleKey(state, key) which returns (newState, Option[Cmd[Msg]]) — the optional command is only ever None today (no submit semantics are baked in), but the shape matches Prompt.handleKey so swapping the two is a small change.
final case class State(lines: Vector[String], cursorRow: Int, cursorCol: Int, scrollTop: Int)
Editor state.
Editor state.
Invariant: lines always contains at least one entry — an empty editor is lines = Vector("") with cursor (0, 0). The cursor row is clamped to [0, lines.size - 1] and the column to [0, currentLine.length] after every transition.
Value parameters
cursorCol
Zero-based UTF-16 column on the cursor's row.
cursorRow
Zero-based row of the cursor.
lines
The editor's lines, top-to-bottom. UTF-16 code-unit strings, no trailing newline.
scrollTop
First visible row (vertical scroll offset). Internal-state-ish; apps generally don't manage it directly — handleKey keeps it consistent with the cursor.
Feed a key event into the editor and return the updated state.
Feed a key event into the editor and return the updated state.
Returns (state, None) for handled keys; (state, None) for unhandled keys (no-op) — the Option[Cmd] shape is reserved for future submit semantics so callers don't have to refactor when those land.
Render the editor at (at, width, height). Returns a list of VNodes — one per visible row plus an InputNode for the cursor row. Apps typically embed this in a BoxNode border for a nicer visual; the widget doesn't draw the border itself.
Render the editor at (at, width, height). Returns a list of VNodes — one per visible row plus an InputNode for the cursor row. Apps typically embed this in a BoxNode border for a nicer visual; the widget doesn't draw the border itself.
Value parameters
at
Top-left of the viewport.
height
Visible height in rows.
state
Current editor state.
width
Visible width in cells (rows are clipped past this).