The extension model
Day extensions are Cargo crates that compose existing pieces, configure native widgets, or add new platform implementations. The amount of native code depends on the approach. Composition reuses existing backends; a tweak configures a widget; a native piece adds a control. Day’s build tools include the extension’s declared sources and dependencies.
Tier 0: pieces composed from existing pieces
A composite piece is Rust code that arranges existing Pieces. It needs no native code or registration, and it works on every target automatically because it bottoms out in Pieces that already do.
pub fn rating(value: Signal<usize>) -> Rating { … } // a row of tappable canvas stars
// consumers:
rating(stars).max(5).editable(true)
Most reusable UI in a Day app is this tier: cards, badges, form rows, charts drawn with
canvas. The shipped day-piece-rating and day-piece-settings crates are composite pieces,
and the composite piece tutorial provides a complete example.
Tier 1: a native leaf widget per toolkit
When the platform has a control Day doesn’t wrap (a combo box, a web view, a map), you write a native piece: one cross-platform front end plus a renderer per toolkit you support.
The front end defines the piece’s identity and its props/patch protocol, and creates a leaf node
(abridged from pieces/day-piece-combobox):
pub const KIND: &str = "day.piece.combobox";
/// A config struct in the usual builder shape; `impl Piece` does the wiring.
pub fn combo_box(items: Signal<Vec<String>>, text: Signal<String>) -> ComboBox { … }
impl Piece for ComboBox {
fn build(self, cx: &mut BuildCx) -> RNode {
let node = cx.leaf(KIND, &ComboProps { … }, Flex { grow_w: true, ..Default::default() });
bind_seeded(initial_items, move || items.get(), move |v: &Vec<String>| {
with_tree(|t| t.patch(node, Box::new(ComboPatch::Items(v.clone())), true));
});
cx.on(node, move |ev| if let Event::TextChanged(t) = ev { /* write the signal */ });
node
}
}
Each backend contributes make (create the native widget) and update (apply a patch),
registered at link time:
// inside #[cfg(feature = "appkit")] — creates an NSComboBox
day_pieces::renderer!(day_appkit::RENDERERS, AppKit,
kind: KIND, props: ComboProps, patch: ComboPatch,
make: make, update: update);
The renderer! macro places an entry in the backend’s link-time registry (a linkme
distributed slice), so the app that depends on your crate gets your renderer through the linker
alone, with no manifest to write, and an app that doesn’t enable your crate’s feature for a given
toolkit compiles none of it.
Two companion macros round this out. day_pieces::glue_modules!(appkit, gtk, …) declares the
feature-and-target-gated module index binding each lib-<toolkit>.rs, the one-liner every
shipped piece uses in place of a hand-written #[cfg]/#[path] block. Web is the one
exception to link-time registration: linkme has no wasm32 implementation, so a web-dom
renderer uses dom_renderer! and registers at runtime from the piece’s constructor.
A piece that implements some toolkits and not others renders a labeled placeholder on the rest, so the gap is visible and coverage can grow toolkit by toolkit. The native piece tutorial walks through all six desktop/mobile backends for one control.
Native dependencies without scaffold edits
Native pieces often wrap a platform library: Lottie’s iOS framework, a Maps SDK’s Gradle artifact. A piece crate declares these in its Cargo metadata:
[package.metadata.day.ios]
swift = ["platform/ios/swift"] # Swift shim sources shipped in the crate
swift-packages = [ … ] # SwiftPM dependencies; a local entry
# ({ path = "swiftui", products = ["MyViews"] }) is scanned for
# SwiftUI views and exported as typed Rust bindings
frameworks = ["WebKit"]
# platform = "16.0" # minimum-OS floor (max across crates wins)
[package.metadata.day.macos] # same shape as .ios, for the macos-appkit leg
swift-packages = [ … ]
[package.metadata.day.android]
java = ["src/DayLottie.java"] # Java sources shipped in the crate: a dir, or one file
gradle-dependencies = ["com.airbnb.android:lottie:6.4.0"]
permissions = ["android.permission.INTERNET"]
# also: res, gradle-repositories, proguard, manifest-components
[package.metadata.day.ohos]
ets = ["platform/harmony/ets"] # ArkTS components (HarmonyOS)
[package.metadata.day.permissions]
uses = ["camera"] # portable permission names, mapped per platform
The extending reference documents every key.
At build time, day build resolves every piece in your app’s dependency graph via
cargo metadata and regenerates the glue the platform projects reference: a local SwiftPM
package for the Xcode side, a JSON manifest the Gradle build reads for Java sources,
dependencies, and merged permissions. On macos-appkit the same aggregation produces
build/day/macos/DayPieces, which its Xcode host project references. Your checked-in platform scaffolds never change; only
generated, gitignored files do. (This is the same architecture Flutter uses for plugin
registration, adapted to Cargo.)
Tier 2: native-language halves
Pieces implemented partly in a platform’s language use native halves: the crate includes
Swift, Java, ArkTS, or C++ sources, declares them under [package.metadata.day.<platform>], and
its tier-1 Rust renderer adopts the views those shims create.
For code that must be written in Swift, that need is covered today by
SwiftUI embedding: a SwiftPM package’s public views become typed Rust
constructors (crate::swiftui::MyView(…)) on macos-appkit and ios-uikit. The matching
Kotlin/Compose leg is not built yet.
Parts: capabilities without UI
Extensions that don’t render (battery, clipboard, Bluetooth) are parts, which
skip all of the above machinery: a part is plain #[cfg]-dispatched functions, with no kind,
renderer, or registry (plus the same Cargo-metadata mechanism when Android needs Java or permissions). The
part tutorial covers six platform implementations of one API.
Choosing a tier
does it render anything?
├─ no → part
└─ yes → is it an existing widget that just needs configuring?
├─ yes → tweak (/docs/tweaks — configure a widget)
└─ no → can you build it from existing pieces (incl. canvas)?
├─ yes → composite piece (uses existing backends)
└─ no → native piece (per-toolkit renderers, placeholder elsewhere)
└─ implementation must live in Swift/Kotlin itself?
├─ Swift → SwiftUI embedding (/docs/internal/swiftui)
└─ Kotlin → Compose leg not built yet
Whichever tier you pick, you package it the same way, by publishing a crate. Consumers add one dependency line, and localization files and assets inside your crate aggregate into their app under your package’s namespace.