API tour

The examples below are API fragments showing common controls and UI patterns. They are not complete apps, and no screenshot-to-code attribution is established for them. Use the beginner journey and technical recipes for designated compiled code. They use day::prelude::* for the common APIs. For the underlying concepts, see Pieces, Reactivity, and Layout.

The UI snippets use generated res::str accessors from an app’s Fluent catalog; see the example messages and catalog setup.

A first app

launch takes window options and a root closure that returns a Piece. Entry points and loop ownership vary by backend; mobile/web hosts can own the loop. The desktop fragment below needs a manifest, build script, and resource catalog as shown in the first-app tutorial.

use day::prelude::*;

day::resources!();

fn main() {
    day::launch(res::window_options().size(480.0, 640.0), root);
}

fn root() -> impl Piece {
    label(res::str::hello_native_world()).padding(Space::XL)
}

Signals: state that binds

A Signal<T> is a Copy reactive cell; copying copies a handle to one shared slot, not the value, so the same signal can live in as many closures as you like. Reading inside a bound closure subscribes it; writing re-runs exactly the closures that read it (Reactivity explains the model).

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

A closure passed to a reactive property, such as a label’s text, tracks its signal reads. Changing the signal reruns that binding. An ordinary closure or event handler does not become reactive just because it reads a signal.

// The message reads the signal and follows both count and locale changes.
label(res::str::click_count(count))

Text, buttons, and layout

(Layout is the full model behind the containers here.)

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

column((
    label(res::str::counter()).font(Font::Title),
    row((
        button(res::str::decrement())
            .action(move || count.update(|c| *c -= 1))
            .id(id!(decrement_button)),
        label(res::str::counter_value(count)).id(id!(counter_label)),
        button(res::str::increment())
            .action(move || count.update(|c| *c += 1))
            .id(id!(increment_button)),
    ))
    .spacing(Space::S),
    divider(),
    spacer(),
))
.spacing(Space::M)
.align(HAlign::Leading)
.padding(Space::L)

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

Inputs

(Each input is a piece; Pieces covers the vocabulary.)

Editable controls take a signal directly. User input changes the signal, and changes from your code update the control.

let name = Signal::new(String::new());
let volume = Signal::new(40.0);
let subscribed = Signal::new(false);
#[derive(Clone, Copy, PartialEq)]
enum Size {
    S,
    M,
    L,
}
let size = Signal::new(Size::M);

column((
    labeled(
        res::str::name(),
        text_field(name)
            .placeholder(res::str::your_name())
            .id(id!(name_field)),
    ),
    labeled(
        res::str::volume(),
        slider(volume).range(0.0..=100.0).id(id!(volume_slider)),
    ),
    labeled(
        res::str::subscribe(),
        toggle(subscribed).id(id!(subscribe_toggle)),
    ),
    picker(
        size,
        [
            (Size::S, res::str::small()),
            (Size::M, res::str::medium()),
            (Size::L, res::str::large()),
        ],
    )
    .segmented()
    .a11y(|a| a.label(res::str::size()))
    .id(id!(size_picker)),
))

picker is one-of-N over a typed value (size is a Signal<Size> here, any Clone + PartialEq type works) with three native stylings (.menu(), .segmented(), .inline()); text_area is the multi-line counterpart of text_field.

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. Chain .otherwise for the else arm.

when(
    move || !name.with(|s| s.is_empty()),
    move || label(res::str::greeting(name)),
)
.otherwise(|| label(res::str::tell_me_your_name()))

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 = (volume.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);
})

(The whole model, with per-platform mappings: Navigation.)

Navigation state lives in signals too. Changing a selection or path updates the native container; using its tabs or back button updates the signal. Choose between two containers:

nav 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());
nav(section)
    .style(NavStyle::Sidebar)
    .title(res::str::my_app())
    .header(sidebar_header)
    .item("home", res::str::home(), home_page)
    .item("settings", res::str::settings(), settings_page)

nav_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());
nav_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`.

Each navigation surface uses a separate signal. A Tabs [nav host](/docs/reference/glossary#nav host) or a nav_stack inside a Sidebar nav host can be nested without additional state synchronization. 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 nav host to Signal<Option<Section>> and the stack to Signal<Vec<Item>>. It’s the same API, compile-checked (navigation guide).

A string-route adapter maps those signals to routes, so keys also serve 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; deep links reference) and dayscript automation (navigate: { route: settings }; testing with dayscript).

Localization and accessibility

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

label(res::str::greeting(name));

progress(move || volume.get() / 100.0)
    .a11y(|a| a.role(Role::Meter).label(res::str::volume_level()));

Ids and testing

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

button(res::str::increment())
    .action(move || count.update(|c| *c += 1))
    .id(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, with free-form text entry plus a native dropdown, both bound to signals:

use day_piece_combobox::combo_box;

let flavors = Signal::new(vec!["vanilla".into(), "chocolate".into()]);
let flavor  = Signal::new(String::new());   // the typed-or-picked text
combo_box(flavors, flavor).id(id!(flavor_combo))

Day Pieces ship as ordinary Rust crates. The extension model explains the tiers, from pure composition to per-toolkit native code. On macOS and iOS the same mechanism hosts your own SwiftUI: declare a local Swift package and call the generated typed constructor (crate::swiftui::MyView(title, count)) like any other piece (SwiftUI embedding).

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