Pieces

A Piece describes a control, layout, or group of controls in a Day interface. Pieces compose into a tree, and the selected backend creates the native widgets. For example, a label becomes an NSTextField on macOS or a TextView on Android. The API tour has examples; this page explains composition and construction.

Start with a control

In code, a Piece is a small plain value, usually a builder struct returned by a free function:

use day::prelude::*;

label("Hello")                       // → Label (a builder)
button("Save").action(|| save())     // → Button
column((label("a"), label("b")))     // → Column

Composing trees

Containers take a tuple of children. Here, temp is a Signal<f64> shared by a slider and a label:

column((
    label("Temperature").font(Font::Headline),
    row((
        slider(temp).range(0.0..=40.0),
        label(move || format!("{:.1}°", temp.get())),
    ))
    .spacing(8.0),
))
.spacing(12.0)
.padding(16.0)

Tuples work up to sixteen children; past that (or when the shape is computed at runtime), collect into a PieceVec:

let stars: Vec<AnyPiece> = (0..5).map(|i| star(i)).collect();
row(PieceVec(stars)).spacing(4.0)

AnyPiece is the type-erased form: a boxed build closure. Reach for it at a boundary that needs one single type, such as a PieceVec like the one above, a stored builder, or a function that branches between two different pieces:

fn status_badge(online: bool) -> AnyPiece {
    if online {
        label(tr("online")).font(Font::Caption).any()
    } else {
        spinner().any()   // a different piece type, so both arms erase
    }
}

An ordinary page or component function does not erase. It returns a concrete piece type, or impl Piece to avoid naming that type:

fn settings_page(dark_mode: Signal<bool>) -> impl Piece {
    column((
        label(tr("settings_title")).font(Font::Title),
        toggle(dark_mode),
    ))
}

Day’s constructors preserve concrete types: column() returns a Column, labeled() a Labeled, and modifiers (.id(), .padding(), .on_tap() …) return Decorated<P>, which keeps the decorated piece’s type. Call .any() where a single AnyPiece type is required. Calling it on an AnyPiece returns the existing value without another allocation.

Because the type is kept, the piece’s builders can be chained after a generic modifier, in either order:

label("Saved").font(Font::Caption).padding(8.0)   // typed first
label("Saved").padding(8.0).font(Font::Caption)   // generic first — same result

A build-time branch between two different piece types takes Either rather than erasing both sides:

if compact { Either::Left(row(children)) } else { Either::Right(column(children)) }

The built-in vocabulary

The day prelude ships a small set of Pieces, grouped roughly as follows:

GroupPieces
Textlabel, text_area
Controlsbutton, toggle, slider, text_field, picker (menu/segmented/inline), progress, spinner
Layoutcolumn, row, zstack, grid, scroll, spacer, divider, form/section
Structurewhen, each, with_environment
Collectionslist (native recycling)
Drawingcanvas, shape (rectangle, circle, capsule, arc, …), image, vector
Navigationnav, nav_stack, nav_link, toolbar
Presentationalert, confirm, prompt, cover, menus

Anything beyond this vocabulary (a combo box, a map, a web view, a Lottie animation, an embedded SwiftUI view) lives in a separate piece crate (day-piece-*) that you add as an ordinary Cargo dependency. Optional widgets are separate dependencies, so apps include only the piece crates they use. The extension model explains how those crates plug in.

Each built-in has a reference page with per-platform notes under internal reference, for example text, lists, and dialogs.

What happens at build

Behind the builders sits one trait with one method:

pub trait Piece: 'static {
    fn build(self, cx: &mut BuildCx) -> RNode;
}

Two parts of that signature matter.

  • build takes self, not &self. A Piece is consumed exactly once. There is no retained view description that Day re-runs and diffs against the last frame.
  • It returns an RNode, a handle to a node in the realized tree: the live structure that owns the native widget, its layout state, and the reactive scope its bindings live in.

