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:

BackendMechanism
AppKit / UIKitobjc2 bindings: Rust calls the Objective-C runtime directly, no shim
GTKgtk4-rs (gobject bindings)
Qta small hand-written C++ shim (day-qt-sys) compiled by cc at build time; Rust calls its C API
XAMLsame pattern with C++/WinRT (day-xaml-sys)
AndroidJNI plus a small Java bridge class shipped with the framework; Rust holds GlobalRefs to widgets
ArkUIthe ArkUI NDK C API (day-arkui-sys)
DOMa 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.