EUI applications without a browser
Gaps§04.9

What is not there yet.

Writing a renderer means rewriting what a browser gives away: text shaping, input methods, accessibility, selection. That is the real cost of this project and it is not hidden in a later milestone. This page is the other half of the catalogue — the things a desktop application expects, each one with where it would be fixed and whether anything is actually blocking it. Seven of them are closed now, and the page keeps those rather than deleting them: what a gap cost to shut is the most useful thing it leaves behind.

Ledgertoday

Twenty-three things a desktop application expects

Scored against what the client does now, not against what the catalogue names. A widget that exists but cannot be reached from the keyboard is partial, not present.

needwhere it standsstate
iconsThirty-three icons under forty-seven names, stroked from the client's own table; an unknown name draws nothing and keeps its spacebuilt
drag & dropAll three wire events are emitted, with a grabbed source and a payload measured against the node that catches it; a file dropped from the desktop arrives on Wayland and nowhere elsepartial
context menuThe event fires on right-click, position: pointer places a panel at the hand, and the catalogue wraps bothbuilt
command paletteNeeds an accelerator to open it, and there is no global key captureabsent
focus trapA modal prop makes its subtree the whole Tab order, and nestsbuilt
escape to closeReaches a handler on the path that asked for it; blurs only when none didbuilt
acceleratorsKeys reach only a focused node holding key_downabsent
arrow navigationA widget can now take the arrows and keep Enter, but there is still no roving focus and no type-aheadpartial
a11y rolesDeclared in props and preferred over the kind; 40 role names, states, values and set positionsbuilt
overlay dismissalA press outside shuts an overlay that holds a blur handler; still no close on scroll or on focus leavingpartial
overlay placementTwo shapes: under its anchor — left-aligned, flipped up, clamped — or at the pointer. No start, end or centred, no offset, no arrowpartial
sticky headersSticky positioning is out of layout version 1absent
grid tracksN equal columns only; no spanning, no named areaspartial
text selectionInside an editable node only — a label cannot be selectedpartial
horizontal scrollbarThe thumb is computed for the vertical axis aloneabsent
exit animationA node carrying exit keeps its painting while it leaves, on the accelerate curvebuilt
density / font scalePlumbed through theme resolution and never setinert
RTL mirroringShaping is bidirectional; layout mirroring is out of version 1absent
split panesDraggable dividers on both axes, keyboard-movable, panels sized in pxbuilt
virtualised listsWindowed, with heights the tree does not holdbuilt
light / dark / paletteResolved from roles, contrast by construction, follows the desktopbuilt
application typefacesA DefFont binds a font role to faces the application serves as content-addressed assets; sans and mono can be replaced. No WOFF2, no variable axespartial
input methodsPreedit anchored to the field, commit as one text_inputbuilt
Iconsbuilt

The kind that used to paint nothing

icon had been one of the sixteen primitives since the first protocol version, sized by layout like a picture and painted by nothing: the painter’s match on node kind had no arm for it, so it fell through and left a hole of the right size. The catalogue worked around it with characters — a chevron was ▾, a tick was ✓ — which land on a 227 KB fallback symbols face and are a poor icon three ways over: they cannot be sized against the control they sit in, cannot take a colour apart from their label, and reach a screen reader as themselves.

There is an arm now, and a table of thirty-three icons behind it, under forty-seven names — fourteen of those names are aliases that reach for a shape another one already draws, so a view can say sort_asc or inventory and get arrow_up or box. Every icon is polylines on a 24-unit grid with a 2-unit stroke, and every segment is the same rounded capsule a chart’s line is made of — so it cost no new pipeline, no font, no asset fetch and no protocol version. A run of one point is a dot, because a capsule of no length is a circle of the stroke’s own radius.

chevron_down
chevron_up
chevron_left
chevron_right
check
minusdash
plus
close
menu
gripdrag
arrow_upsort_asc
arrow_downsort_desc
arrow_left
arrow_right
more_h
more_v
dot
search
circle
calendar
logout
warning
griddashboard
docorders
userscustomers
boxinventory
folderfiles
star
spherescene
filterfunnel
chartreports
sliderssettings
lockpermissions

An icon with no size of its own takes a square from the font size in force, so one beside a label needs no measurement from the server. An unknown name draws nothing and keeps its space — which is the rule that lets the set grow without a client release, and is a MUST in 03 §2 now rather than an accident of the implementation.

Nothing in the catalogue draws an icon as a character any more. The select’s chevron, the accordion’s and the tree’s disclosures, the calendar’s month arrows, the chip’s remove and the checkbox’s tick and dash are icons. A few call sites still pass a character — icon_button keeps taking one, so an application that has not moved yet still works — but where an icon is given beside it, the character is never drawn. Pagination gained something else on the way: its ends are disabled now, so the page cannot be walked past either end, which the character version had never stopped.

