Architecture

This page is the map: which crates exist, why the boundaries sit where they do, and how a build actually happens on each platform. The companion page, How rendering works, follows a widget through the running system.

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-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)

The boundary everything crosses is day-spec: 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, with no dyn dispatch and no message bus. 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. There is no runtime toolkit registry, no abstraction layer choosing a backend at startup. The AppKit build literally does not contain GTK code, and a call like “set this label’s text” compiles down to the backend’s concrete function.

The costs of this choice are the ones you’d guess: n targets mean n compilations (CI budgets around it; your laptop builds one at a time), and there’s no single “universal Linux binary” that picks GTK or Qt at runtime. The benefit is that a widget update compiles to a direct call into the one linked backend, with no dispatch layer between, 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.

How a build works

day build -p <target> orchestrates; platform tools do the platform work. Desktop targets are plain cargo builds (each target gets its own CARGO_TARGET_DIR, so parallel target builds never contend). Mobile targets invert control with the callback pattern, borrowed deliberately 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). Metadata flows one way: Day.toml (identity, version) is 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.

The native seams

Each backend crosses into its toolkit using the narrowest viable mechanism:

BackendSeam
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)

The shims are deliberately boring: create widget, set property, forward event. All policy (layout, reactivity, when to update what) lives on the shared Rust side, which keeps each new backend’s surface area small and auditable.

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.