API tour
The examples below cover common Day controls and UI patterns, drawn from the Showcase app in
the gallery. They use day::prelude::* for the common APIs. For the underlying
concepts, see Pieces, Reactivity, and Layout.
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() -> impl Piece {
label("Hello, native world").padding(24.0)
}
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.
// This label re-reads `count` whenever it changes; nothing else is touched.
label(move || format!("{count} clicks", count = count.get()))
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("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
(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);
let size = Signal::new(0usize);
column((
text_field(name).placeholder("Your name"),
slider(volume).range(0.0..=100.0),
toggle(subscribed),
picker(["Small", "Medium", "Large"], size).segmented(),
))
picker is one-of-N 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(move || format!("Hi, {}", name.get())),
)
.otherwise(|| label("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);
})
Navigation
(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("My App")
.header(sidebar_header)
.item("home", "Home", home_page)
.item("settings", "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/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).
Deep links and dayscript
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(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, 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("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.