Skip to Content
ftui-layoutOverview

Layout Overview

ftui-layout is the deterministic geometry engine for FrankenTUI. It has no knowledge of colors, text, or rendering — it turns Rects and Constraints into more Rects, and it decides where split boundaries live when a user drags an edge with a trackpad. That’s it.

Two pillars anchor the crate. They share a file tree and a crate name, but they serve very different purposes, and most widgets only ever need the first one.

The two pillars

Everything else in the crate — breakpoints, the e-graph optimizer, incremental cache, dep-graph — supports one of these two pillars.

Mental model

┌─────────────────────────────────────────────────────────────┐ │ ftui-layout │ │ │ │ ┌─────────────────────┐ ┌───────────────────────────┐ │ │ │ Flex + Grid │ │ Pane workspace │ │ │ │ (constraint solver)│ │ (9K-line split-tree) │ │ │ │ │ │ │ │ │ │ Rect ──split──▶ │ │ PaneTree + Operations │ │ │ │ Vec<Rect> │ │ PaneInteractionTimeline │ │ │ │ │ │ PaneDragResizeMachine │ │ │ │ stateless, │ │ PaneSemanticInputEvent │ │ │ │ per-frame │ │ magnetic docking │ │ │ └─────────────────────┘ └───────────────────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ 90%+ of widget layout User-driven workspace UX │ │ (status bar, sidebar, grid) (IDE-style tmux-style panes) │ └─────────────────────────────────────────────────────────────┘

Which one do I use?

Reach for Flex / Grid when…

You want to divide a Rect into sub-rects for a widget tree. You know at build time (or at render time) the constraints: “left sidebar is 30 cells, main is Fill, footer is 1 row.” The decision is per-frame and stateless — you call split() inside view() and the result lives one frame.

Reach for PaneTree when…

The user interactively manages layout. They drag dividers. They split panes with a keybind. They swap, close, undo, redo, and you need that entire history to be serializable to disk and replayable byte-for-byte. Think tmux, Zellij, or the editor/terminal pane of an IDE.

You almost never need both at once

A widget built with Flex inside a single pane leaf is the normal pattern. The pane system positions the leaf; the leaf uses Flex to lay out its internals. They do not fight.

The shared philosophy

Both pillars obey the same five rules:

  • Deterministic. Same inputs, same outputs, every time. No RNG, no clock-dependent behavior, no global state.
  • Serializable. Constraints and pane trees can be written to disk and read back. Forward-compatible extensions bags let you round-trip future fields.
  • Validated. Malformed trees are rejected with diagnostic reports. Overspecified constraint sets produce a best-effort solve, not a panic.
  • Allocation-conscious. Rects is a SmallVec<[Rect; 8]>; most layouts never touch the heap.
  • Host-agnostic. The crate does not import ftui-render. Terminal and web backends consume the same outputs.

ftui-layout has zero unsafe code (#![forbid(unsafe_code)] at the crate root). The complexity lives in the algorithms, not in raw pointers.

A representative example — both pillars in one screen

A three-pane workspace (editor | terminal / preview), where the editor is internally a Flex-based status bar over a content area:

┌──────────────────────┬──────────────────────┐ │ editor pane (leaf) │ terminal pane (leaf) │ │ ┌──────────────────┐ │ │ │ │ gutter │ content │ │ ──────────────────── │ │ │ │ │ │ preview pane (leaf) │ │ └──────────────────┘ │ │ │ status bar │ │ └──────────────────────┴──────────────────────┘ └── Flex::vertical() ──┘ └── PaneTree: HSplit(left, VSplit(top, bot)) ──┘

The PaneTree owns the vertical divider between columns and the horizontal divider in the right column. The user can drag them. Inside the editor leaf, a Flex::vertical owns the split between content and status bar — the user cannot drag that divider because the widget author didn’t expose it.

Where to go next