How it works

From a .rux file to pixels on the GPU: the pipeline, the crates it leans on, and the parts that are honestly still crude.

Rux is a pipeline, not a framework. A .rux file goes in one end, a native window comes out the other, and every stage is a crate you can read in an afternoon. The whole runtime is about 7,000 lines.

The pipeline

  app.rux


┌──────────────┐   <template> / <style> / <script>
│  rux-parser  │   hand-rolled SFC split + XML-ish template parser
└──────┬───────┘

┌──────────────┐   lightningcss parses the CSS; our cascade resolves it onto
│  rux-style   │   nodes. Directives (r-for/r-if/r-model), {{ bindings }},
└──────┬───────┘   and component expansion happen here → a styled node tree

┌──────────────┐   Style → taffy (flex / grid / block) → absolute paint items,
│  rux-layout  │   plus hit, focus and scroll regions
└──────┬───────┘

┌──────────────┐   paint items → a vello Scene: fills, gradients, shadows,
│  rux-paint   │   borders, clips, transforms, images, glyph runs
└──────┬───────┘

┌──────────────┐   winit window + wgpu surface; vello renders; input, focus,
│  rux-shell   │   selection, scrolling, clipboard, and the file watcher
└──────────────┘

Two crates sit alongside rather than in the flow: rux-script (the rhai engine holding state and running @tap handlers) and rux-text (parley shaping, measuring and drawing text). rux-runtime owns the Document that ties it together — load, resolve imports, build the engine, rebuild the tree.

CrateJob
rux-parserSFC split + template parser (ours)
rux-stylelightningcss → our cascade → Style; directives; components
rux-scriptrhai engine (state + handlers) + host:: registry
rux-layoutStyle → taffy → paint items, hit / focus / scroll regions
rux-textparley shaping, measure, wrapping; vello glyph drawing
rux-paintpaint items → vello scene
rux-runtimeDocument: load, imports, engine, rebuild
rux-shellwindow, GPU, input, focus, clipboard, watcher
rux-clirux [file.rux]

Layout is CSS, not a lookalike

rux-style runs real lightningcss over your <style> block, then applies its own cascade: specificity, source order, and inheritance for the properties that inherit (color, font-size, font-family). The result is a Style struct per node, which rux-layout maps onto taffy — the same layout engine Dioxus and Bevy use.

That mapping is the honest boundary of "it's just CSS." Taffy implements flexbox, CSS grid and block layout properly, so those behave the way you expect, traps included:

Those aren't Rux quirks — that's CSS. What taffy has no notion of is inline text flow: there's no inline formatting context, so text can't wrap around a floating image and display: inline isn't real. Everything defaults to display: block, and you reach for display: flex to lay things out.

Text hugs its box

Text is parley end to end: shaping, line breaking, cursor geometry, font fallback. One deliberate deviation is worth knowing, because it shows up everywhere:

Rux trims leading. A line box is ascent + descent, not the font's full line height, and the baseline sits at top + ascent. Text therefore hugs its box, and padding reads equally on all four sides — which is what you want when you're styling with CSS. Set line-height and you get the normal behaviour back.

The cost is that anything positioning itself against drawn glyphs must use our line stepping, not parley's. That's why selection_rects takes only the horizontal extent from parley's selection geometry and recomputes y itself: take parley's rects wholesale and the highlight drifts further off the glyphs with every wrapped line.

Painting

rux-layout doesn't produce a widget tree — it produces a flat, ordered list of paint items: Rect, Text, Image, Shadow, Tick, and the bracket pairs PushClip/PopClip, PushTransform/PopTransform, PushOpacity/PopOpacity. rux-paint walks that list into a vello Scene, keeping a transform stack so a transformed element carries its subtree with it.

Vello renders the scene with compute shaders on the GPU via wgpu. Rux is event-driven: it paints in response to a redraw request — resize, tap, keystroke, reload — not on a frame clock. An idle Rux window uses no CPU. The one exception is the caret, which blinks on a 530 ms timer while an input is focused, and stops the timer the moment it isn't.

State, and the rebuild

State lives in rhai signals:

<script>
  let n = signal(0);
</script>

{{ n }} reads it. @tap="n = n + 1" writes it. r-model="name" binds an input two ways. Handlers are interpreted, so they hot-reload; heavy work goes to compiled Rust through a host:: registry, which needs a rebuild. That split is deliberate: script is glue, Rust is the engine.

Here is the crude part, stated plainly: any signal change rebuilds the entire tree. Not a diff, not a subscription — the whole thing, then a fresh layout.

At these sizes it's imperceptible, and it's simple to reason about. But it means every piece of ephemeral UI state — the caret, the selection, scroll offsets, which dropdown is open, where keyboard focus is — is thrown away on every keystroke and has to be put back by hand afterwards. There's a named restore pass for each one. This has already produced one real bug (the caret stayed visible in the input you'd just left, because the restore pass only ever set a caret and never cleared one), which is why the roadmap keeps a running list of every restore pass that exists.

Fine-grained, per-binding reactivity deletes that whole category. It's the next significant piece of work, and the last real divergence between the architecture doc and the code.

Hot reload

A notify watcher watches the file's directory recursively — so imported components reload too — and wakes the event loop through an EventLoopProxy on any .rux change. The Document reloads, rebuilds, repaints. If the new file fails to parse, the last good document stays on screen and the error goes to stderr rather than blanking the window.

That's a stand-in for a proper dev overlay, and it points at a real ceiling: Rux's error surfacing is poor. Unknown CSS is ignored (it now warns once per property), a bad file gives you stderr, and there's no in-window diagnostic. It's on the list.

Why compose instead of build

Every heavy part of Rux is someone else's crate — taffy, parley, vello, wgpu, winit, lightningcss, rhai. That's Law 4, and it's why a UI language with a GPU renderer fits in ~7,000 lines. It also means Rux inherits their bugs and their release cadences, which is a trade worth making: those crates are maintained by people who care about layout, text and rasterisation more than this project ever could.