mirui 0.46.3

A lightweight, no_std ECS-driven UI framework for embedded, mobile, desktop, and WebAssembly
Documentation
# Quickstart

How to go from zero to a running mirui application on desktop, mobile, WebAssembly, or embedded hardware while sharing the same UI model.

This guide is the long-form companion to the README and the docs.rs
crate-level introduction. It expects familiarity with `cargo` and
basic Rust, plus an SPI datasheet reading habit if you target an MCU.

## Contents

1. [Toolchain prerequisites](#toolchain-prerequisites)
2. [Desktop SDL — five minutes from zero](#desktop-sdl)
3. [ESP32-C3 embedded — fifteen minutes from zero](#esp32-c3-embedded)
4. [Cargo workspace — share UI code across multiple targets](#cargo-workspace)
5. [Android and iOS](#android-and-ios)
6. [Skip the boilerplate with `cargo-generate`](#skip-the-boilerplate)
7. [Where to go next](#where-to-go-next)

## Toolchain prerequisites

Stable Rust (1.85 or newer) is enough for the desktop path:

```bash
rustup default stable
rustup update
```

The ESP32-C3 path also wants a `riscv32imc` target and the `espflash`
flasher. Both work on stable:

```bash
rustup target add riscv32imc-unknown-none-elf
cargo install espflash
```

For the workspace template that mixes desktop and embedded crates, the
above two are enough — no nightly required.

## Desktop SDL

The SDL backend is the fastest way to see mirui render anything. SDL2 is
linked dynamically; install it from your package manager:

```bash
brew install sdl2          # macOS
apt-get install libsdl2-dev   # Debian / Ubuntu
```

Create a fresh project:

```bash
cargo new hello-mirui
cd hello-mirui
```

`Cargo.toml`:

```toml
[package]
name = "hello-mirui"
version = "0.1.0"
edition = "2024"

[dependencies]
mirui = { version = "0.46", features = ["sdl"] }
```

`src/main.rs`:

```rust
use mirui::prelude::*;
use mirui::surface::sdl::SdlSurface;
use mirui::ui::widgets::{ParagraphStyle, Text};

fn main() {
    let backend = SdlSurface::new("hello mirui", 480, 320);
    let mut app = App::new(backend);
    app.with_default_widgets().with_default_systems();

    let root = app.spawn_root().id();

    ui! {
        :(
            parent: root
            world: &mut app.world
        :)

        Column (grow: 1.0) {
            View (
                bg_color: ColorToken::Primary,
                text_color: ColorToken::OnPrimary,
                height: 40,
                border_radius: 8,
                padding: Padding::all(10)
            ) {
                Text ("Hello mirui!")
            }
            View (bg_color: ColorToken::SurfaceVariant, grow: 1.0)
            Text ("ECS + DSL", height: 30, paragraph: ParagraphStyle::label())
        }
    };

    app.run();
}
```

Run it:

```bash
cargo run
```

A 480x320 window appears with a blue header, a darker content area, and
a footer strip — three widgets stacked inside a column container.

If `cargo run` fails to find `libSDL2.dylib` on macOS (Apple Silicon),
add the Homebrew lib path:

```bash
export LIBRARY_PATH="/opt/homebrew/lib:$LIBRARY_PATH"
```

### Optional audio

Use `sdl-audio` on desktop or `web-audio` in the browser. Both features install the same fixed-capacity command resource through `AudioPlugin`; bare-metal applications can fill a caller-owned PCM buffer with `AudioMixer` or implement `AudioSink` for an I2S/DMA driver.

```rust
use mirui::app::plugins::AudioPlugin;
use mirui::audio::{AudioBank, AudioBus, AudioTone, SdlAudioSink, Waveform};

static AUDIO: AudioBank = AudioBank::new(&[]);

app.add_plugin(AudioPlugin::new(SdlAudioSink::new(), &AUDIO));

if let Some(audio) = app.world.resource_mut::<AudioBus>() {
    audio.tone(AudioTone::new(72, Waveform::Sine, 240, 190));
}
```

## ESP32-C3 embedded

The embedded path takes a hardware kit, an SPI display, and a USB cable.
The shape of an mirui ESP project is small — `no_std` `main`, esp-hal
peripherals, and a `FramebufSurface` whose flush closure speaks SPI to
the panel — but the BSP wiring (clock setup, SPI mode + DMA buffers,
ST7735/ST7789/GC9A01 init sequence, panel reset) is dozens of lines and
moves with each esp-hal release.

Rather than reproduce that wiring here and let it bit-rot, the
canonical reference is the
[mirui-examples](https://github.com/W-Mai/mirui-examples) project, which
this guide tracks. Start there:

```bash
git clone https://github.com/W-Mai/mirui-examples
cd mirui-examples/examples/esp32c3-animation
cargo build --release --no-default-features --features=demo-threebody
```

The `esp32c3-animation` crate's `src/board.rs` carries the SPI + ST7735
driver, `src/main.rs` ties it into `App::new(FramebufSurface::…)`, and
`Cargo.toml` pins compatible esp-hal / esp-alloc / esp-bootloader-esp-idf
versions. Copy the project, point `board.rs` at your own panel's
pinout, and replace the demo widgets with your own `ui!` tree.

Once it builds, flash with:

```bash
espflash flash --monitor target/riscv32imc-unknown-none-elf/release/mirui-esp32c3
```

The piece that's universal across boards is the mirui side:

```rust
let backend = FramebufSurface::with_format(
    W, H,
    mirui::render::texture::ColorFormat::RGB565Swapped,  // or RGB565
    |bytes: &[u8], area: mirui::types::PhysicalRect| {
        // Push `bytes` to your LCD over SPI for the window described by `area`.
    },
);

let mut app = App::new(backend);
app.with_default_widgets().with_default_systems();
// app.spawn_root(), build the ui! tree, app.run() — same as the desktop hello.
```

`ColorFormat::RGB565Swapped` is the byte order most ST7735/ST7789 panels
expect when the host MCU is little-endian; use `RGB565` if your panel
takes the bytes the other way around. mirui's
[Surface trait docs](../src/surface/mod.rs) cover the rest of the
contract — `display_info`, `flush`, `poll_event`, persistence — and
which to override for a custom backend.

## Cargo workspace

When the same UI code should drive both a desktop window and an MCU
panel, put the UI in a shared library crate and let one binary crate
per target consume it. mirui ships a Cargo workspace template that
sets this up; you can also build it by hand.

Layout:

```
my-app/
├── Cargo.toml             # [workspace] members = ["app", "targets/*"]
├── app/                   # shared UI library, std/no_std dual
│   ├── Cargo.toml
│   └── src/lib.rs
└── targets/               # one crate per target, glob-matched
    ├── desktop/           # SDL bin
    │   ├── Cargo.toml
    │   └── src/main.rs
    └── esp32c3/           # ESP32-C3 bin
        ├── Cargo.toml
        ├── .cargo/config.toml
        ├── rust-toolchain.toml
        └── src/main.rs
```

Root `Cargo.toml`:

```toml
[workspace]
resolver = "2"
members = ["app", "targets/*"]
```

The `targets/*` glob means a new target crate dropped into
`targets/<name>/` is picked up without editing the workspace manifest.

`app/Cargo.toml`:

```toml
[package]
name = "app"
version = "0.1.0"
edition = "2024"

[features]
default = []
std = ["mirui/std"]

[dependencies]
mirui = { version = "0.46", default-features = false, features = ["quad-aa"] }
```

`app/src/lib.rs`:

```rust
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;

use mirui::prelude::*;
use mirui::ecs::{Entity, World};

pub fn build_ui(world: &mut World, parent: Entity) -> Entity {
    ui! {
        :(
            parent: parent
            world: world
        :)

        View (bg_color: ColorToken::Surface) {
            Text ("Hello mirui!", text_color: ColorToken::OnSurface)
        }
    }
}
```

`targets/desktop/Cargo.toml` enables `sdl` on mirui and `std` on `app`;
`targets/esp32c3/Cargo.toml` keeps both at `default-features = false`
and adds the esp-hal stack. Each target's `main.rs` opens a Surface,
constructs `App`, and calls `app::build_ui` to populate the tree.

### Adding a new target

The workspace is meant to grow. To add ESP32-S3, RP2040, STM32, or any
other MCU:

1. Copy an existing target as a starting point: `cp -r targets/esp32c3 targets/esp32s3`.
2. Update the new crate's `Cargo.toml` with the right `[package].name`
   and BSP dependencies.
3. Update `.cargo/config.toml` and `rust-toolchain.toml` for the new
   target triple and linker script.
4. Adjust `src/main.rs` to talk to the new chip's clocks, SPI, and
   panel.

Build it with `cargo build -p esp32s3`. Workspace membership is
automatic — the glob picks the new directory up on the next `cargo`
invocation.

## Android and iOS

The `android` and `ios` generator templates ask for a rendering path. `wgpu` submits mirui draw commands directly to the platform WGPU surface. `sw` retains one caller-budgeted RGBA framebuffer, rasterizes with the software backend, uploads dirty regions, and uses WGPU only for presentation. Both paths keep the `App`, ECS world, and reactive state alive across native suspend and resume while recreating the platform surface.

```bash
cargo generate W-Mai/mirui-templates android --name hello-mirui-android
cargo generate W-Mai/mirui-templates ios --name hello-mirui-ios
```

Android uses a NativeActivity entry point and builds for `aarch64-linux-android` through `cargo-apk`. iOS generates a Rust static library plus a minimal Xcode application host for simulator and device targets. The generated READMEs contain the required Rust targets and launch commands.

Mobile applications retain their world and renderer state across suspend and resume. Touch contacts are mapped to stable compact pointer IDs for their full lifetime, and a focused `TextInput` opens the native software keyboard while preserving UTF-8 text delivered by native input events. Custom editors can opt into the same behavior by attaching both `Focusable` and `TextEditable`.

Software-rendered hosts use native device density within an explicit framebuffer budget. Resize admission is transactional: an allocation or budget failure leaves the last valid framebuffer active. WGPU acquisition recovers once from outdated or lost surfaces and skips transient timeout or occlusion frames. Native memory warnings release reconstructible text, path-placement, offscreen, software-raster, and GPU caches while preserving application state; plugins can release their own caches through `Plugin::on_memory_warning`.

`app.spawn_root()` keeps children inside the current safe area by default; fullscreen content can opt out with `app.spawn_root().ignore_safe_area().id()`. iOS renders against the full drawable while publishing logical safe-area insets separately, so edge-to-edge backgrounds and inset content share one coordinate space.

## Skip the boilerplate

The same templates this guide walks through by hand are published as a
[`cargo-generate`](https://github.com/cargo-generate/cargo-generate)
template repository. After installing the tool:

```bash
cargo install cargo-generate
```

Generate a project from any of the templates:

```bash
# Single-target SDL
cargo generate W-Mai/mirui-templates sdl-only --name hello-mirui

# Single-target ESP32-C3
cargo generate W-Mai/mirui-templates esp32c3 --name hello-mirui-esp32c3

# Multi-target Cargo workspace (app + targets/desktop + targets/esp32c3)
cargo generate W-Mai/mirui-templates workspace --name my-app

# Browser Canvas 2D
cargo generate W-Mai/mirui-templates wasm --name hello-mirui-web

# Android NativeActivity; choose WGPU or software rendering when prompted
cargo generate W-Mai/mirui-templates android --name hello-mirui-android

# iPhone and iPad Xcode project; choose WGPU or software rendering when prompted
cargo generate W-Mai/mirui-templates ios --name hello-mirui-ios
```

Each template asks for the project name and the mirui version, fills the Cargo manifests and source files, and produces a buildable project. The `wasm` template uses the shipped `web-canvas` Surface and runs through trunk.

## Where to go next

You have a running mirui app. The next steps depend on what you want to build:

- **Add your own widget** — inspect the built-in widgets and Gallery compositions for rendering, theme integration, typed properties, and animation contracts.
- **Drive your own LCD or touch IC** — implement the `Surface` boundary and use the ESP32-C3 board integration in [`mirui-examples`](https://github.com/W-Mai/mirui-examples) as a complete framebuffer and input reference.
- **React to state changes declaratively** — [`state-management.md`](state-management.md) covers `Signal<T>`, `Computed<T>`, `Effect`, DSL bindings, and lifecycle-safe disposal.
- **Persist user state across runs** — the `persistence_counter` Gallery demo connects `PersistencePlugin` to lifecycle pause and resume hooks.

The working examples in [`gallery/examples/`](../gallery/examples/) and [`mirui-examples`](https://github.com/W-Mai/mirui-examples) exercise the same public APIs across desktop, browser, and embedded targets.