Rux v0.7.0: the fork, and things that move
Rux stops being stock rhai: a function can read and write the signals around it, a missing property raises instead of evaluating to nothing, and `?.` exists. Things animate, with a transition property, enter and leave, and progress you can drive with a finger. Vector geometry is an element. Plus the cross fade that worked once and then never again.
Two things were true of every Rux release before this one. A function could not change what was on the screen, so every handler in every example was written inline. And nothing moved: a box appeared where it was going to be, and left by ceasing to be drawn. v0.7.0 is the release where the language stopped borrowing someone else's script engine, and where a screen acquired a middle state between one arrangement and the next.
What landed
A function sees the scope it was written in, and can write it. fn bump() { count++ } is a working handler, and @tap="bump()" moves the screen. This
retires the single biggest trap in the language, the one /learn spent a
callout on and docs/05-as-built.md called "the single biggest trap": before
this, a fn could read a copy of a signal, write to the copy, and report no
error while nothing happened.
The mechanism was already in rhai, as an opt-in call form that runs in the caller's scope. The fork makes that what a call means, and the change is two lines. That is worth saying plainly, because it is wildly out of proportion to the size of the language change, and it is the strongest argument that vendoring the whole engine was right: the change was cheap precisely because it could be made in the one place that already knew about scopes. One consequence stands: a method call does not capture the caller's scope, because method dispatch passes its receiver by reference and the scope cannot also be borrowed. A function that needs the surrounding state is written as a plain call.
A missing map property raises instead of evaluating to nothing. Silence was
exactly the failure the dev overlay exists to kill: a typo in a property name
produced (), which then flowed through arithmetic and interpolation and turned
up as a blank on screen three steps away from its cause. Strict bindings make it
an error where it happens. ?. and ?? ship in the same release rather than
later, because they are not a separate nicety: they are the escape hatch that
strictness creates the need for. Without them, every legitimately absent
property, from data still loading to an optional field, turns a silently wrong
result into a noisily wrong one with no way to say "absent is fine".
Alongside those, truthiness is JavaScript's rather than rhai's, ++ exists, and
arrow functions are a spelling of a closure.
Things move. transition: opacity 200ms ease-out in a stylesheet animates a
property when it changes, and r-transition animates an element arriving and
leaving, with :enter-from and :leave-to naming the states at the ends. A
transition's progress can be bound rather than run off the clock, so a panel
follows a finger and commits or reverses on release. The router takes an
r-transition too, so a navigation is a crossover rather than a cut, and a
navigation can itself be driven by a drag: the pages move with the hand, and the
release decides whether the history moves.
Vector geometry is an element. <path d="…"> takes the full SVG path
grammar, arcs and smooth continuations and relative commands included. Paint is
CSS rather than attributes, so fill and stroke and stroke-width cascade and
inherit like everything else. :d binds an expression, which makes a chart a
computed path, and transition: d morphs one shape into another when the two
have the same sequence of commands. When they do not, the shape cuts, which is
the documented rule rather than a defect.
Layout gained the rest of position. All five values are honored, and
sticky in particular, which parsed for four releases and quietly fell through
to relative the whole time. Inside a scroller a sticky box clamps to the
scroller's content box rather than to the part on screen. A transform now
establishes a containing block, which is what makes an absolutely positioned
child of a moving element move with it.
A route can refuse. A guard runs before the history moves, so a refusal
leaves no entry behind to go back to, and it covers Back as well as forwards.
Returning false cancels, a string redirects, and anything else allows.
A handler can read the screen. query(".row") returns element handles by CSS
selector, and a handle answers reads about the frame already on screen. Reads
only, and only from a handler: there is no frame to measure before there has
been one, and tree mutation is deliberately not part of this.
Gestures beyond @tap. @press, @release, @longpress, @swipe and
@drag, each carrying its touch points. A @drag claims the finger, so the
page underneath does not scroll while a card is being dragged.
Lifecycle. mounted and unmounted run as inline bodies, and unmounted
never fires unless mounted fired.
The editor knows the language. The VS Code extension completes elements,
attributes, directives, honored CSS properties and their values, the script
globals, and the names the open document declares. What it offers comes from the
same list the runtime consults when it warns about a property it does not honor,
which is the point of the arrangement: if the editor offers it, it works. A
completion list that suggested float and then left a warning in the dev overlay
would be worse than no list. Hover answers what the popup answered a minute ago
and then took away, go to definition follows a use path to the file, and the
outline is the three sections and what is in them.
One behaviour change, and it is the only one in v0.7. A document's rules now
style the components the document uses. That was chosen deliberately, and it
means a component and its caller that share a class name now collide.
<style scoped> opts out and means the same thing from either side. Worth
knowing separately: variables cascade into components, rules do not.
The bug the tests were green through
The cross fade between pages worked the first time and never again.
It was reported that way, in those words, and every check made before that had navigated exactly once. So the defect sat outside the test, and outside every manual look at it too. Measuring four navigations by colour channel showed the first fading properly, from 255 down to 99 over the full 800ms, and the second, third and fourth collapsing to zero in about 100ms.
A finished track kept its from at the value it had set off from, and a track
that is no longer active answers with from. So the next transition on that node
was handed a stale start: a page that had faded in from 0 to 1 was asked to leave
and dutifully ran from 0 to 0. The first navigation worked, and hid it, because
that page's track came from a settled state where the start and the target agree.
Two things about how this was found are worth more than the fix. It took a sequence, which no single-action check could have produced. And it took being contradicted: an earlier claim that both pages animate was disputed, the first probe built to settle it was a bad test that controlled away the very thing under suspicion, and that was pointed out too. Measuring the heading band afterwards showed brightness collapsing within 150ms of the click and then climbing back over 1400ms, which is the arrival alone. The eye was right and the claim was wrong.
It was not the only one that needed eyes. A shape set off from a square towards a circle and froze partway, permanently, because the equality check that tells the animator's own last write apart from a fresh authored value had no arm for path geometry and answered "different" every frame. A chart's filled band drew a whole band's height below its line and across the buttons underneath. A departing page was invisible at zero opacity while its scrollbar drew at full strength over the page behind it. None of the 615 tests passing at the time had an opinion about any of them.
The suite is 675 tests now, and green. That number is worth reading as what it is: protection against these coming back, not evidence that the next one will be caught.
What this release is not
There is no packaging. rux build moved to v0.8, where it is the first item,
and rux.toml moves with it. A .rux app still runs through the rux binary,
and there is no bundle and no standalone executable.
Almost none of the motion has met a finger. Every animation run above was a mouse or a laptop touchpad, and a touchpad reaches the app as a mouse and reports one finger however many are on it. A drag driven swap is the case that most wants a real hand, and the axis claim has never met one. Multi-touch, pinch and rotate are unverified for the same reason: the vocabulary is there and the second finger is not.
Animation ships with no prefers-reduced-motion. A whole animation system
landed in this release and Rux never asks the operating system whether the person
using it has said they want less of it. For some people motion is a medical
problem rather than a taste, so that is an accessibility gap, it is the urgent
half of what is missing, and naming it here is not the same as fixing it. It is
the first thing v0.8 does.
What is missing is the asking, not the mechanism. A document can turn its own
motion off today, with the same trick examples/router.rux uses to run a
navigation in slow motion: put a class on the root and override the durations
under it.
<template>
<screen :class="#{ still: reduce_motion }">
<view class="card" :class="#{ lifted: up }" @tap="up = !up" />
<text @tap="reduce_motion = !reduce_motion">Reduce motion: {{ reduce_motion }}</text>
</screen>
</template>
<style>
.card {
width: 120px; height: 120px; background-color: #4c7dff;
transition: transform 240ms ease-out, background-color 240ms ease-out;
}
.card.lifted { transform: translateY(-40px); background-color: #ffb454; }
/* The opt-out: one class on the root turns the motion off under it. */
.still .card { transition: none; }
</style>
<script>
let reduce_motion = signal(false);
let up = signal(false);
</script>
transition: none removes the declaration rather than shortening it, so nothing
animates and nothing is left half-applied. An app that wants to be careful about
this can offer that switch in its own settings in v0.7. What it cannot do is
default it from the system, and that is on Rux rather than on the author.
unmounted is document level. A per instance unmount hook is waiting on
instance pruning, so a component cannot yet run something of its own on the way
out.
Still standing from before: two <text> siblings stack rather than flowing
inline, which is the known ceiling rather than a v0.7 gap; <input type="select">
has no native picker; and a <path> has never been through a real icon export
from a design tool, though the grammar is complete and tested.
Next
v0.8 is packaging: rux build, rux.toml, and getting a Rux app onto a phone
without a week of Gradle. Which is also what will finally put every one of the
unverified rows above in front of a real hand.
The reduced motion query goes out ahead of all of that. Packaging is the bulk of the milestone and will take several releases to land, and an accessibility gap does not get to wait behind it.