Skip to Content
ftui-widgetsOverview

Widgets overview

ftui-widgets contains roughly 80 widget implementations, but it is not a miscellaneous grab-bag the way many widget crates accrete over time. Every widget either consumes a Rect and fills it (a pure Widget) or consumes a Rect and mutably updates a piece of state (a StatefulWidget). That is the whole API surface. The remaining 60,000+ lines of code are concrete implementations on top of two traits.

This page orients you to what is in the box and how it fits into the broader frame pipeline. If you want the exact trait signatures with file:line citations, jump to the traits page. If you want the full A-Z inventory, jump to the catalog.

Why a curated library, not a framework

Most TUI widget crates end up at one of two extremes:

  • A grab-bag. A dumping ground of weakly-related helpers with no shared vocabulary. Widgets can’t compose because each one invents its own state-management story.
  • A framework. An all-or-nothing abstraction that forces you into its render loop, its event model, and its layout engine.

FrankenTUI takes a third path: a curated, stateful-first library sitting on top of a tiny trait surface. Widgets don’t drive the frame — the runtime does. Widgets don’t own their own I/O — the presenter does. Widgets don’t diff themselves — the buffer diff does. Their sole job is to turn (model, area) into buffer writes.

That minimalism is what lets 80 widgets coexist without stepping on each other: a Block can wrap a Table which contains an Input which is anchored to a Popover, and the composition is just nested render(area, frame) calls.

The stateless / stateful distinction

Every widget picks one of two traits. The distinction is load-bearing:

TraitPurposeExample
WidgetPure render. Given (area, frame), draw cells. No persistent state.Paragraph, Block, Rule, Badge
StatefulWidgetRender with a mutable State. During render, the widget is allowed to clamp, adjust scroll offset, or cache layout.List, Table, Tree, VirtualizedList, Tabs, Scrollbar

The default choice is Widget. You only reach for StatefulWidget when the render pass itself must mutate state — typically to keep a selected row visible as the viewport resizes, or to memoise an expensive layout decision across frames.

The state in StatefulWidget is held by you (the caller), not the widget. Widgets are cheap to construct every frame — only the state is long-lived. This is the opposite of most object-oriented widget kits.

See the traits page for the exact signatures and an explanation of what each parameter (area, frame, state) gives you.

Where widgets live in the render pipeline

A widget’s render() call is one small step inside a much larger pipeline:

The widget layer sees only steps B and C. It receives a Rect (already allocated by layout), writes into Frame.buffer, and returns. It never touches ANSI, never calls write!() on stdout, never computes a diff.

That separation is what makes widgets trivially testable: given a known area and a synthetic Frame, the widget’s output is a deterministic function. You can unit-test a Table by constructing a Frame, calling render, and asserting on the resulting Buffer. No terminal required.

Widgets are re-created every frame — they are meant to be cheap value objects. If you find yourself caching a Block across frames, you are probably holding it wrong. Cache the data that goes into the block, not the block itself.

Degradation and is_essential

Widgets declare whether they are essential. The Widget trait provides a default is_essential(&self) -> bool { false }; text inputs and primary content areas override it to return true.

When the frame runs over budget (see frame budget) the degradation level rises: Full → SimpleBorders → NoStyling → EssentialOnly → Skeleton. At EssentialOnly, the frame only renders widgets that declare is_essential() == true. Decorative widgets (gradients, sparklines, ornamental borders) are silently skipped.

This means your Input for a search box keeps working even on a 3-FPS virtual terminal on a 16-year-old server, while the candy around it quietly fades.

Quick shape of a widget

A minimal stateless widget:

use ftui_widgets::Widget; use ftui_render::{Rect, frame::Frame}; struct Tag<'a> { label: &'a str } impl Widget for Tag<'_> { fn render(&self, area: Rect, frame: &mut Frame) { let text = format!("[{}]", self.label); frame.buffer.draw_text(area.x, area.y, &text, Default::default()); } fn is_essential(&self) -> bool { false } }

A minimal stateful widget:

use ftui_widgets::StatefulWidget; struct Counter; #[derive(Default)] struct CounterState { pub n: u64 } impl StatefulWidget for Counter { type State = CounterState; fn render(&self, area: Rect, frame: &mut Frame, state: &mut CounterState) { state.n = state.n.wrapping_add(1); // allowed: render may mutate let s = state.n.to_string(); frame.buffer.draw_text(area.x, area.y, &s, Default::default()); } }

Both snippets are end-to-end correct: no traits to derive, no macros, no registration.

What makes this library distinctive

A handful of widgets do things that are genuinely hard to find elsewhere:

Pitfalls

  • Rendering outside view(). Widgets can only register hit regions, cursor position, and link IDs during the view phase. Calling Widget::render from update() silently corrupts frame state.
  • Holding StatefulWidget state in the widget. The trait deliberately separates them. If you embed state in the widget, you have re-invented React’s single-store footgun.
  • Ignoring is_essential(). At EssentialOnly degradation, a widget that returns false simply won’t render. Inputs and primary outputs must override the default.

Where next