mirui
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 —
Canvasexposesfill_path/stroke_path/draw_line/draw_arc;DrawCommand::FillPathputs 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!andtimer!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;
SimActionfor scripted multi-touch in tests - Input feedback — opt-in
InputFeedbackPluginpaints 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+PrevRectmachinery - HiDPI — automatic scale factor propagation
- Plugins — bundle clock, perf, input feedback into objects
Appdrives 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
[]
= "0.22"
= "0.22"
use *;
use SdlSurface;
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
ui!
Powered by xrune. Integer literals
in attributes (height: 40) coerce to Fixed / Dimension via Into.
Common attributes
| Attribute | Type | Description |
|---|---|---|
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 ColorTokens; switch palette
with app.with_theme(Theme::light()), swap at runtime with
app.set_theme(...).
use ;
let mut theme = dark;
theme.set;
app.with_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
use ;
let mut spring = new;
spring.target;
// 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.
| Plugin | Inserts |
|---|---|
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:
use *;
use Plugin;
/// MyHotkeysPlugin — Esc quits.
///
/// **Inserts**
/// - resource: none
/// - system: none
/// - view: none
/// - entity: none
/// - hooks: on_event
;
app.add_plugin;
ScrollView
use ;
ui! ;
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:
use compose_backend;
compose_backend!
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
// Spawn
let e = world.spawn;
world.insert;
// Query
let mut buf = Vecnew;
world...collect_into;
// Resources
world.insert_resource;
let seed = world..unwrap.0;
// Systems
app.add_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:
| Demo | frame avg | FPS | Notes |
|---|---|---|---|
| Three-body (widgets + dirty rect) | ~13 ms | ~77 | 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. 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 hosts the ESP32-C3 demos:
demo-threebody(default) — three gravitating bodiesdemo-particles— pulse rings, bouncing bars, particlesdemo-subpixel— bars moving by 1 px vs 0.1 px (subpixel AA)demo-shapes— clock face viadraw_line/draw_arcdemo-butterfly— flapping vector butterflydemo-coverflow— cover flow with 3D quad transformsdemo-flipcard,demo-gesture,demo-widgets— additional showcasesdemo-hidpi-downscale/demo-hidpi-upscale— HiDPI mode toggles
Flash with cargo run --release --features demo-XXX --no-default-features.
License
MIT