Events§06.1

Two of the thirty-two are on the wire and nowhere else

The protocol names thirty-two event kinds. Thirty are emitted by the client. The other two are decodable, documented, and never sent — a server may subscribe to them and will wait forever.

double_click0x02
resize0x10

Drag and drop used to be three of the dead ones, and is not any more: a node carrying drag is grabbed, drag_over reaches whatever is under the hand, and a release becomes drop — each payload measured against the node whose handler catches it. A file dragged in from the desktop arrives too, as a file_drag and then the same file_pick the dialog sends, on Wayland and nowhere else yet.

It is worth being exact about which of the two left is a hole and which is a decision. double_click is a convenience a server can approximate from two clicks and a clock. resize cannot be approximated by anything: the server never learns a node’s box, so a view that needs one works it out from the viewport it is told about.

Keyboard§06.3

There is no global key capture

That is the specification’s own sentence, under the heading What the client will not report. Keystrokes reach a focused editable node, or a focused node that explicitly holds a key_down handler. Nothing else.

Read it together with the driver and the consequence is sharp: if nothing has focus, no key is reported at all. Ctrl+K, Ctrl+S, / and ? — the whole accelerator vocabulary of a desktop application — are unreachable, and a command palette cannot be opened by the gesture that opens command palettes.

Escape used to have the matching problem from the other side, and this section used to ask for three changes to fix it: Escape reaching a handler on the path, a modal prop, an autofocus prop. All three shipped — below is what they do — so an overlay now closes on the key every overlay closes on.

What is left here is the paragraph above it and nothing else: no global capture, so no accelerator and no command palette opened by the gesture that opens command palettes. That one is the specification’s decision rather than unwritten work, and changing it means changing 06 §5 first.

Layout§04.9

Stated so no one goes looking

The layout specification has a section listing what it deliberately does not do in version 1. It is short, and every line of it is felt by some desktop pattern.

no sticky positioning

No sticky table header, no sticky section header in a list, no toolbar pinned inside a scroller. The data grid keeps its header outside the virtualised list instead, which works and is the pattern to copy.

grid: N equal columns

No track sizing, no spanning, no named areas. A table with a 32 px checkbox column, a flexible name and a 120 px date is built from rows with per-cell widths — which is why resizing a column means re-emitting every row.

no aspect-ratio

Thumbnails, media placeholders and avatars in a responsive grid need a height the server has worked out for them.

no RTL mirroring

Text shaping is bidirectional; layout is not. A leading edge is always the left one.

no cross-container stacking

z orders siblings within one stack. Between two overlays, order in the tree is the order on screen.

no auto margins, no order

Pushing one button to the right of a toolbar is a spacer with grow, which the catalogue does throughout.

Splitsbuilt

Draggable dividers, on both axes

This page listed split panes as unwritten rather than blocked, and the distinction was worth something: they are written now, and nothing in the client changed to allow it.

A press on the divider captures the pointer, so a drag that runs off it still arrives. The move and release handlers sit on the container rather than the divider, and the payload of a pointer event is measured against the node whose handler catches it — so the number reaching the server is already the divider’s position inside its container, needing no arithmetic and no memory of where the drag began. Moves are coalesced to one per frame, not one per sample the mouse sends.

The divider also holds key_down, which is what puts it in the Tab order: a split can be moved with the arrow keys, and recentred with Home, by someone who never touches a pointer.

what it does
  • Vertical and horizontal dividers, and they nest
  • A minimum size for each panel, clamped in one place
  • The fraction is per mille, so it survives state as an integer
  • Both conversions round, so a divider dropped on a pixel rebuilds on that pixel — checked across every pixel of travel
  • A separator role, an orientation, and the position as a value
content that answers its panel
  • Each panel is built by a function of its own width in pixels
  • So a view can branch on the panel instead of the window
  • bp() is no use here: its rungs are Tailwind’s and a 309 px panel and a 505 px one are both xs
  • pane_bp(px) is the same idea at the scale a panel lives at — 200, 320, 480, 720

What is still missing is the latency: every frame of a drag is a round trip, because a local chunk cannot read the event that triggered it. On a loopback or a LAN that is invisible; over a long link it would not be. That is the chunk-payload gap below, and it is the one thing here that a protocol change would fix.

A11y§03.6

A widget says what it is, instead of being guessed at

The mapping used to be kind and handler alone: anything with a click handler was a button named by the text inside it. So a checkbox, a switch, a tab, a menu item and a slider all reached AT-SPI, UIA and AX as “Button”, and none of them carried a state. A screen reader user could not tell a switch from a link.

