Skip to content
UrushiDocumentation

View

A View is Urushi’s central presentation value: a renderer-neutral description of content and composition before any terminal surface draws it. Every View resolves to a rectangle of styled terminal cells. The same View can become static CLI output, a prompt frame, an Urushi TUI frame, cells in an existing Ratatui application, or a fallback region beneath terminal graphics.

Styles decorate Views. Layout combines and sizes them. Components and themes produce them. Canvas is a free-positioned View node. Output surfaces decide when and where the resolved result is drawn. This shared value is what lets the rest of Urushi compose without sharing one event loop or terminal lifecycle.

Every card above is still a View. The difference is how the value is built, not which renderer or application surface can use it.

Run the complete View quickstart ↓

Need Use
One plain or multi-style text flow text or styled_text
Box model, border, title, dimensions, alignment, or overflow block or titled_block
Horizontal arrangement row
Vertical arrangement column
Shared column widths grid
Finite window onto larger content viewport
Report a resolved region or cursor origin anchor_block or anchor
Explicit coordinates, overlap, or line geometry Canvas
Reusable list, table, tree, or scrollbar meaning Components

A View is a value, not an event loop or a terminal session. Text, blocks, rows, columns, grids, viewports, and Canvas surfaces all resolve to rectangles. That common shape lets a bordered block sit beside a table or Canvas just as text can.

Build the complete tree first, then resolve it once under an Available area:

View resolution

Resolution applies one available area to the complete immutable View tree.

  1. dataView treeContent and composition
  2. with
    dataAvailable areaFinite or unbounded axes
  3. resolve
    processLayout resolutionMeasure, allocate, place, and collect anchors
  4. result
    dataResolvedViewA rectangle of styled cells

Resolution performs terminal-cell measurement, wrapping, box sizing, allocation, placement, viewport projection, and anchor collection. A renderer then turns the ResolvedView into ANSI output, a prompt frame, an Urushi TUI screen, or Ratatui cells. print_view and println_view perform resolution and rendering for standard streams.

Available::NONE means unbounded, not zero. Nodes with intrinsic content can resolve without an allocation. Area-dependent claims such as Length::Fill, projected viewport axes, and a Canvas without a fallback extent require a finite allocation.

A viewport does not scroll itself, an anchor does not own focus, and a View does not read events or retain application state. The application changes its state and produces a new View; the selected output surface owns its lifecycle.

This example stacks a heading above two bordered status blocks:

Terminal window
cargo new view-demo
cd view-demo
cargo add urushi

Replace src/main.rs with:

use std::io;
use urushi::{
Align, BlockStyle, Border, Color, TextStyle, VerticalAlign, View,
};
fn status(name: &str, value: &str, color: Color) -> View {
View::titled_block(
BlockStyle::new().border(Border::ROUNDED).padding((0, 1)),
name,
View::text(value, TextStyle::new().foreground(color).bold()),
)
}
fn main() -> io::Result<()> {
let statuses = View::row(
VerticalAlign::Top,
[
status("Build", "ready", Color::GREEN),
status("Tests", "passing", Color::CYAN),
],
);
let view = View::column(
Align::Left,
[
View::text("Workspace", TextStyle::new().bold()),
statuses,
],
);
urushi::println_view(&view)
}

Run it with cargo run:

Rendered output

Workspace
╭ Build ╮╭ Tests ──╮
│ ready ││ passing │
╰───────╯╰─────────╯

row arranges children horizontally; column arranges them vertically. Each block measures its title, padding, border, and child before the parent row chooses its height.

Use the free resolve function for a one-shot View or when the caller does not retain a rendering object. Use one Resolver when the same host repeatedly resolves immutable View snapshots and wants unchanged subtree output to be reused between frames. Both paths produce the same observable ResolvedView.

This complete example resolves two frames. Only the viewport origin changes, so the retained child can be reprojected instead of being assembled again:

use std::error::Error;
use urushi::{
Available, Projection, ProjectionBoundary, RenderSettings, Resolver,
TextStyle, View, Viewport, render,
};
fn frame(origin: i64) -> View {
View::viewport(
Viewport::horizontal(Projection::new(
origin,
ProjectionBoundary::Preserve,
)),
View::text("ABCDEFGH", TextStyle::new()),
)
}
fn main() -> Result<(), Box<dyn Error>> {
let mut resolver = Resolver::new();
let area = Available::size(4, 1);
let first = resolver.resolve(&frame(0), area)?;
let second = resolver.resolve(&frame(2), area)?;
assert_eq!(render(&first, &RenderSettings::default()), "ABCD");
assert_eq!(render(&second, &RenderSettings::default()), "CDEF");
resolver.clear();
let rebuilt = resolver.resolve(&frame(2), area)?;
assert_eq!(rebuilt, second);
Ok(())
}
Two resolved frames
frame 1, origin 0: ABCD
frame 2, origin 2: CDEF
after clear: CDEF

clear drops every retained evaluation artifact; it does not change layout semantics. Call it when the host deliberately wants a cold rebuild. Ordinary View or available-area changes already invalidate the affected artifacts, so application code does not clear the Resolver after every state update.

The Resolver retains reconciliation inputs and materialized View output only. It does not own application state, clocks, event handling, redraw scheduling, terminal capabilities, graphics protocol state, or the output surface. The host updates its state, builds the next immutable View, decides when to draw, and keeps one Resolver with that drawing lifecycle.