← All posts

Rux v0.5.1: what a phone found

A text field on a phone now behaves like one: drag moves the caret, a long press takes the word, the view follows the caret past the edge, and a toolbar gives you copy and paste, which a browser had never offered at all.

Rux v0.5.1 is tagged. Text input on a phone works: dragging moves the caret, a long press selects the word under your finger, a field scrolls to follow the caret instead of losing it off the edge, and a toolbar offers Copy, Cut, Paste and Select all. That last one is new on the desktop web too, where copying out of a Rux field was simply not possible before.

That is the announcement. The rest of this post is about how the release happened, because it was not planned and the way it unfolded is the useful part.

Four bugs in a line

v0.5.0 shipped a soft keyboard and an IME, and said plainly that neither had ever met a phone. A day later one did. What follows is the order things were found, which matters, because each was hidden behind the one before it.

The first was a diagnostic pointing at a line that does not exist. An undefined binding reported Variable not found: names (line 1, position 1). That position comes from rhai, and it is measured inside the expression, not inside the file. Since every {{ }} is compiled as its own small script, the line was always 1, whatever the file looked like. Printed beside a filename it reads as a location and is not one. It is now stripped: the expression is already quoted in the message, and a position that is always the same number tells nobody anything. This is the same defect v0.5.0 removed from CSS warnings, surviving in a place where the text arrived from a library rather than being written here.

The second was that touch used the mouse's gestures. A finger dragged across text selected it, because touch was routed down the same press, drag, release path a pointer takes, and that is what a pointer means. A phone means something else. Drag moves the caret, a long press takes the word under the finger, and a long press followed by a drag extends from there. That is the model now, so the long press is the only gesture that selects, which is exactly what frees the drag to mean something else.

Two things fell out of building it. The decision is one-way on purpose: a press that moves before the timer is a caret drag and cannot become a selection afterwards, however long the finger then rests, because a drag that turns into a selection halfway through is worse than either. And a resting finger raises no events at all, so the press deadline had to become a second clock beside the caret blink, in the one place this otherwise event-driven loop ever waits.

The third was not a touch bug at all. With the caret now draggable, dragging it toward the end of a long value showed that the field never scrolled: the caret walked out of the box and was clipped away. That was true of typing, arrows, End, a tap and a drag, on every platform, since long before this release. A text field whose content outgrows its width could not be used, and nobody had noticed, because nobody had dragged a caret along one.

The cause is a single line of styling. An input with no overflow of its own is given clip, while a textarea is given scroll. Only the second produces a scroll region, and a scroll region is what the existing "keep the caret in view" code moves. For an input there was nothing to move. That code is also vertical only, and computes a caret x it discards.

The fourth was created by fixing the third. A long press in a scrolled field selected the word roughly one scroll-distance behind the finger. Placing a caret had been taught about the new offset; picking a word had not. Two functions, written separately, each converting a pointer into a position in a string by hand.

The lesson, which is narrower than it looks

Three of these four were invisible to a desktop as well as to the test suite. The suite was green throughout, and would have stayed green: none of it is the kind of thing a test reaches, because the tests assert what the code computes and every one of these was about what a person sees.

The fourth is the more specific lesson, and it is why two of these fixes end with a refactor instead of a repair. Two places doing the same coordinate arithmetic will eventually disagree. Adding the missing offset would have fixed the symptom; there would still have been two conversions, and the next change would have had the same coin-flip. There is now one function that turns a pointer into a position in a string, and it cannot disagree with itself. The toolbar was written the same way for the same reason: its geometry is one function shared by the painter and the hit test, with button widths estimated rather than measured precisely so the hit test needs no text engine and structurally cannot drift from what was drawn.

The toolbar, and a clipboard that was never there

The thing all of that was in service of: selecting text is no use without somewhere to send it.

A phone has no Ctrl+C. Less obviously, a browser gave Rux no clipboard at all, on any device: the crate that talks to the system clipboard is desktop-only, so copy and cut in the playground did nothing and always had. The browser's own copy bubble is not available either, whatever the selection says, because the hidden input it would act on is untouchable by design: it is one pixel square, transparent, and passes every tap through to the canvas. Setting a selection on it from code does not raise native selection UI. That was checked on a phone rather than assumed, which is the only reason it is stated here as fact.

So Rux draws its own: Copy, Cut, Paste, Select all, above the focused field while something is selected. On the web it goes through navigator.clipboard, which changes the shape of paste rather than just its implementation. Writing can be fired and forgotten. Reading cannot, because it is a promise and may prompt for permission, so a paste is started by the tap and applied when the read resolves. A refused prompt is silent: declining is a decision, not a fault.

The four actions were lifted out of the keyboard shortcut handler rather than reimplemented, so Ctrl+C and the Copy button are the same code.

crates.io, at last

The one item of v0.5 that was still outstanding. cargo publish rejects a path dependency that carries no version, and 24 of them carried none; the versions now live in one [workspace.dependencies] block, so a release bumps them in a single place.

Eleven crates go up with this tag. Two are held back: rux-web, because a wasm cdylib is not something anything can depend on, and rux-highlight, whose grammar handling is not ready to be anyone's dependency. Every library says in its own description that it is internal to Rux and that the supported entry point is ruxlang, because the split matters: ruxlang is the thing with a promise attached, and the crates under it are free to move.

cargo install ruxlang

What this release is not

The toolbar is Rux's, not the platform's. It does not look like the Android or iOS selection menu, it has no handles to drag the selection ends with, and it does not offer Look up, Translate or anything else a native menu carries.

Expression warnings still carry no file line. Dropping a line that was always wrong is not the same as finding the right one, and the template parser still does not record where each binding began.

Word-wise movement, triple-click line select, and drag-and-drop of selected text are all still missing, and text selection has no ::selection styling.

Unchanged and still the largest gap: there is no true inline text flow, so two <text> elements cannot share a line. A component takes props but cannot emit events or render children. r-for has no keys. The reactive tier has no effects or computed values. There is still no mobile code in the repository: everything here is a browser on a phone.

Next

v0.6: applications bigger than one screen. External CSS files, component slots and events, a router, keyed r-for, and computed values.