Rux v0.3.0: fine-grained reactivity, dynamic styles, and syntax coloring
A signal change now patches only the bindings that read it, in place, instead of rebuilding the tree, which deletes the restore-pass category that caused v0.1's worst bug. Plus Vue-style :class / :style bindings and .rux syntax highlighting for VS Code and the site.
v0.2 closed with a promise: every feature it shipped needed its own restore
pass: put the caret back, put the selection back, put the scroll offsets back,
remember the open dropdown, remember focus. Per-binding subscriptions would
delete the whole category. v0.3 is that deletion, plus two things that ride on top
of it: dynamic :class / :style bindings, and real syntax coloring for .rux.
95 tests pass, and every claim below was driven in the window before it was called
done.
Fine-grained reactivity
Through v0.2, any signal write rebuilt the entire tree and then restored the ephemeral state the rebuild threw away. It worked, but every new feature added one more thing to restore, and every restore pass was a chance to reproduce v0.1's nastiest bug (the caret that wouldn't leave the input you'd just left).
v0.3 replaces the rebuild with subscriptions. A binding records exactly the signals it reads, a write records exactly the signals it changed, and reflecting a change touches only the bindings that intersect. The mechanism, end to end:
- Reads are tracked in
rux-scriptthrough rhai'son_varhook, filtered down to the top-level signal names. Writes are caught by snapshotting the signals around a handler and diffing. Neither needs any cooperation from the script, so signals stay plain values: no.get()/.set(), no wrapper type. - Display-only changes patch in place. Text
{{ }}, input values,r-showvisibility, and:src/:optionsattributes are rewritten on their existing nodes. No rebuild, so the caret, selection, and scroll offsets are never disturbed in the first place, so there is nothing to restore. - Structural changes reconcile in place.
r-if/r-for, checkbox / radio toggles, and component props splice just their own subtree: build the new fragment, swap it in, restore focus scoped to that subtree alone.
The restore-pass category is gone. Set RUX_TRACE=1 and every interaction prints
the path it took, either patched in place (no rebuild) or rebuilt (structural), and
across the examples the wholesale rebuild simply doesn't fire anymore.
RUX_TRACE=1 cargo run -p rux-cli -- examples/form.rux
One deliberate non-deletion worth noting: apply_focus stays. It's still the
mechanism that sets a caret and that scopes focus restoration after a reconcile.
What's retired is its old job of restoring the whole tree on every keystroke.
:class and :style
With patching in place, dynamic attributes became cheap to add. Rux now has Vue-style bindings:
<view class="chip" r-for="c in colors" :class="c" :style="`background: ${c}`">
<text class="chiplabel">{{ c }}</text>
</view>
:classmerges its result into the classes that drive CSS matching, so a bound class cascades exactly like a written one. String, array, and object / conditional forms are all accepted (:class="{ active: isOpen }").:styleevaluates to a declaration string and overlays it on the matched rules, so inline wins, the same precedence as CSS.- String interpolation in an attribute value is free:
:-attributes are rhai expressions, and rhai already does backtick template literals, so:style="`background: ${c}`"needs no template-layer code at all.
The one wrinkle, documented rather than hidden: a whole-number signal renders
through rhai's float default inside a backtick string (82.0, not 82). It's
valid CSS, but reach for ${n.to_int()} when you want the integer.
Syntax coloring, and editor tooling
.rux files now get highlighted, from a single self-contained TextMate grammar
that serves two consumers: VS Code, and the code blocks on this site (the same
rux.tmLanguage.json, copied into the Zola build). One grammar, no fork, no second
source of truth to drift.
The VS Code extension picked up the rest of the basics:
- Snippets for the common shapes (
screen,view,r-for, an input bound to a signal). - Section folding for
<template>/<style>/<script>. - A basic Format Document (
Shift+Alt+F) that re-indents the tag tree. - A
.ruxfile icon, and a prebuilt.vsixin the repo.
Labels
A <text for="…"> label is now wired to its target: tapping the label focuses a
bound text input, or fires a toggle target's @tap. The role="label"
accessibility half is still unbuilt.
Still missing
As plainly as the wins:
- Reactivity:
r-forreconciles by count, not by key, so reordering a list rebuilds more rows than a keyed diff would. Effects and computed values are not a separate concept yet; a{{ }}expression is the only "computed". - Script: no first-class function that mutates a signal. Only a statement in the top scope mutates state, so a reusable handler has to be inlined, not called. This is a known constraint of stock rhai and a motivator for the planned fork.
- CSS: still no variables /
var(),@media, or pseudo-classes. The syntheticcheckedclass remains a stopgap.
Next
The v0.3 banner is spent, so v0.4 turns to the CSS gaps that everything else waits
on: custom properties, @media, and the pseudo-classes that retire the checked
hack. That's the pool the roadmap draws the next Fridays from.