# mirui
[](https://crates.io/crates/mirui)
[](https://docs.rs/mirui)
[](LICENSE)
A `no_std`, ECS-driven UI framework for embedded, desktop, and (planned)
WebAssembly. Renders with 24.8 fixed-point subpixel precision on a
software rasterizer designed for MCUs without an FPU; optionally runs
on top of SDL2 (CPU or hardware-accelerated) on desktop.
## Features
- **ECS architecture** — entities, components, systems, resources, queries; system scheduler with named priority slots
- **`no_std` + `alloc`** — runs on bare-metal MCUs (ESP32-C3, STM32) with a global allocator
- **Subpixel rasterizer** — 24.8 fixed-point throughout (layout, rendering, hit-test, events). Scanline coverage AA on any `Path`; SDF / 2×2 supersample fast paths for quad fills
- **Vector drawing** — `Canvas` exposes `fill_path` / `stroke_path` / `draw_line` / `draw_arc`; `DrawCommand::FillPath` puts path fills inside the same View pipeline as built-in widgets
- **Layout** — Flexbox, absolute positioning, padding, justify / align; `Dimension::{Px, Percent, Auto, Content}`
- **Animation** — Tween, Spring (WWDC23-derived critical damping), retargetable; declarative `animate!` and `timer!` macros
- **Theme** — `ColorToken` / `ThemedColor`; built-in dark / light + custom tokens; per-`WidgetState` (Hovered / Pressed / Error / Disabled) overlay routing
- **Interaction states** — hover, press, error, disabled propagated through ECS markers; system-level dispatch
- **Multi-touch** — pinch / rotate gesture recognition from raw pointer streams; `SimAction` for scripted multi-touch in tests
- **Input feedback** — opt-in `InputFeedbackPlugin` paints a cursor dot and a magnetic-membrane water drop responding to rotary / wheel / click input
- **Dirty-flag partial refresh** — only re-renders changed regions; per-entity `Dirty` + `PrevRect` machinery
- **HiDPI** — automatic scale factor propagation
- **Plugins** — bundle clock, perf, input feedback into objects `App` drives through five lifecycle hooks
- **Pluggable backends** — SDL2 CPU, SDL2 GPU (hardware-accelerated), `FramebufSurface` (embedded RGB565 / ARGB8888 / RGB888 / RGB565Swapped), `compose_backend!` for routing primitives across multiple backends
- **Declarative DSL** — `ui!` macro for nested widget trees with attributes, enchants, walk loops, conditionals
## Quick Start
```toml
[dependencies]
mirui = "0.17"
mirui-macros = "0.17"
```
```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 :)
header (
bg_color: ColorToken::Primary,
text_color: ColorToken::OnPrimary,
height: 40, text: "Hello mirui!", border_radius: 8
) {}
body (grow: 1.0, bg_color: ColorToken::SurfaceVariant) {}
footer (height: 30, text: "ECS + DSL") {}
};
app.set_root(root);
app.run();
}
```
`mirui::prelude` brings `App`, layout types, `Color` / `Dimension` /
`Fixed`, `Entity` / `World`, `WidgetBuilder`, theme tokens, and the
`ui!` macro. Surface backends, plugins, and individual widget kinds
stay on their canonical paths so the prelude doesn't pin a platform
or feature choice.
## DSL Syntax
```rust
ui! {
:(parent: root, world: &mut world :)
// Widget with attributes
container (direction: FlexDirection::Column, grow: 1.0) {
header (text: "Header", height: 40) {}
body (grow: 1.0) {}
}
// Enchants — attach extra ECS components to the spawned entity
img (width: 16, height: 16, image: Image::new(&IMG_THUMBS_UP)) [
PhysicsBody { x: Fixed::ZERO, y: Fixed::ZERO },
Velocity { vx: Fixed::from_int(1), vy: Fixed::ZERO },
] {}
// Iteration
walk items.iter() with item {
row (text: item.name, bg_color: item.color) {}
}
// Conditional
if show_footer {
footer (text: "visible") {}
}
}
```
Powered by [xrune](https://github.com/W-Mai/xrune). Integer literals
in attributes (`height: 40`) coerce to `Fixed` / `Dimension` via `Into`.
### Common attributes
| `bg_color` / `text_color` / `border_color` | `Color` or `ColorToken` | Solid colour or theme token |
| `text` | `&str` | Text content |
| `border_radius` / `border_width` | `Fixed` | Subpixel-accurate |
| `width` / `height` | `Dimension` | `Px / Percent / Auto / Content` |
| `grow` | `f32` | Flex grow factor |
| `direction` | `FlexDirection` | Row / Column |
| `justify` / `align` | `JustifyContent` / `AlignItems` | Axis alignment |
| `padding` | `Padding` | Inner padding |
| `position` | `Position` | Flex / Absolute |
| `left` / `top` | `Dimension` | Absolute position |
| `image` | `Image` | Image component |
## Theme
Built-in widgets read colours through `ColorToken`s; switch palette
with `app.with_theme(Theme::light())`, swap at runtime with
`app.set_theme(...)`.
```rust
use mirui::widget::theme::{Theme, ColorToken};
let mut theme = Theme::dark();
theme.set(ColorToken::Custom("brand_accent"), Color::rgb(255, 105, 180));
app.with_theme(theme);
```
`WidgetState` (`Hovered` / `Pressed` / `Error` / `Disabled`) routes
overlays automatically: hover blends 8% `OnSurface`, press 12%, error
16% `Error`, disabled blends text/icon to 38% on `Surface` and
container roles to 12%. No widget needs to author per-state logic.
## Animation
```rust
use mirui::anim::{Spring, SpringConfig};
let mut spring = Spring::new(
SpringConfig::new(220, 0.3), // 220 ms perceptual duration, 30% bounce
Fixed::ZERO,
);
spring.target(Fixed::from_int(100));
// driven each frame by the animation system
```
`#[mirui::animate!(...)]` and `mirui::timer!(...)` macros declare
motion components that the framework's animation / timer systems
tick automatically.
## Plugins
Plugins package cross-cutting behaviour. Each plugin's docstring lists
what it inserts so reading `add_plugin(...)` is enough to know what
changes in `World`.
| `StdInstantClockPlugin` | resource: `MonoClock` (std-only) |
| `PerfReportPlugin` | resource: `PerfAccum`; hook: `post_render` |
| `FpsSummaryPlugin` | hook: `post_render` |
| `InputFeedbackPlugin` | resources: `InputFeedback`, `InputFeedbackInput`; systems: cursor + rotary feedback; views: cursor (pri 90), rotary (pri 91); entities: `OverlayCursor` (lazy), `OverlayRotary` (eager); hooks: `on_event`, `pre_render` |
Custom plugin:
```rust
use mirui::prelude::*;
use mirui::plugin::Plugin;
/// MyHotkeysPlugin — Esc quits.
///
/// **Inserts**
/// - resource: none
/// - system: none
/// - view: none
/// - entity: none
/// - hooks: on_event
struct MyHotkeysPlugin;
impl<B, F> Plugin<B, F> for MyHotkeysPlugin
where B: mirui::surface::Surface, F: mirui::app::RendererFactory<B>
{
fn build(&mut self, _app: &mut App<B, F>) {}
fn on_event(&mut self, _world: &mut World, event: &mirui::event::input::InputEvent) -> bool {
matches!(event, mirui::event::input::InputEvent::Key {
code: mirui::event::input::KEY_ESCAPE, pressed: true,
})
}
}
app.add_plugin(MyHotkeysPlugin);
```
## ScrollView
```rust
use mirui::event::scroll::{ScrollAxis, ScrollConfig, ScrollOffset};
ui! {
:(parent: root, world: &mut world :)
scroll_container (direction: FlexDirection::Column, grow: 1.0) [
ScrollOffset { x: Fixed::ZERO, y: Fixed::ZERO },
ScrollConfig {
direction: ScrollAxis::Vertical,
elastic: true,
content_height: Fixed::from_int(800),
content_width: Fixed::ZERO,
}
] {
walk items.iter() with item {
row (height: 60, bg_color: item.color, text: item.label) {}
}
}
};
```
Drag scrolling, inertia (spring-damped), elastic bounce, iOS-style
scroll chaining across nested scroll views, and per-axis content
clamping.
## Hybrid Backends — `compose_backend!`
Route different draw primitives to different backends, no runtime
dispatch:
```rust
use mirui_macros::compose_backend;
compose_backend! {
pub struct Hybrid {
sw: SwRenderer,
gpu: MyGpuBackend,
}
route {
default => sw,
blit => gpu,
clear => gpu,
}
}
```
Generated `Hybrid<__B0, __B1>` is generic over each backend's lifetime
parameters. See `gallery/examples/compose_backend_demo.rs` and
`compose_backend_dsl.rs`.
## ECS
```rust
// Spawn
let e = world.spawn();
world.insert(e, MyComponent { ... });
// Query
let mut buf = Vec::new();
world.query::<PhysicsBody>().and::<Velocity>().collect_into(&mut buf);
// Resources
world.insert_resource(GameSeed(42));
let seed = world.resource::<GameSeed>().unwrap().0;
// Systems
#[mirui::system(order = SystemSlot::Animation)]
fn physics_system(world: &mut World) { /* ... */ }
app.add_system(physics_system::system());
```
`SystemSlot` enum names the standard scheduling positions
(`SimInput / DeltaTime / InteractionState / Animation / Timer /
ScrollInertia / LazyList / TabPages / Normal`). Lower values run
earlier; user systems default to `Normal`.
## Performance
ESP32-C3 (RV32 160 MHz, no FPU) + ST7735S 128×128 SPI:
| Three-body (widgets + dirty rect) | ~14 ms | ~70 | Default `quad-aa` off; partial refresh |
| Cover-flow (3D quad transforms) | ~52 ms | ~19 | `default-features = false` |
`App::run` writes a per-stage `FrameTimings` resource each frame
(input / systems / layout / render / flush / seed_prev) and pushes
`frame_nanos` into a 256-sample `FrameStats` ring for jitter / p99
analysis. `FpsSummaryPlugin` averages and prints the breakdown,
`BudgetReportPlugin` warns when avg or p99 cross a configured
threshold.
### Drilling into spans
Wrap any code with `mirui::trace_span!("name")` or annotate a fn
with `#[mirui::trace_fn("name")]`. With a clock plugin installed
(`StdInstantClockPlugin` on desktop, a custom one calling
`mirui::perf::set_clock` on bare metal), every invocation records
into a ring buffer that `mirui::perf::drain_events()` returns.
`mirui::perf::format_chrome_event` writes one event as Chrome
trace JSON for [Perfetto](https://ui.perfetto.dev). On `std`
`PerfReportPlugin::with_perfetto_writer` dumps the stream to a
file. On ESP, the bundled `mirui-examples/examples/esp32c3-animation`
demo prints `[trace] {...}` lines through `esp_println`; the
host-side `tools/esp-trace.py` script collects them into a
Perfetto-loadable JSON file.
## Hardware Examples
[mirui-examples](https://github.com/W-Mai/mirui-examples) hosts the
ESP32-C3 demos:
- `demo-threebody` (default) — three gravitating bodies
- `demo-particles` — pulse rings, bouncing bars, particles
- `demo-subpixel` — bars moving by 1 px vs 0.1 px (subpixel AA)
- `demo-shapes` — clock face via `draw_line` / `draw_arc`
- `demo-butterfly` — flapping vector butterfly
- `demo-coverflow` — cover flow with 3D quad transforms
- `demo-flipcard`, `demo-gesture`, `demo-widgets` — additional showcases
- `demo-hidpi-downscale` / `demo-hidpi-upscale` — HiDPI mode toggles
Flash with `cargo run --release --features demo-XXX --no-default-features`.
## License
MIT