API tour

The whole authoring surface in one pass, for people who learn from code. Everything below is real Day API. The snippets are lifted from the showcase app that produces the gallery, and use day::prelude::*; brings all of it in. The pages after this one (Pieces, Reactivity, Layout) explain the model behind what you’ll see here.

A first app

launch takes window options and a root closure that returns the top Piece. It owns the native main loop.

use day::prelude::*;

fn main() {
    day::launch(
        WindowOptions {
            title: "Hello".into(),
            size: Size::new(480.0, 640.0),
            ..Default::default()
        },
        root,
    );
}

fn root() -> AnyPiece {
    label("Hello, native world").padding(24.0).any()
}

Signals: state that binds

A Signal<T> is a Copy reactive cell. Clone it into as many closures as you like.

let count = Signal::new(0i64);

count.get();            // read (tracks the caller as a dependency)
count.set(5);           // replace
count.update(|c| *c += 1);   // mutate in place
count.with(|c| c.abs());     // borrow without cloning
count.get_untracked();  // read without creating a dependency

Any closure that reads a signal becomes reactive: when the signal changes, only that binding re-runs. There is no component re-render and no tree diff.

// This label re-reads `count` whenever it changes; nothing else is touched.
label(move || format!("{count} clicks", count = count.get()))

Text, buttons, and layout

Pieces compose with plain function calls; containers take a tuple of children and expose builder methods for spacing, padding, and alignment.

column((
    label("Counter").font(Font::Title),
    row((
        button("").action(move || count.update(|c| *c -= 1)),
        label(move || count.get().to_string()),
        button("+").action(move || count.update(|c| *c += 1)),
    ))
    .spacing(8.0),
    divider(),
    spacer(),
))
.spacing(12.0)
.align(HAlign::Leading)
.padding(16.0)

Wrap any subtree in scroll(...) to make it scroll natively.

Inputs

Two-way controls take a signal directly; the user’s edits flow back into it (origin-tagged, so there is no feedback echo).

let name = Signal::new(String::new());
let volume = Signal::new(40.0);
let subscribed = Signal::new(false);

column((
    text_field(name).placeholder("Your name"),
    slider(volume).range(0.0..=100.0),
    toggle(subscribed),
))

Keyboard focus is a binding too: .focused(editing) ties a control to a Signal<bool>, or .focused((field, Field::Name)) binds one control of a form sharing a Signal<Option<Field>>: native focus changes write the signal, writing the signal moves focus (and None dismisses the soft keyboard on mobile). text_field(...).on_submit(...) handles the Return key, so chaining fields is one signal write. The focus reference has the rules and the per-platform map.

Conditionals and collections

when shows a subtree while a condition holds; it is itself reactive.

when(
    move || !name.with(|s| s.is_empty()),
    move || label(move || format!("Hi, {}", name.get())),
)

Keyed collections (each) build one child per item and reconcile by key when the list changes, so each row keeps its own state across updates.

Progress and canvas

progress takes a fraction (a value or a reactive closure); spinner is indeterminate. canvas hands you a native 2D drawing surface; Day never rasterizes it itself.

progress(move || volume.get() / 100.0);   // determinate, tracks the slider live
spinner();                                 // indeterminate

canvas(move |d, size| {
    let r = Rect::from_size(size).inset(8.0);
    d.stroke(Shape::Arc { rect: r, start_deg: 135.0, sweep_deg: 270.0 },
             Color::rgba(0.5, 0.5, 0.55, 0.35), 6.0);
    let frac = (value.get() / 100.0).clamp(0.0, 1.0);
    d.stroke(Shape::Arc { rect: r, start_deg: 135.0, sweep_deg: 270.0 * frac },
             Color::hex(0x2F6FDE), 6.0);
})

Day models navigation as a projection of an app-owned signal: you own the state, and the native container is reconciled to it. There are two primitives:

selector is a one-of-N choice bound to a Signal<String>. Its .style picks the native chrome: Sidebar becomes a NavigationSplitView (an AdwNavigationSplitView on GTK, an NSSplitView source list on macOS, a pushing list on mobile); Tabs becomes a native tab widget.

let section = Signal::new(String::new());
selector(section)
    .style(SelectorStyle::Sidebar)
    .title("My App")
    .header(sidebar_header)
    .item("home",     "Home",     home_page)
    .item("settings", "Settings", settings_page)

stack is a push/pop stack bound to a Signal<Vec<String>> path. Day reconciles the native stack (UINavigationController, AdwNavigationView, the Android back stack) to the path.

let path = Signal::new(Vec::<String>::new());
stack(path, home_view).destination(|key| detail_view(key))
// push:  path.update(|p| p.push("item-42".into()));
// the native back button writes the pop back into `path`.

Because each surface owns its own signal, nesting costs nothing: a Tabs selector or a stack inside a Sidebar selector needs no extra wiring. Keys don’t have to be strings: declare a day::routes! { enum Section { Home => "home", … } } enum (or implement Route by hand for keys that carry data, like Item { id: u32 }"item-42") and bind the selector to Signal<Option<Section>> and the stack to Signal<Vec<Item>>. It’s the same API, compile-checked (navigation guide).

A thin string-route adapter sits over those signals, so keys double as routes:

navigate("settings");   // select the settings section / tab
nav_back();             // pop the innermost surface
current_route();        // the full path, outermost surface first

The same keys drive deep links (DAY_DEEPLINK=settings) and dayscript automation (navigate: { route: settings }).

Localization and accessibility

Text localizes through Fluent with tr, including interpolated signal arguments. Every Piece can carry accessibility metadata.

label(tr("greeting").arg("name", name));

progress(move || volume.get() / 100.0)
    .a11y(|a| a.role(Role::Meter).label("Volume level"));

Ids and testing

Give any Piece a stable .id("…") and dayscript can find, drive, and assert it, using the same script on every platform.

button("Increment").action(move || count.update(|c| *c += 1)).id("increment-button")

Extending with Day Pieces

A native component you write (or install) plugs in like a built-in. The showcase’s flavor picker is an external combo_box Piece from a separate crate:

use day_piece_combobox::combo_box;

let flavors = Signal::new(vec!["vanilla".into(), "chocolate".into()]);
let flavor  = Signal::new(Some(0usize));
combo_box(flavors, flavor).id("flavor-combo")

Day Pieces ship as ordinary Rust crates. The extension model explains the tiers, from pure composition to per-toolkit native code.

Next: Pieces for the model behind all of this, or the CLI & projects that build, launch, and script it.