API style: argument clarity
Rust has no named arguments, so Day emulates their clarity where it pays and keeps SwiftUI-like terseness where it doesn’t. The rules, in priority order:
-
No bare
bool(or otherwise unreadable literal) as a configuration flag in a public signature. A call site must not readd.text(…, true)or.reorderable(true). Use a two-variant enum (TextAlign::Center,Boundary::Yes), a presence builder (.multi_select()/.no_separators()), or a feature value (rule 6). A positional control-flow condition is different: inwhen(cond, build)andapply_if(cond, f)the condition is the call’s subject, not a flag on something else, and it stays positional. -
Required bundles of 3+ concrete-typed values → a struct parameter with named fields at the call site. This is the closest Rust gets to named arguments:
d.text(&res::str::canvas_value(40_i64).format(), center, TextStyle { size: 22.0, color: accent, anchor: TextAnchor::CENTERED, ..Default::default() });This is already the house style at the spec boundary (
NavProps { title, split },TextFieldPatch::Text { text, from_native }); apply it to app-facing APIs whenever the fields are concrete types. The generatedres::strfunctions follow it for messages with two or more variables (res::str::items_moved(res::str::ItemsMoved { count, folder })). -
Generic-ergonomic constructors keep ≤3 positional, type-distinct arguments.
route("controls", res::str::nav_controls(), controls_page)andCommand::new(id!(add), res::str::add(), add_item)stay positional: the three types are mutually incompatible, so every mis-ordering is a compile error, and funnelingimpl IntoText<M>through struct fields would force.into()/Box::newnoise at every call site (struct literals don’t do implicit conversion). Names would cost more ergonomics than they buy. -
Optional configuration → builder methods (
.spacing(Space::S),.align(…),.padding(Space::L)). Never grow a constructor’s positional list for options. -
Conventional-order exemptions. Universally-fixed orders stay positional even with same-typed arguments:
Color::rgba(r, g, b, a),Size::new(w, h),.frame(w, h), rect(x, y, w, h). -
Coupled configuration is one value with its own builder. A feature whose settings only make sense together (search: the query, the prompt, the placement, the reveal policy; reorder: the handler, the guard, the availability) is one value —
Search::new(query).prompt(..),Reorder::new(f).guard(g),ContentList::new(build).width(w),Sections::restored(key)— handed to one method (.searchable(search),.reorderable(reorder)). Never a prefixed-method family (search_prompt,search_placement, …), and never a method whose effect depends on a prior call (“call aftersearchable”, “applies to the item just added”). The piece is in the feature state exactly when it carries the value, so the feature cannot be half configured. -
Three spellings for a reactive input. Two-way state is
impl Binding<T>(aSignal<T>, a day-modelField, aMappedview; stored asAnyBinding<T>). A read isimpl IntoReactive<T, M>(a constant, a signal, or a closure, all accepted by one method:.enabled(false),.enabled(busy),.enabled(move || !busy.get())). An event isimpl Fn. Text follows rule 8. Static configuration is a plain value, and a collection is anIntoIteratoror a closure that derives it. A bareSignal<T>parameter, or afoo/foo_when/foo_reactivepair, is a violation: one method, one marker-generic parameter. -
Anything a person reads is
impl IntoText<M>: a label, a title, a prompt, a tooltip, a menu entry, a delete affordance, an accessibility string. The caller passes a literal, aLocalizedText(res::str::x(), no.format()), aSignal<String>or a closure, and the text is live. Filenames, ids and keys are the onlyInto<String>text. -
Modifiers are classified, and a piece’s docs name its semantic target. Identity modifiers (
id,id_of) name the piece’s own node wherever they sit in the chain; outer-layout facts (grid_span,grid_align) land on the outermost node whatever follows; wrapping modifiers are ordered, as they read. See “Modifier classes” below. No rule may ask the caller to place.idbefore.padding. -
Items are values. A nav item, a toolbar entry, a menu entry, a form row, a picker option is a value —
item(key, title).icon(..).page(..),toolbar_button(id, label),menu_item(title),(value, title)— taken as a value, a tuple of values, anOption, aVec, or a derived list, never as positional arguments to the container plus trailing “decorates the last added” calls. Decorations go on the item. -
Builder traits are generated with
#[day::builder(FooBuilder)]on the piece’s inherentimplblock. Hand-written*Buildertraits andmap_innerforwards are not accepted.
Scope: the rules bind the app-facing surface (day-pieces, the Day umbrella, day-core’s
BuildCx/nav API, the piece crates). The engine’s internal interfaces (the Toolkit trait,
TreeOps, FFI shims) prefer the same, but a documented bool parameter is acceptable where
changing it would ripple through every backend for internal call sites only.
The UI snippets use generated res::str accessors from an app’s Fluent catalog; see the
example messages and catalog setup.
Typed builders and erasure
A builder method must not throw away the piece’s type. Two rules follow from that:
-
Generic modifiers return
Decorated<Self>, neverAnyPiece. EveryDecoratemethod, and every extension trait a toolkit orday-tweak-*crate adds (.gtk(…),.tooltip(…),.tickmarks(…)), returnsDecorated<Self>.Decoratedcarries an ordered op list beside the piece it wraps, and its inherent methods shadow the trait’s, so chains stay flat rather than nestingDecorated<Decorated<…>>. The one exception isDecorate::modifier, becauseModifieris defined overAnyPieceand cannot preserve a type it never sees. -
A piece’s own builders are reachable after a modifier through a generated
*Buildertrait.Label’s inherent methods are the implementation;#[day::builder(LabelBuilder)]on thatimplblock emits the trait, its impl forLabel, and the forwarding impl forDecorated<Label>, which is what makeslabel(…).padding(8.0).font(…)resolve, so a piece never imposes a “typed modifiers first” ordering rule on its callers.#[day::builder(GaugeBuilder)] // `Decorated` and `Piece` in scope (use day::prelude::*) impl Gauge { pub fn ticks(mut self, n: usize) -> Self { self.ticks = n; self } }Every
pub fnthat takesselfby value and returnsSelfbecomes a trait method;&selfaccessors, non-pubhelpers and methods returning anything else are left out. A generic piece names the parameters the trait carries (#[day::builder(NavBuilder<K>)]), and their bounds are copied from the impl. Name the trait after the piece (LabelBuilder,ButtonBuilder,ColumnBuilder,RowBuilder);*Stylenames belong to the value enums (PickerStyle,NavStyle). Skip any method whose nameDecoratealready defines: the inherent method onDecoratedwins resolution, so a forwarded twin would never be called.
Erasure stays explicit and one-way: .any() at a boundary that needs a single AnyPiece (a
Vec<AnyPiece> of differently typed children, an -> AnyPiece signature, a stored piece). It is
free on a piece that is already erased (AnyPiece::any is inherent and returns self). A
runtime-sized list of one piece type needs no erasure: Vec<P>, [P; N] and Option<P> are
child sequences (row(stars), row((icon, cond.then(|| badge))); None builds nothing), and
Option<P> is a Piece in its own right. A build-time branch between two piece types uses
Either<A, B> rather than erasing both arms; a branch on a signal uses when(…).otherwise(…),
keyed, or switch.
Deferring to build time is not such a boundary. A constructor whose body must wait for the
build (it reads an ambient environment, a scope, or the laid-out size) defers through
piece_fn, which returns the concrete PieceFn<F>, so canvas, frame_clock, shape_group,
shape_group_fn, each and with_environment return impl Piece and cost no box. Where the
deferred piece is worth a name, it defers inside its own build instead and stays a struct:
labeled reads the enclosing form’s shared label column at build time and still returns
Labeled<P>. No constructor in day-pieces returns AnyPiece.
form and labeled were the last two that did, until 2026-08-24. Neither half of the reasoning
justified the exception. The first was that form rows are collected more
often than consumed inline — across day and eleven app repos, 10 of ~170 labeled call sites
needed a uniform type, because rows go into section((…)) tuples and PieceSeq accepts
heterogeneous tuples. The second was that erasing bounds monomorphization, which is true but
cheap to give up: un-erasing both cost +90 KB of machine code on Day-Showcase’s macos-appkit
release binary (+0.81% of __text, +0.40% stripped) and +10% on that crate’s compile time. The
Option/Vec/array child impls and the generated builder traits are to be measured the same
way (a stripped release build of Day-Showcase, __text and total); no figure is recorded yet.
There is no From<P> for AnyPiece, and there cannot be a blanket one: AnyPiece implements
Piece, so a blanket impl collides with core’s reflexive impl<T> From<T> for T. .any()
is the single spelling.
Modifier classes
A Decorated<P> chain is a piece plus its modifiers, and only one class of modifier is ordered:
- Identity (
id,id_of) names the piece’s own node, the one itsbuildreturns, wherever it sits in the chain:label(t).padding(8.0).id(id!(x))andlabel(t).id(id!(x)).padding(8.0)are the same tree. A dayscript step addressed to that id reaches the handler a wrapping modifier above it registered (.padding(..).on_tap(..)), through every wrapper the same chain built, a native container such as.background(..)included, as a native gesture on an inner view reaches the recognizer on its superview. The id never has to move to make a tap land, and the step stops at the chain’s outermost node, so a tappable card around a button leaves the button’s tap alone. - Wrapping modifiers (
padding,frame,background,grow,a11y,on_tap, …) each apply to what came before, so they compose outward in call order, as they read. This is the class an ordering rule may still apply to:.a11yannotates the node built so far, and a tweak goes after a modifier that rebuilds the backing widget (docs/tweaks.md). - Outer-layout facts (
grid_span,grid_align) describe the piece to the container it sits in, so they land on the outermost node whatever wrappers follow them (docs/grid.md).
wrapper_id is the one way to name a wrapper deliberately: it tags whatever node is outermost
at the point it is called, for a frame assertion on a padded box. A piece whose own node is not
the obvious target (a stepper’s field, a list’s scroller) says so in its docs; .id goes there.
Tokens
Two value vocabularies keep configuration semantic where a literal would be a magic number.
Spaceis the spacing scale:XS(4),S(8),M(12),L(16),XL(24) points, accepted wherever a container or modifier takes a gap or an inset (.spacing(Space::S),.padding(Space::L),Insets::symmetric(Space::L.into(), Space::XS.into())). Fonts are semantic (Font::Headline) and the house rule forbids point sizes; spacing is the same kind of thing, with room for the platforms to resolve a step differently. A rawf64stays accepted, andInsets::{all, symmetric, horizontal, vertical, top, bottom, leading, trailing}build an inset without a struct literal (horizontal_total()/vertical_total()sum a pair).TextPresetnames a text style once:const CAPTION: TextPreset = TextPreset::new(Font::Caption).secondary();thenlabel(t).preset(&CAPTION). EveryTextPresetbuilder isconst, so a preset is a constant, and.presetapplies the sameLabelBuildersteps the equivalent chain would (docs/text.md)..card()is the same move for a surface: the platform’s grouped-content surface with rounded corners and aSpace::Minset, in place of apadding/background/corner_radiusidiom.