mirui 0.25.2

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

How to go from zero to a running mirui application — desktop, embedded,
or sharing UI code across both.

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. [Skip the boilerplate with `cargo-generate`]#skip-the-boilerplate
6. [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.25", features = ["sdl"] }
```

`src/main.rs`:

```rust
use mirui::prelude::*;
use mirui::surface::sdl::SdlSurface;

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 = WidgetBuilder::new(&mut app.world)
        .bg_color(ColorToken::Surface)
        .layout(LayoutStyle {
            direction: FlexDirection::Column,
            width: Dimension::px(480),
            height: Dimension::px(320),
            ..Default::default()
        })
        .id();

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

        column (direction: FlexDirection::Column, grow: 1.0) {
            header (
                bg_color: ColorToken::Primary,
                text_color: ColorToken::OnPrimary,
                height: 40,
                text: "Hello mirui!",
                border_radius: 8
            ) {}
            content (bg_color: ColorToken::SurfaceVariant, grow: 1.0) {}
            footer (height: 30, text: "ECS + DSL") {}
        }
    };

    app.set_root(root);
    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"
```

## 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::draw::texture::ColorFormat::RGB565Swapped,  // or RGB565
    |bytes: &[u8], area: &Rect| {
        // 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();
// build the ui! tree, set_root, 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.25", 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
        :)

        root (bg_color: ColorToken::Surface) {
            hello (
                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.

## 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
```

Each template asks for the project name and the mirui version, fills
the Cargo manifests and source files in, and leaves you with a project
that builds on the first `cargo build`.

A `wasm` template is published alongside the others; it pins a future
`web-canvas` Surface backend that has not landed yet, so it is a
placeholder and will not build until that backend ships.

## Where to go next

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

- **Add your own widget** — see the widget cookbook (planned for the
  1.0 cycle) for the rendering, theme integration, and animation
  contracts the built-in widgets follow.
- **Drive your own LCD or touch IC** — the surface cookbook (planned)
  walks through ST7789, GC9A01, FT6236, GT911, and the
  `Surface` trait that ties them to mirui.
- **React to state changes declaratively** — the state-management
  story (planned for 1.0) layers `Signal<T>` / `Computed<T>` /
  `Effect` over the existing ECS World.
- **Persist user state across runs** — the lifecycle plugin (planned
  for 1.0) ships `PersistencePlugin` plus pause / resume hooks for
  embedded power management.

Until those land, the working examples in
[`gallery/examples/`](../gallery/examples/) and
[`mirui-examples`](https://github.com/W-Mai/mirui-examples) are the
most complete reference.