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 uses the toolkit contract to manage trees and delegate widget operations. Public portable APIs can still have unsupported target operations; a dependency boundary alone does not establish complete runtime support.

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. A missing or discarded registration can instead produce a runtime placeholder. Link success alone does not prove that every renderer was retained; exercise the extension on the target. 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 -t <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 -t ios-uikit                    day build -t 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 generated projects are derived under build/, while explicitly materialized native projects are app-owned customization overrides. day project add-target can create host sources. Inspect the selected host mode before editing a generated file. 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 through openharmony-rs’s ohos-sys bindings; the ArkTS-facing module through ohos-rs’s napi-ohos (no C++)
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 policy and reactivity have shared Rust implementations. A backend must still implement widget semantics, native measurement, callbacks, lifetime, resources, and platform integration; code size and effort are not established by this boundary.

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 JSON event stream (--format json) whose current schema must be checked against the CLI version, and documented exit codes, so scripts and CI consume the same interface people do.

Source and lifecycle contracts

The facade manifest selects backend features; winui includes xaml because they share one implementation. The core launch path installs the toolkit, scheduler, and executor hooks before building the app tree. Platform::RUN_ENDS_APP distinguishes an owned loop from a host-owned loop; a return from a mobile/web host setup is not necessarily app exit. Lifecycle and async shutdown document cleanup and the limits of OS callbacks.

Use backend contracts for implementation obligations and design records for proposals. The extension model covers optional native contributions. See the platform reference for target requirements and limitations.