Your Piece functions run once, at mount time. Everything dynamic afterward flows through signals, which are bound to individual native attributes during that single build. The reactivity page covers what that means for your code, including the costs.

When a Piece’s build runs, three things are created together and live together:

   Piece (builder)          realized tree node             native widget
  ┌───────────────┐   build   ┌──────────────────┐  realize  ┌─────────────┐
  │ label("Hi")   │ ────────► │ kind: "label"    │ ────────► │ NSTextField │
  │  .id("hi")    │           │ handle ──────────┼───────────│  (AppKit)   │
  └───────────────┘           │ layout, flex     │           └─────────────┘
                              │ scope ──┐        │
                              │ id, a11y│        │
                              └─────────┼────────┘

                              reactive Scope: owns this
                              node's bindings + handlers
  • The node records the Piece’s kind, its place in the tree, its layout behavior, and its accessibility annotations.
  • The native widget is created immediately through the toolkit backend (an NSButton, a GtkEntry, …) and inserted into its native parent at the right index. Containers like column and row get a plain native container view; decorators (padding, frame) get no widget at all and exist purely in Day’s tree.
  • The scope owns every binding and event handler the build created. When the node is later removed (a when arm switches, an each row disappears), disposing the scope tears down its bindings and handlers in one step, and the native widget is released.

The details of that machinery (the tree structure, measurement, and how events travel back) are on How rendering works.

Conditional and repeated structure

Because build runs once, structural change is explicit. Two Pieces express it:

// A subtree that exists only while the condition holds. The closure re-runs
// when `cond`'s signals change; the old arm's scope is disposed.
when(move || logged_in.get(), move || profile_panel())

// With an else arm. Exactly one arm is mounted at a time, and the two need
// not return the same Piece type.
when(move || logged_in.get(), move || profile_panel())
    .otherwise(move || sign_in_form())

// A keyed collection. Rows are created, moved, and disposed by key diffing —
// surviving rows keep their nodes and native widgets.
each(
    items(
        move || todos.get(),      // data
        |t: &Todo| t.id,          // stable key
    ),
    |slot| todo_row(slot),        // per-row builder; slot tracks the item
)

each takes a row source and a row builder. items(data, key_of) is the row source for plain data; a model collection supplies one directly, and list accepts the same sources.

each compares the old and new key sequences to decide which rows to keep, build, or remove. A when flip or a row removal is a real structural edit (native widgets are added and removed), so it costs more than a bound-attribute update. For long scrolling data, prefer list, which hands rows to the platform’s recycling list widget instead of materializing every row.

Identity, for testing and accessibility

Any Piece can carry a stable string id:

button(tr("save")).action(save).id("save-button")

dayscript targets elements by id, accessibility uses them as stable automation identifiers, and debug output prints them. They’re optional everywhere, but pages you intend to test should id their interactive elements; day lint catches an id used twice and a navigate to a route that doesn’t exist.

Where Pieces come from

You can use pieces from the framework, combine them into your own components, or add native controls through separate crates:

  1. Built-ins: the vocabulary above, implemented in day-pieces with a renderer in every toolkit backend.
  2. Composite pieces: plain Rust functions or builder structs that compose existing Pieces. They need no native code and work on every target automatically. Most of your app is this; so are the in-tree day-piece-rating and day-piece-settings, and the star-rating tutorial.
  3. Native pieces: a new leaf widget with a per-toolkit implementation, registered at link time. This is how day-piece-webview wraps WKWebView/WebView/WebKitGTK, how day-piece-swiftui hosts your own SwiftUI views on macOS and iOS, and how you’d wrap a platform control Day doesn’t cover. See the native piece tutorial.

Composite pieces reuse existing widget implementations. Native pieces require an implementation for each toolkit you support (a piece that only implements AppKit and UIKit renders a labeled placeholder elsewhere, so the gap is visible and the app keeps running).


Next: Reactivity, the signals that update the widget tree.