A node may now declare itself, in props the client reads: a role from forty names, a label that overrides the text inside, and the states — checked (with its third value), expanded, selected, disabled, read-only, required, invalid, busy, modal — plus a value with its range, a place in a set, a level, an orientation and a live urgency. Props already travel, so this cost nothing on the wire: it is a change to one file in the client and a section of the specification.

three rules, now normative
  • Leafness follows the role, not the handler. A button or a tab is named by its text and exposed without children; a tab_list, a menu or a grid keeps what it holds — which the old rule swallowed
  • A disabled node keeps its role. Disabling drops the handlers, so without a declaration there is no button left to infer and the control decays into an unnamed group
  • A present zero is not an absence. value_now: 0 is a slider at the bottom of its range
what the todo now says
  • CheckBox "Write the spec" checked=Yes
  • Button "Remove Write the spec" — where it used to say Button "×"
  • Tab "Overview" selected=true 1 of 3
  • Slider "Value" value=40
  • Button "Previous page" disabled

An unknown role name falls back to the kind rather than failing, which is what lets the vocabulary grow without a client release. Sixteen vectors hold the rules, including every role discriminant surviving the to_u8/from_u8 round trip its journey across the worker’s process boundary makes of it. The kind-mapping default is pinned by a separate test, so a tree that declares nothing is provably exposed exactly as it was before any of this existed.

Half-done, and worth saying so. The client reads all of it; the catalogue is still declaring its way through. Checkbox, switch, tabs, segmented, icon buttons, pagination, the calendar arrows, the chip’s remove, slider, progress, toast and the split divider say what they are — a run of the gallery answers with six TabLists, sixteen Tabs, four Separators, two Sliders and two Progresses where every one of those used to be a button or a group. Against that, three hundred and thirty-eight nodes are still plain buttons: menus, the select, the tree, the data grid, dialogs and the day cells have not been declared yet. And a real screen reader remains the only honest test — SNAPSHOT_A11Y=1 on the off-screen renderer is the one that can run without one.

Keyboard§03.1

Four things a server cannot do for itself

It does not own Tab. It was never told about Escape. And it cannot know which node the client will treat as pressed. So a dialog could not trap focus, could not open with focus inside it, and could not close on the key every dialog closes on. Three props and one changed rule fix all four.

what the client now reads
  • modal — while it is laid out, the Tab order is its subtree alone. Innermost wins, so a dialog over a dialog traps in the second
  • autofocus — focus starts here when the surface arrives, and is not reclaimed by a later batch: someone tabbing through an open dialog is not pulled back to its first field
  • keys — the keys this node wants, and it is sent no others
and what that buys
  • A tab can take ArrowLeft and ArrowRight and still be activated by Enter — which a key_down handler used to make impossible, because it claimed every key
  • A dialog can listen for Escape without hearing every letter typed into the field inside it
  • Fewer round trips: a key a node did not ask for is not sent at all
  • A node with a handler and no keys prop still hears everything, so nothing that worked stops

Escape follows from the third. It reaches a handler on the path that asked for it, and focus is left where it was so the surface can put it back; with nothing listening it drops focus, which is what it has always done. Driven through a real server: the gallery’s sheet opens at 940 nodes carrying Dialog "A sheet" modal, and one Escape later it is 934 and the dialog is gone.

Seventeen vectors hold it, and the off-screen renderer grew SNAPSHOT_KEYS so a focus ring, a trapped Tab or a surface that closes on a key can be looked at without a keyboard. What is still missing is above this: roving focus and type-ahead are a widget’s to implement over the wire for now, one round trip per arrow, and accelerators need the global capture the specification still refuses.

Blockedor not

The distinction that actually matters

Most of this page is work, not impossibility. It is worth separating the two, because the status page had one of them filed wrong: column resize was recorded as blocked, and it is not.

genuinely blocked
  • Sticky headers — layout version 1 has no sticky, and faking it needs a scroll offset the server does not have
  • A masked password field — nothing in the client hides an input’s glyphs
  • Local keyboard navigation and local drag — a bytecode chunk cannot see the event that triggered it, so it never learns which key, or where the pointer is
  • Control heights that follow density — the theme resolves them and no dimension on the wire can reference them
merely unwritten
  • Column resize — the same server-driven drag split_pane now uses; what is left is re-emitting the row styles a width change touches
  • link, time_picker, list_item, panel — ordinary compositions, waiting their turn. context_menu, textarea, radio_group, combobox and rating came off this list and are in the catalogue

The rule the two columns come from: a thing is blocked when expressing it would need a new node kind, a new style key, a new event, or a chunk that can read its own event. Everything else is a function someone has not written yet.