Architecture
Day separates the shared UI model from platform-specific widget implementations. This page explains the crate boundaries and build process. Rendering follows the corresponding runtime path, from creating a widget to handling its events.
The crate graph
┌────────────────┐
your app ─────────► │ day │ facade: re-exports + one launch() per backend feature
└───┬───────┬────┘
│ │ (exactly one, by Cargo feature)
┌────────────┘ └──────────────┐
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ day-pieces │ built-in pieces │ toolkit backend │ day-appkit / day-uikit /
│ │ + Decorate API │ (one per binary) │ day-gtk / day-qt / day-android /
└──────┬───────┘ └────────┬─────────┘ day-xaml / day-arkui / day-dom /
│ │ day-mock
▼ │ implements
┌──────────────┐ realized tree, ▼
│ day-core │ layout engine, ┌──────────────┐
│ │ mounting, events │ day-spec │ the Toolkit trait, props/patches,
└──────┬───────┘ ◄──────────────────│ │ events, resources, window options
▼ └──────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ day-reactive │ │ day-geometry │ │ day-l10n / │
│ signals etc. │ │ Point/Size/… │ │ day-fluent │
└──────────────┘ └──────────────┘ └──────────────┘
day-cli (the `day` binary) day-script (dayscript engine, compiled into apps)
day-build (each app's build.rs: typed resource constants, SwiftUI bindings)
Support crates omitted from the diagram: day-fonts and day-vector (shared resource rules the
CLI and runtime agree on), day-toolchain (SDK discovery). Crash reporting lives in the external
day-piece-break repository.
JS/TS miniapps run on day-lite, which lives in its own
repository.
day-spec is the boundary between portable and platform code: it defines the Toolkit trait and
the descriptor types (LabelProps, ButtonPatch, events, …) that flow across it. day-core is
written against that trait and monomorphized over the concrete backend, so core code calls native
operations directly. Everything above day-spec is portable; everything below it is one platform’s
business.
Around the core sit the extension surfaces: pieces/day-piece-* crates add widgets
(extension model), parts/day-part-* add headless capabilities
(parts), and day-mock is a full Toolkit implementation with no display, used by
tests.
One binary per target
A Day binary contains exactly one backend, selected by a Cargo feature at compile time. The backend is chosen when the binary is built, so the AppKit build contains only AppKit code, and a call like “set this label’s text” compiles down to the backend’s concrete function.
The cost is that n targets mean n compilations (CI budgets around it; your laptop builds one at a time), and a Linux app ships as a GTK build or a Qt build rather than one binary that picks at runtime. The benefit is that a widget update compiles to a direct call into the one linked backend, and dead-code elimination works on whole toolkits.
The same idea extends to piece renderers: backends expose a link-time registry (a linkme
distributed slice), and each piece crate’s renderer registers into it during linking. Startup
iterates the slice once to build the kind → renderer table. Registration failures are link
errors, not runtime surprises. The one exception is day-dom: linkme has no wasm32
implementation, so the web backend keeps a runtime registry that pieces register into from
their constructors.
How a build works
day build -p <target> orchestrates; platform tools do the platform work. Most desktop targets
are plain cargo builds (each target gets its own CARGO_TARGET_DIR, so parallel target builds
never contend); the exception is macos-appkit, which builds through its platform/macos/
Xcode host project like a mobile target — including any macOS Swift a dependency contributes,
the SwiftUI embedding path. Mobile targets invert control with the callback pattern, borrowed
from Flutter: the checked-in platform project drives, and calls back into day for the Rust
part, so building from Xcode/Android Studio and building from the CLI produce identical results
and neither goes stale.
day build -p ios-uikit day build -p android-mdc
────────────────────────── ─────────────────────────────
day CLI day CLI
│ generates DayPieces SwiftPM pkg │ cargo-ndk → libapp.so per ABI
│ (piece Swift shims + deps) │ writes day-pieces.json (piece java/
▼ │ gradle deps/permissions) + app props
xcodebuild ──► "Build Rust (day)" phase ▼
│ calls `day xcode-backend gradle ──► reads the generated files,
│ build` → cargo staticlib │ stages jniLibs, merges manifests
▼ for the iOS triple ▼
Runner.app ◄── links libapp.a app-debug.apk
The same shape covers OpenHarmony (hvigor builds the ArkTS host around a cross-compiled
libentry.so), and macos-appkit generates its own DayPieces SwiftPM package (at
build/day/macos/DayPieces), referenced by the platform/macos/
Xcode host project. Metadata flows one way: Day.toml (identity) and the Cargo
version are conveyed into generated, gitignored files that the checked-in projects read; the
scaffolds themselves are never edited by tooling. Project structure documents every directory;
Packaging covers the signed-artifact pipeline built on top.
How each backend reaches its toolkit
Each backend crosses into its toolkit using the narrowest viable mechanism:
| Backend | Mechanism |
|---|---|
| AppKit / UIKit | objc2 bindings: Rust calls the Objective-C runtime directly, no shim |
| GTK | gtk4-rs (gobject bindings) |
| Qt | a small hand-written C++ shim (day-qt-sys) compiled by cc at build time; Rust calls its C API |
| XAML | same pattern with C++/WinRT (day-xaml-sys) |
| Android | JNI plus a small Java bridge class shipped with the framework; Rust holds GlobalRefs to widgets |
| ArkUI | the ArkUI NDK C API (day-arkui-sys) |
| DOM | a wasm32 extern "C" boundary implemented by a small JS shim the CLI embeds in the page |
Each shim creates widgets, sets properties, and forwards events. Layout, reactivity, and update policy live on the shared Rust side, so a new backend is a small amount of code.
Where the CLI fits
day-cli is a separate binary with no dependency on the UI crates. Its jobs: scaffolding
(day new), orchestration (build/launch across targets in parallel, streaming prefixed
logs), diagnosis (doctor, with per-toolkit probes and fix-it text), validation (lint),
distribution (pack, sign), and the plumbing subcommands the platform builds call back into.
It’s built in the flutter_tools mold: services behind injectable traits for testability, a
stable JSON event stream (--format json), and documented exit codes, so scripts and CI
consume the same interface people do.