Rux v0.2.0 — selection, scrollbars, and a lot of CSS
Text selection and the system clipboard, select and textarea, keyboard focus, real scrollbars in both axes, and the CSS push: fonts, gradients, shadows, transforms, per-corner radius, grid placement.
v0.1 could open a window and edit a field. v0.2 is about making that field feel like a field — and about closing the gap between "valid CSS" and "CSS that does something."
Four things landed: text selection + clipboard, the last two input types, scrolling polish, and a serious CSS push. 74 tests pass, and every item below was driven in the window before it was called done.
Text selection and the clipboard
A focused input now has a selection, not just a caret.
- Drag across text to select it; double-click selects a word.
- Shift + any movement (arrows, Home/End, Up/Down in a textarea) extends from the anchor; the same movement without Shift collapses.
- Typing, pasting, Backspace and Delete replace what's selected.
- Ctrl+A / C / X / V against the real system clipboard, via arboard. Paste three lines into a single-line input and it keeps the first, which is what you'd expect and not what a naive implementation does.
The modelling decision that made this clean: a selection isn't a second thing to
track alongside the caret, it's the range between a caret and an anchor. So
focus became { model, caret, anchor }, and the one restore pass that puts the
caret back after a rebuild puts the selection back too. Given that v0.1's nastiest
bug was a restore pass that only handled half its job, not adding a second one
mattered.
The trap worth writing down: parley will hand you selection rectangles, and they're laid out on parley's line pitch. Rux draws lines with the leading trimmed, which is a different pitch. Take those rects as-is and the highlight drifts a little further off the glyphs with every wrapped line. It's invisible in a one-line field, obvious the moment you drag through a textarea. So Rux takes only the horizontal extent from parley and recomputes the vertical from its own line stepping, keyed by the line index parley helpfully returns beside each rect.
select, textarea, and keyboard focus
type="textarea"— multi-line: Enter inserts a newline, the value wraps, Up/Down move the caret between lines, and it scrolls vertically with the caret kept in view as you type.type="select"— shows the bound value and opens a dropdown of:options: one floating panel with a shadow, the current value as a pill, hairline separators.- Tab / Shift+Tab now move a focus ring through every focusable: text inputs, buttons, checkboxes, radios, selects. Space/Enter activates the focused one. Tabbing to something below the fold scrolls it into view.
Two fixes came straight out of using it: inputs were hugging their text and
shrinking as you typed (they now default to width: 100%), and single-line
inputs wrapped when they should clip.
Scrolling that behaves like scrolling
v0.1's scrolling was wheel-only and vertical-only. Now:
- Horizontal scrolling — the offset is two-axis, so a box scrolls whichever way its content actually overflows. Shift+wheel goes sideways.
- Scrollbars — an overlay on the box's trailing edge, only on an axis that has travel, with the thumb sized as the box's fraction of the content (floored so it stays grabbable). When both axes scroll, the tracks stop short of the corner.
- Drag a thumb, and a press on a thumb never becomes a tap on the content underneath it.
- Keyboard — arrows, PageUp/PageDown, Home/End scroll the box under the pointer, reached only after a focused input has declined the key.
- Touch — a finger drags the content. This is the one piece here that has not been verified on real hardware; there's no touch device on the machine it was written on.
The bug in this batch is a good advertisement for looking at things. The horizontal thumb was painted using the track's length as its thickness: a pale slab across the entire box, impossible to miss on screen, and completely invisible to a test suite that only ever checked the vertical bar. The axis you didn't test is the axis that's broken.
The CSS push
The biggest gap in v0.1 wasn't a missing feature, it was silence: you wrote valid CSS, nothing happened, and nothing told you why. v0.2 attacks that from both ends: more honored properties, and a warning when a declaration is ignored.
Newly honored: font-family (you could not choose a font at all before, the
single most visible gap), font-style, letter-spacing, word-spacing,
line-height, text-decoration, white-space: nowrap, box-shadow, linear and
radial gradients, background-image: url(), transform (translate/scale/rotate),
per-corner border-radius, grid-column / grid-row / grid-auto-*,
aspect-ratio, position + inset, align-self / justify-self /
align-content, row-gap / column-gap, and cursor: pointer.
And two silent-wrongness bugs, which are worse than gaps:
>,+and~were all matched as descendant combinators..a > .bwas quietly styling grandchildren. Selectors now record a combinator per compound pair and match accordingly, siblings included.- Named colours didn't resolve. Worse, lightningcss minifies
#ff0000intored, so writing plain hex red got you the default colour. A full CSS named-colour table now backs the parser.
transform came with a caveat that's documented rather than hidden: hit, focus
and scroll regions are computed untransformed, so a transformed element still taps
at its original position.
Still missing
Written as plainly as the wins:
- CSS: variables and
var(),@media, pseudo-classes (:hover,:focus,:checked; the syntheticcheckedclass is a stopgap waiting for them),!important, per-side border colours,box-sizing,text-overflow. - Text: no word-wise movement (Ctrl+arrows), no triple-click line select, no
drag-and-drop of a selection, no
::selectionstyling. - Scrolling: no track-click paging, no kinetic touch fling, and
overflow-xcan't differ fromoverflow-y. - The big one: a signal change still rebuilds the whole tree.
Next: fine-grained reactivity
v0.2 shipped four features that each needed their own restore pass: put the caret back, put the selection back, put the scroll offsets back, remember which dropdown was open, remember where focus was. That list is now six entries long, and every entry is a chance to reproduce v0.1's caret bug.
Per-binding subscriptions delete the entire category. That's v0.3, and it's the last real divergence between the architecture doc and the code.