mirui
A lightweight, no_std ECS-driven UI framework for embedded, desktop, and WebAssembly. Renders with 24.8 fixed-point subpixel precision on a software rasterizer designed for MCUs without an FPU.
Features
- ECS architecture — entities, components, systems, resources, queries
no_std+alloc— runs on bare-metal MCUs (ESP32-C3, STM32) with a global allocator- Subpixel rasterizer — 24.8 fixed-point throughout the pipeline (layout, rendering, events). Scanline coverage anti-aliasing on any
Path - Vector drawing API —
fill_path/stroke_path/draw_line/draw_arconDrawBackend, cubic Bezier accurate circles - Declarative DSL —
ui!macro powered by xrune - Flexbox + absolute positioning — familiar layout model with
Dimension::{Px, Percent, Auto, Content} - HiDPI — automatic scale factor propagation
- Dirty-flag partial refresh — only re-renders changed regions
- ScrollView — inertia, elastic bounce, scroll chaining, spring resistance
- Widgets — Button, Checkbox, ProgressBar, Image, ScrollView
- Pluggable backends — SDL2 (desktop), FramebufBackend (embedded RGB565 / ARGB8888 / RGB888 / RGB565Swapped)
Quick Start
[]
= "0.4"
= "0.4"
use App;
use SdlBackend;
use *;
use ;
use WidgetBuilder;
use ui;
DSL Syntax
ui!
Supported Attributes
| Attribute | Type | Description |
|---|---|---|
bg_color |
Color |
Background color |
text |
&str |
Text content |
text_color |
Color |
Text color |
border_radius |
Fixed |
Corner radius (subpixel) |
border_color |
Color |
Border color |
width / height |
Dimension |
Px / Percent / Auto / Content |
grow |
f32 |
Flex grow factor |
direction |
FlexDirection |
Row / Column |
justify |
JustifyContent |
Main axis alignment |
align |
AlignItems |
Cross axis alignment |
padding |
Padding |
Inner padding |
position |
Position |
Flex / Absolute |
left / top |
Dimension |
Absolute position |
image |
Image |
Image component |
Integer literals passed in the DSL (e.g. height: 40, border_radius: 8) are coerced to Fixed or Dimension via Into.
Vector Drawing (0.3+)
DrawBackend is a full 2D rendering surface. Every primitive — solid rects, borders, text, blits, arbitrary paths — goes through it.
use DrawBackend;
use Path;
use ;
// Inside any code holding a `&mut impl DrawBackend`:
// Stroked line
backend.draw_line;
// Stroked arc (degrees, counter-clockwise from +X axis)
backend.draw_arc;
// Filled custom path
let mut path = new;
path.move_to;
path.cubic_to;
path.close;
backend.fill_path;
Paths are flattened via De Casteljau (8 segments per quadratic, 16 per cubic) then rasterized with a 4-sub-scanline coverage integration into the target texture. No allocation per pixel; per-edge sqrt only for strokes falling inside the AA ramp.
Hybrid Backends — compose_backend! (0.3.1+)
Want the path rasterizer on software but blit and clear on a GPU fast path? Declare a hybrid struct and a route table; the compose_backend! proc-macro emits the full DrawBackend + Renderer impls statically — no runtime dispatch.
use compose_backend;
compose_backend!
Generated:
Hybrid is generic over one type parameter per field, so backends carrying lifetimes (SwDrawBackend<'fb>) flow through without needing the struct itself to declare any.
Plugging into App
App takes a second generic F: RendererFactory that defaults to SwDrawBackendFactory (so every existing App::new(backend) call keeps working). To use a hybrid backend in the normal run loop:
let mut app = with_factory;
app.run;
Error messages are reasonable — unknown method names and unknown field names in route { ... } come with Levenshtein "did you mean" suggestions.
See examples/compose_backend_demo.rs (direct API) and examples/compose_backend_dsl.rs (ECS + ui! + App::with_factory).
Plugins (0.4+)
Bundle cross-cutting behaviour — monotonic clock, FPS summary, logging, hotkeys — into objects App drives through five lifecycle hooks:
use Plugin;
use ;
app.add_plugin
.add_plugin
.add_system;
app.run;
Plugin trait has one required method (build) and four optional hooks:
| Hook | When |
|---|---|
build(&mut self, app) |
Once at add_plugin — register systems, insert resources, swap app.clock |
pre_render(world) |
Before each render / render_dirty |
post_render(world, render_nanos) |
After each render, with the measured duration |
on_event(world, event) -> bool |
For every input event before widget dispatch; true consumes it |
on_quit(world) |
Right before App::run returns |
Any FnMut(&mut App<B, F>) is a plugin via a blanket impl, so simple setup can be a closure:
app.add_plugin;
Built-in plugins
StdInstantClockPlugin(feature = "std") — swapsapp.clockto astd::time::Instant-backed monotonic clock. Without a clock plugin installed,post_rendersees0every frame and timing-oriented plugins no-op.FpsSummaryPlugin— accumulatesrender_nanosover a configurable frame bucket and prints an average. UseFpsSummaryPlugin::new(count).with_sink(my_sink)to route the output somewhere other than stderr (an LCD overlay, a UART log).
On bare metal an application normally writes its own clock plugin (e.g. an esp_hal systimer reader) and points the existing FpsSummaryPlugin at esp_println through with_sink.
Event consumption
on_event returning true stops further widget dispatch for that event — use it for global hotkeys:
ECS
// Spawn entities
let e = world.spawn;
world.insert;
// Query
let mut buf = Vecnew;
world....collect_into;
for e in &buf
// Resources (global singletons)
world.insert_resource;
let dt = world..unwrap.0;
// Systems
app.add_system;
app.systems.add_fn;
ScrollView
ui! ;
Features: drag scrolling, inertia, elastic bounce with spring resistance, iOS-style scroll chaining across nested scroll views.
Performance
ESP32-C3 (RISC-V 160 MHz, no FPU) + ST7735S 128×128 SPI display, RGB565:
| Demo | FPS | Notes |
|---|---|---|
| Three-body (widgets + dirty rect) | 160 | border_radius: 3 anti-aliasing enabled |
Shapes (clock face, raw DrawBackend) |
32-35 | 1 circle + 12 tick lines + sweeping hand per frame |
Butterfly (vector, raw DrawBackend) |
30-32 | 8 fill_path + 3 draw_line per frame; Lissajous flight + yaw rotation |
Binary size: mirui + a typical ESP32 app + esp-hal around 120 KB .text for the vector demos.
Hardware Examples
mirui-examples has the ESP32-C3 demos above:
demo-threebody(default) — three gravitating bodies rendered with widgets + dirty rect refreshdemo-particles— pulse rings, bouncing bars, floating particlesdemo-subpixel— two bars moving by 1 px vs 0.1 px, showcasing subpixel AAdemo-shapes— clock face drawn viadraw_line/draw_arcdemo-butterfly— flapping, flying, yaw-rotating vector butterfly
Flash with cargo run --release --features demo-butterfly --no-default-features (or any other demo-* feature).
License
MIT