Composition and Layout
painted’s layout layer is intentionally small: it’s a set of pure functions that transform Block → Block (or join multiple blocks into one).
This lets you build complex UI surfaces without introducing mutable layout state.
See also:
docs/ARCHITECTURE.md: stack + data flow (../ARCHITECTURE.md)
Joins
Join blocks horizontally or vertically, optionally with alignment and gaps.
class Align(Enum):
START = "start" # top or left
CENTER = "center"
END = "end" # bottom or right
def join_horizontal(*blocks: Block, gap: int = 0,
align: Align = Align.START) -> Block:
def join_vertical(*blocks: Block, gap: int = 0,
align: Align = Align.START) -> Block:
Padding and Borders
Padding adds empty space. Borders wrap content, optionally with a title.
def pad(block: Block, *, left: int = 0, right: int = 0,
top: int = 0, bottom: int = 0, style: Style = Style()) -> Block:
def border(block: Block, chars: BorderChars = ROUNDED,
style: Style = Style(), title: str | None = None,
title_style: Style | None = None) -> Block:
Truncation and Slicing
Truncation is the simplest “responsive layout”: cap width, show an ellipsis, preserve the rest of the composition pipeline.
Slicing (vslice) is the bridge to scrollable UIs: build a full block, then window into it.
def truncate(block: Block, width: int, ellipsis: str = "…") -> Block:
def vslice(block: Block, offset: int, height: int) -> Block:
Pattern: “compose, then paint”
The render loop usually looks like:
- Construct blocks from state (text, tables, lists).
- Compose them (join/pad/border/truncate).
- Paint the result into a buffer view.
The key mental model: layout is pure and local — no retained layout objects, no incremental mutation.