Eight applications, and one client that drew all of them.
Every picture below is a real render, taken off the wire: a Soli server
answering an EUI session, the Rust client laying it out and painting it
on the GPU, captured at 2× by the repository's own
snapshot tool. No mockups, and no browser anywhere in the
chain.
They all live in one Soli application —
examples/counter-app — and each is a route of four
words: a handler that takes an event and returns state, and a view that
takes state and returns a hash. Clone the repository and you are running
these in about a minute.
soli serve examples/counter-app --port 5092
cargo run -p eui-client -- ws://127.0.0.1:5092/_eui/session/tracker
A tracker, mixed by the server
FastTracker 2's shape: sixty-four rows, eight channels, five instrument
shapes, and a cursor that lives under the keyboard. The client has no
synthesiser and no note scheduler — on Play the server
walks the pattern into PCM, writes a wav, and the window
fetches it by hash like any picture. The milliseconds the window reports
back are what move the row cursor: the pattern scrolls because the sound
says where it is.
- 169 344
- samples mixed for one pattern at 22 kHz, 16-bit 0.9 s of interpreted Soli — eight times faster than it plays
- 2
- handlers on the whole grid, one click and one key_down a pattern is a character grid, so a click's point is two divisions
- 2 skins
- theme roles, or FastTracker 2's own bevels the modern one follows the viewer's desktop palette; the retro one is painted in hex
A code editor, and the highlighting is a function
The buffer is an array of lines in the session's state; the keyboard
arrives as key_down on one box, and every keystroke is a
round trip that comes back as a patch — the line that changed and
the cursor that moved. There is no text widget involved: the client has
a field whose caret belongs to the window, and that is the wrong shape
for an editor, which wants the gutter, the highlighting and the cursor
to be one thing.
The lexer is thirty lines of Soli. It cuts a line into keywords,
strings, comments and numbers, and each token gets a text
node with a role for its colour — so the same buffer is
olive on one desktop and blue on another, and the client is never asked
to know what a keyword is. What it opens is its own source. Nothing is
ever written back: there is no route that could.
- 27 of 475
- lines sent for a window of a 475-line file the buffer is windowed around the cursor, like the tracker's pattern
- 5 kinds
- the lexer answers with: keyword, string, comment, number, the rest each one a theme role, resolved on the client (05 §1)
- 1 handler
- for the whole keyboard a click on a line moves the cursor; the box itself only takes keys
A music player that ships as one binary
Search, a record, a track list, a transport, and a disc that turns for
exactly as long as something plays. Its sleeves are drawn from each
record's own name, so nothing is fetched; point it at a Spotify client
and the server fetches the real artwork instead, crops it and re-encodes
it, and the window still sees an ordinary picture from its own origin.
Packaged with soli desktop build it is one file in the
application menu.
- 0 pictures
- fetched by the window, ever assets come from the session's own origin, by BLAKE3 hash (01 §2.2)
- MPRIS
- plays on the machine it runs on, or on a Spotify Connect device the server talks to the desktop bus; the client only draws
Every widget, on one page
The conformance page: navbar, tabs, stat tiles, banner, progress, badges, chips, segmented control, pager, breadcrumb, stepper, select, slider, sound, moving picture, three calendars, a data grid with inline editing, four charts, an accordion, a tree, a menu, a tooltip, an empty state, a form and a sheet. Every one of them is a Soli function that returns a hash — there is no widget runtime in the client to update.
- 107
- functions in the catalogue, none of them in the client the component reference lists all of them
- 1 breakpoint
- per layout decision, evaluated on the server the tiles wrap themselves; the client has no media queries
Forty thousand cards, and only the ones you can see
A social feed, windowed: the client sends the row range it can show, the server builds those cards and no others. A like is one round trip that patches one card. Given an X account it fills the same cards from a real timeline instead, and says why in the header when the API refuses.
- 40 000
- rows the client knows the height of row heights are arithmetic, not measurement (04 §7.1)
- 1 window
- of cards built per scroll, not forty thousand the server is told the range; the rest are never encoded
Ten thousand rows, sorted without rebuilding one
The performance check. Sorting sends a permutation of keys, not a new table: the client matches keyed children by key and moves them. Mounting the whole thing and sorting it are both measured in the repository's budget suite, and the numbers on the front page come from that run.
- 50 014
- nodes in the session, five per row only the rows in view are measured or painted
- < 2 ms
- of CPU per scrolled frame, the budget this holds to
spec/10 §2 — measured by
cargo xtask bench
The list everyone writes first
Keyed rows, checkboxes that carry the id of what they toggle, and a text field whose value belongs to the client: what a person has typed survives every re-render the application does for other reasons, because the caret, the selection and the clipboard never leave the window (03 §3).
Nine nodes, and the whole protocol
A title, a number, two buttons and a hint. It is here because it is the smallest thing that exercises every part of the round trip — and because one of its buttons never asks the server at all: a declared local handler updates the number in the window and reports afterwards. The other is 25 bytes out and 9 bytes back.
- 25 B
- a click, on the wire and 9 bytes for the answer that changes the number
- 0 B
- for the button that runs locally a declared handler, not code from the network (07 §1)
What it takes to add the ninth
One line in config/routes.sl, a handler, a view. No client
release, no widget registration, no build step: the client already knows
the sixteen kinds, and everything above composes them.
router_eui("tracker", "tracker#tracker", "tracker#tracker_view")
The handler is (event, params, state) -> state. The view is
state -> hash. Everything else — diffing the tree,
encoding the patch, resolving the styles, fetching the assets —
belongs to the two ends and not to you.