Built-in Widgets #

Every builder returns an Element; all compose freely.

Text #

Word-wrapped styled text; newlines start new lines. .style(…) styles the current span, .span(content, style) appends further spans:

text("✓ ").style(green).span("done", Style::default())

Markdown (requires 'markdown' feature enabled) #

CommonMark via pulldown-cmark: headings, emphasis, inline code, fenced code blocks, lists. Word-wraps at render width with exact height measurement, and caches its parse within the frame. .styles(MarkdownStyles { … }) overrides the palette.

Spinner #

An animated activity indicator that requires zero app code to animate: it reports animated() → ~80ms and the runtime ticks while it's visible. The glyph derives from wall-clock time, so there's no state to store. .done(bool) swaps the animation for a ✓ (.hide_checkmark() omits it), .label_style(…) / .spinner_style(…) style the parts.

A label computed from wall-clock time (elapsed seconds, say) updates on the spinner's own ticks — view-side time needs no subscription.

Panel #

Border-and-title chrome: .title(…) (top-left), .title_right(…), .footer(…) (bottom-right), .border_style(…), .title_style(…), .pad_x(cols) for interior padding. Forwards its child's cursor through the chrome, and suppresses it when there's no room to render.

Text Area #

The built-in multi-line input, strict-Elm style: contents and cursor live in a TextAreaState in your model; the element borrows it.

// model
input: TextAreaState,
// update — unclaimed keys and pastes route here via keymap fallthrough
Msg::Input(ev) => self.input.handle(&ev),
// view
text_area(&self.input)
.placeholder("Type a message…")
.track_focus(&self.input_focus)
.max_height(6)

TextAreaState is grapheme-aware (combining marks, wide characters, emoji clusters) and exposes insert_str / insert_newline / take_text / set_text / is_blank. Soft wrap is on by default (.wrap(false) to truncate); one wrap computation is the single source of truth for layout, height, cursor position, and the scroll window. handle deliberately ignores Ctrl/Alt chords and policy keys (Enter, Tab, Esc) — those belong to your keymap.

TextAreaState.handle covers ordinary editing. If you want emacs bindings, undo, and a kill ring, wrap tui-textarea instead — the components chapter shows the adapter.

Viewport #

A fixed-height window over a list of lines showing the most recent content — live command output, log tails. .height(rows), .style(…), .wrap(bool).

Combinators #

From ElementExt on any element: .any() (type-erase to AnyElement<'a>), .pad_left(cols), .pad_top(rows). From Fluent on any builder: .when(cond, f), .when_some(option, f).