06 — Events
Status: normative for the kinds and payloads; implemented by eui-proto
(kinds) and eui-client (emission).
An event frame is a node, a kind, the atom naming the server-side handler, and a payload. Nothing in it is trusted until the server has validated it against the node's schema.
Event := node:varint event:u8 name:varint payload:Value
1. Kinds and payloads
| id | Kind | Payload | Coalesced |
|---|---|---|---|
0x01 | click | List[Float x, Float y], local to the node the event names — the one holding the handler, not the leaf under the pointer | |
0x02 | double_click | as click | |
0x03 | pointer_down | List[Float x, Float y, Int button] | |
0x04 | pointer_up | as pointer_down | |
0x05 | pointer_move | List[Float x, Float y] | per frame |
0x06 | pointer_enter | Null | |
0x07 | pointer_leave | Null | |
0x08 | key_down | List[Str key, Int modifiers] | |
0x09 | key_up | as key_down | |
0x0A | text_input | Str, the committed text | |
0x0B | focus | Null | |
0x0C | blur | Null | |
0x0D | change | Str, the editable node's whole value; for a track (spec 03 §3.4) Int, or List[Int lo, Int hi] for two handles | per frame |
0x0E | submit | Null | |
0x0F | scroll | List[Int x, Int y], the new offsets | per frame |
0x10 | resize | List[Float w, Float h] | per frame |
0x11 | context_menu | as click | |
0x12 | drag_start | as pointer_down (§6) | once a gesture |
0x13 | drag_over | List[Float x, Float y, Int slot] (§6) | per frame, and only when the target or the slot changes |
0x14 | drop | List[Float x, Float y, Int slot], slot = -1 for a cancel (§6) | once a gesture |
0x15 | long_press | as click | on a held contact, §5.1 |
0x16 | window | List[Int first, Int last], the rows a windowed list needs (spec 04 §7.1), inclusive | when the range changes, once a scroll has landed |
0x17 | ended | Null, a sound or a picture reached its end (spec 03 §7, §8) | |
0x18 | time_update | List[Int position_ms, Int duration_ms] | at most 10/s |
0x19 | wake | Null, the node's wake interval elapsed (§1.1) | every wake ms, at most 10/s |
0x1A | file_pick | List[Int upload, Str name, Int size], one per file the person chose (spec 03 §3.2); the bytes follow as Upload frames | |
0x1B | file_save | Str, the name the person chose; the bytes are owed as Blob frames (spec 03 §3.2) | |
0x1C | location | List[Float latitude, Float longitude, Float accuracy_m], coarse (§1.2) | every locate ms, at most 1/s |
0x1D | nfc_tag | List[Str uid, List[List[Str kind, Str payload]]], one tag for a scan the person started (spec 03 §3.3) | |
0x1E | file_drag | List[Bool over], a file is over a node carrying drop, or has left it (spec 03 §3.2) | only when the node under the file changes |
0x1F | back | Null, the person asked to go back (§1.3) | |
0x20 | level | List[Int peak_left, Int peak_right], 0..=100, how loud a sound has been since the last one (spec 03 §7) | on time_update's clock, and only when it changes |
Coordinates are logical pixels relative to the node's border box. button is
0 primary, 1 secondary, 2 middle. modifiers is a bit set: 1 shift,
2 control, 4 alt, 8 super. key is the key's name as in the W3C UI
Events KeyboardEvent.key value ("Enter", "a", "ArrowLeft").
1.1 Being woken
Every other event is something that happened. wake is the one an
application asks for: a node carrying a wake prop — an integer of
milliseconds — and a wake handler is sent one wake event every
that many milliseconds, for as long as it carries both. Nothing else
starts or stops it: remove the prop, the handler or the node and the
clock is gone.
It exists because an application that watches something the client cannot see — a player on another device, a job on a server, a countdown — has no other way to be given time. Without it such an application can only refresh when the person touches it.
A client MUST bound this, because it is the one event a server can ask for without anyone doing anything:
- a period below 100 ms is raised to 100 ms;
- at most four nodes wake at once, in tree order; the rest are ignored;
- a window that was not painted for a while owes one event, not the ones it missed: the next is due a period after the one that fires, not after the one that was scheduled;
- a clock already running keeps its phase when the tree is re-rendered. Only a node that was not waking, or whose period changed, starts one.
The budget of 10-budgets.md §1 — zero wakeups at rest — is about a
window nobody asked to wake. A node with a wake prop is a window asked
to wake, and it costs what it asked for.
1.2 Being placed
The second event an application asks for rather than receives. A node
carrying a locate prop — an integer of milliseconds — and a
location handler is told where the machine is on that interval, for as
long as it carries both. Dropping either takes the node out of the next
tree and the radio with it, exactly as a wake stops when its prop goes.
Four things must hold, and a client MUST check every one:
- the
locationcapability was granted (01-transport.md§2.1). Without it the tree is not even read forlocate, and a fix the platform offers is dropped rather than kept; - the window has the input. Nothing is reported while it does not — an application does not get to follow somebody because its window is open behind something else;
- the platform has actually produced a fix. Asking is not knowing, and a client MUST NOT invent one;
- the node's interval has elapsed. The floor is one second, whatever was asked for: no positioning hardware means anything faster, and the cost of asking is a radio rather than a timer.
The answer is coarse and the client makes it so. §2.1 grants the power to read a coarse location and there is no second capability for a fine one, so latitude and longitude are rounded to a thousandth of a degree — about 110 m at the equator and less everywhere else — and the reported accuracy is never better than 100 m, whatever the receiver claimed. The rounding happens in the client, on the side a server cannot argue with. A client MUST NOT offer a way to ask for more.
The count has a ceiling as wake does: a page wants one fix, and a server
that asks for a hundred gets two.
1.3 Going back
back is the one event with nothing under it. A system back button, a
mouse's fourth button, the platform's back chord — Alt+Left, or ⌘+Left
where the platform's own modifier is Command — and a swipe from the leading
edge of the screen are all the same request, and a conforming client MUST
report them identically: a server cannot tell a finger from a mouse (§5) and has no more
business telling these four apart, and a field that said which would be the
fingerprinting surface 00-rationale.md refuses.
Because nothing is under it, §2's walk to the nearest handler has nothing to
walk from. back is delivered to the mounted root when the root holds a
handler for it, and to nothing at all otherwise. The root is the one node
a server always knows, and is already where a component's own state lives
(07-bytecode.md §1).
A session whose root holds no back handler MUST have the gesture left to
the platform. This is not a courtesy. On a phone the platform's own meaning
for back is "leave this application", a client that consumes the gesture
without a handler to give it to has taken that away, and a person is then
inside an application with no way out. Holding a handler is how an
application says it has somewhere to go back to; the absence of one is how
everyone else gets out.
A back is a request and not a change: what it does is the server's to decide,
and the client MUST NOT assume it was granted. A client MAY show the movement
before the answer arrives, under 07-bytecode.md §6's rule
for anything provisional — it has to be able to put it back.
2. Emission rules
-
double_clickis a secondclickon the same handler within 500 ms, and it is emitted in addition to that secondclick, never instead of it. Both are in §1's table as separate kinds, and the previous rule already says a client emits neither unless a handler wants it — so a node that handles onlyclicksees two clicks, and one that handles both sees the second click and the double. A third click inside the window starts a new pair rather than reporting a second double: what a person means by three clicks is not two double-clicks. The interval is fixed here and is not a field: it is a property of hands, not of applications, and an application that could shorten it would be building a control nobody can hit. -
resizeis emitted when a node's laid-out box changes size, carrying the new width and height in logical px, and only for a node that asked for it. It is per-frame coalesced like the others, and it does not fire for a move: a node pushed sideways by a sibling is the same size and has nothing to report. The first layout of a node reports nothing either — a size that was never anything else has not changed — so a server that wants the initial box asks for it inconnectrather than waiting for an event that is not coming. -
A client emits an event only for a node that has a handler for that kind. There is no bubbling: the server composed the tree and attached handlers where it wanted them. A
clickon atextinside a button reaches the button because the button's handler is the nearest one on the path from the hit node to the root. That walk is the whole dispatch algorithm. -
pointer_move,scroll,resizeanddrag_overare coalesced: at most one per frame per node, carrying the latest value.drag_overis coalesced twice: per frame as the others are, and again against the last one sent, so a drag that crosses no boundary reports nothing at all. Six hundred samples down a list of forty is forty events (§6). -
A gesture that became a drag reports no
pointer_upand noclick: the lift is thedrop. Without this, putting a card down also activates it. -
changefires when an editable node's value settles — on blur, onEnterin a single-line field, or after 300 ms in which the value did not move. The timer is armed only while the value differs from the one the server last sent, so a character typed and taken back owes nothing and wakes nothing; a composition in progress counts as input, and the timer does not run out inside one. A client MUST NOT emitchangefor a value equal to the last it sent, nor for a node that has left the tree.text_inputfires per committed insertion and exists for local handlers; a server that subscribes to it across a wide-area link has misread the design. -
A track (spec 03 §3.4) reports
changethe moment the value under the hand crosses into anothertrack_step, and not once more. The quantiser has already done for a hand what the 300 ms does for a field, so the timer does not run and nothing is held for it. The same sentence above still governs — a client MUST NOT emitchangefor a value equal to the last it sent — so a hand crossing a hundred steps owes a hundred events and a hand shaking on one owes none. At most one is in flight at a time: while one is unanswered the latest value waits, and it is the latest that then goes, never a queue of the ones it passed. A press and the lift each report at once, being one event and not a stream; the arrows do too. -
A server batch does not move a track under the hand. The client draws its own value until the gesture ends and adopts the next batch's
track_valueafter it —07-bytecode.md§6's provisional rule, for state the client holds rather than a tree it edited. A server that clamps a value it was sent is obeyed at the end of the gesture and not in the middle of it, because a handle that jumped back under a finger that had not moved is a widget fighting the person using it. What the client compares against is what it last sent, not what the server last said: re-seeding it from the answer would have a hand still at 80 tell a server that clamps to 75 about 80 again, once per round trip, for as long as it stayed there. -
A
Handler::Localruns the chunk and emits nothing. AHandler::LocalThenServerruns the chunk, then emits.
3. What the client will not report
-
Keystrokes outside a focused editable node, other than to a node that explicitly holds a
key_downhandler and has focus. There is no global key capture. -
An input method's composition in progress. The client shows the preedit in the focused field and reports nothing; the committed text arrives as one
text_input, and a composition abandoned by a blur leaves no trace. -
Tab,Shift+TabandEscape: they move or drop focus (spec 03 §3) and are consumed by the client. So are the scrolling keys — the arrows, page keys,HomeandEnd— outside an editable node: they scroll, and only the resultingscrollis reported. On a focused track handle (spec 03 §3.4) they move it instead, and only the resultingchangeis reported.EnterandSpaceon a focused activatable node arrive as theclickthey stand for, at the node's centre. -
Pointer position while the window is unfocused or the pointer is outside it.
-
Clipboard contents without the
clipboard.readcapability. -
Any path on the filesystem. A
file_pickreports the file's name and afile_savethe name chosen for it; which directory either came from is the person's business, and the client keeps it. Nor is a dismissed dialog reported: an application learns that a person opened a dialog and thought better of it only if it was told, and it is not told. -
Where the machine is, unless a node asked (§1.2) — and then only as coarsely as §1.2 says, only while the window has the input, and never more than once a second. A client MUST NOT report a fix to a node that did not ask, and MUST NOT keep one when the capability is absent.
-
That a scan found nothing. A tag that was read is an event; a person who held their phone up and changed their mind is not, for the reason a dismissed dialog is not.
-
That the tree under a resting pointer was rebuilt. A batch is not a gesture: a node re-found by its id after one is the node the pointer was already on, so no
pointer_enter,pointer_leaveorpointer_movefollows from the rebuild (spec 03 §3). Reporting one would put a move on the wire per batch for every page that handles one — and on a page woken by a timer, an answer to that move is another batch. -
Anything else about the machine beyond the
Viewportframe. -
that a back gesture (§5) was begun and abandoned. A stroke from the edge that springs back changed nothing, and reporting it would make a hand that changed its mind indistinguishable from one that did not.
4. Server-side validation
The server validates every event against the node it names: the node exists in the tree it last sent, it has a handler of that kind naming that atom, and the payload has the shape in §1. A local handler's effect is advisory; the server re-derives state from its own model before anything is trusted.
Enforced: EventKind::payload_fits in crates/eui-proto/src/node.rs for
the shape, and lang/src/serve/eui/session.rs::validate for all three. The
shape check lives in the wire crate rather than in a server because it is a
fact about the format and there are seven servers.
A failure drops the event; it does not end the session. This paragraph said the opposite for a long time, and no implementation ever did it — for a good reason, which is now written down rather than left as a divergence. An event arrives from a client that may be a version behind, on a tree the server has since replaced, at a node that existed a moment ago. Ending the session for one of those costs a person their application for a mouse movement, and the tree they lose is the tree that would have told them why. A server MUST refuse the event and MAY log it; a server that also wants to end the session on a malformed payload MAY, but it is not the rule, because the cheap attack is then a single bad frame against every reader.
A float where §1 asks for a float, and an integer where it does too: a coordinate of exactly zero is an integer to any encoder that writes the narrowest form of a number, and a server that refused it would refuse the top-left corner of every node. The reverse is not true — a fractional button, slot or row index is not a narrower spelling of anything.
5. Touch
A touch screen reports contacts; this protocol has a pointer and no contact of any kind. No event kind in §1 is added for touch, and none is reserved. A server cannot tell a finger from a mouse, and must not try: the same tree works on both because the client resolves the difference before anything is emitted.
Resolving it is not the window's job either. Whether a stroke belongs to the
node under it or to the view behind it turns on whether that node asked to
hear pointer_move — which is a fact about the tree — so the client does
this on the near side of the tree, next to hit-testing.
A conforming client MUST follow one contact at a time. A second contact arriving while one is live is ignored until the first lifts: version 1 has no gesture that wants two, and a resting palm must not move the view.
The first contact is followed like this:
-
Down. The pointer moves to the contact and presses button
0:pointer_movethenpointer_down, exactly as a mouse would. -
The gesture is then taken or undecided. It is taken if the node the press landed on resolves a
pointer_movehandler, or carriestrackordrag_handle(spec 03 §3.4), or if the press took hold of a scrollbar thumb (spec 03 §2) — a slider, a split bar, a drag of any kind. A track is named by its prop rather than by a handler, which is the point of §3.4 there: it is the one case where the prop replaced apointer_movehandler that only ever existed to claim the stroke. Otherwise it is undecided. -
Taken: every move is a
pointer_moveat the contact, coalesced by §2 like any other, and the lift is apointer_up. The view does not scroll, and no fling follows. -
Undecided: moves report nothing and the pointer stays where it landed, so a tap that wobbles still resolves to the node it was aimed at. The gesture is decided by whichever comes first:
- the contact lifts — a tap:
pointer_up, andclickby the rule in §2, since press and release resolve to the same handler; - the contact passes the slop, a client-chosen distance from where
it landed which SHOULD be about 8 logical px — a scroll.
4a. The edge. A contact that landed within a short distance of the
window's leading edge — about 20 logical px, mirrored under a
right-to-left reading order — in a session whose root holds a
backhandler (§1.3) is the navigator's, whatever it landed on: it is undecided even over a node that asked for moves or carriesdrag_handle. Past the slop inwards it becomes a back gesture; past the slop along the edge it is an ordinary scroll and the strip is forgotten; a lift inside the slop is the tap it was aimed at. A back gesture gives the press back exactly as a scroll does —pointer_up, noclick— and the client then moves the topmost subtree that said how it leaves (03 §5.1) with the contact. On release past half the window's width, or still moving inwards when it left the glass,backis reported and the subtree goes; otherwise it returns and nothing is reported at all.
This is the one gesture that outranks the tree, and it is worth saying what that costs: a slider or a split bar within the strip loses its stroke. The strip is a bezel's width and not a thumb's for that reason — wide enough to find without looking, narrow enough that what it takes is an edge nobody puts a control against — and an application that wants the whole edge back can have it by not taking
backat all. - the contact lifts — a tap:
-
Scroll. The press is given back before the view moves: the node that took it receives
pointer_upand noclickfollows, because none was meant. The view then moves against the contact, by the whole displacement from where it landed — the slop is part of the stroke, not swallowed by it — and by each further delta after that, reported asscrollunder §2's coalescing. -
Lift after a scroll. A contact still moving when it leaves the glass carries the view on, over a client-chosen decay. A contact that has been still for a short time before it lifts does not: it was placed, moved and held, and throwing the view then is a bug a person feels as the page running away from them.
-
Cancel. A platform may take a gesture away — a system edge swipe, a call arriving, the application going to the background. Whatever was pressed receives
pointer_up; noclickfollows and no fling.
5.1 The held contact
A gesture that is undecided is also being held, and the client runs a timer of 500 ms from where the contact landed. Whichever of these comes first decides it:
- the contact passes the slop — a scroll, exactly as step 4 above, and the timer is forgotten;
- the contact lifts — a tap, and the timer is forgotten;
- the timer elapses with the contact still inside the slop, and then:
- if the press resolves a node carrying
drag(spec 03 §3.4), the gesture becomes a drag.drag_startis emitted, the press is not given back — nopointer_up, noclick— every later move is adrag_over, and the lift is thedrop. A client SHOULD ask the platform for a haptic; - else if the press resolves a
long_presshandler,long_pressis emitted, and the press is then given back as a scroll gives it back:pointer_up, noclick, because a long press that opened a menu must not also activate what it opened from. The gesture ends there; - else nothing happens and the contact stays undecided.
- if the press resolves a node carrying
long_press is not emitted for a mouse. A held button is not a gesture, and
context_menu is already what the second button means.
This adds no event kind and tells no finger from a mouse. The server
writes drag and accepts and hears the same three events either way; it is
the client that picks the grab suiting the input it has — eight pixels of
travel for a mouse, a handle or half a second for a finger. That is this
section's own principle applied one level further in. A client with a mouse
runs no timer, because nothing is ever undecided for one.
What this costs, plainly: a slow drag begun by a slowly-moving finger on a row with no handle is a scroll. There is no way around it that does not steal strokes from the list, and the handle is the escape.
Nothing here is particular to one platform. Android delivers contacts
through MotionEvent and iOS through touchesBegan/Moved/Ended/
Cancelled; both arrive as the four phases above, and the rules that follow
are the client's, not the platform's.
At the end of every gesture the client MUST clear hover, emitting
pointer_leave where one is due. A finger leaves nothing under the pointer,
and a node lit on pointer_enter would otherwise stay lit with nothing left
to put it out.
pointer_enter and pointer_leave therefore bracket a tap rather than
describing a resting pointer, and cursor (spec 03 §4) means nothing on a
touch screen. An application whose only affordance is hover has no touch
behaviour, and this specification does not invent one for it.
6. Dragging
A drag is the one gesture where what the person is doing and what the application is being told are furthest apart. The hand moves continuously; the model changes a handful of times. So the client resolves the whole of the hand — how a press becomes a grab, what is under it, which slot it is in, when a list should scroll because the hand is at its edge — and reports only the changes, in the same way it resolves slop, fling, hover and a scrollbar and reports only what they land on.
The client owns the hand; the server owns the order.
6.1 The gesture
One drag at a time, because there is one pointer and §5 allows one contact.
- Armed. A
pointer_downwhose path reaches a node carryingdrag(spec 03 §3.4) arms the gesture. Nothing is emitted and nothing is visible to the server: an arming press that turns out to be a click MUST be indistinguishable from one that never armed. - Grabbed. The gesture becomes a drag when the pointer leaves the slop —
the same distance §5 uses, about 8 logical px — or at once if the press
landed on a
drag_handle, or after the hold in §5.1, or from the keyboard (spec 03 §3).drag_startis emitted once, to the nearest handler above the source: the node carryingdrag. - Over. While the drag runs the client resolves, each frame, the node
under the pointer and the slot within it, and emits
drag_overto the nearest handler above that target — but only when the pair has changed since the last one sent. A drag that crosses no boundary is silent. - Dropped. The lift emits
droponce, to the target, carrying the slot it landed in. - Cancelled.
Escape, a cancelled contact (§5 step 7), the window losing the input, or the source leaving the tree all end the drag with adropcarryingslot = -1. A cancel is an ordinary drop with a sentinel, so a server that handlesdropand nothing else is correct and complete. It is reported to the source; when the source is what went missing, to the container last reported over, which is still there and is the node that would have received the drop. When neither is left the drag ends and nothing is emitted — the server took the row away itself, and already knows.
Dispatch is §2's and unchanged — the nearest handler on the path, no bubbling. What is new is that a second, independent walk finds the props. The prop says what a node is; the handler says who hears. A row is draggable and the list is what hears the drop, and a container with no handler is not a target however it is marked.
6.2 The slot
slot is the position the thing in the hand would take, counted among the
container's draggable items and not among its children, so a header, a
footer or a divider is skipped and the number indexes the records the server
holds rather than the nodes it sent. For a windowed list (spec 04 §7.1) it is
a row index, which is the same quantity for a container whose children are its
rows.
The rule is the slot whose box contains the pointer, clamped to the ends — not the nearest boundary. The difference matters as soon as the server previews the move by making it: the thing in the hand is then under the pointer, so the slot does not change again until the pointer genuinely leaves that box, and the oscillation a midpoint rule produces cannot happen. It also needs no hysteresis for rows of unequal height.
A client MUST NOT resolve a target inside the source's own subtree. Dropping a folder into itself is not a move, and a panel that follows the pointer (spec 04 §5) is never hit-tested at all.
6.3 What the client does not do
- It does not move anything. The tree changes when the server says so. A
client that reordered optimistically would have to reconcile two orders, and
a
MoveChild(spec 02 §5) costs four bytes. - It does not invent a ghost. What the hand carries is the thing itself:
the server answers each
drag_overby making the move, so the item travels and the list is its own preview. An application that wants something under the cursor as well declares it — anoverlaywithposition: pointer(spec 04 §5), which the client keeps under the hand without laying anything out again, revealed by thedrag_starthandler's own local chunk so that it is up before the server has answered. A chunk cannot read the pointer and does not need to: it reveals, and the client places. The end of the gesture takes that reveal back. A local-then-server chunk's effects are provisional (spec 07 §6), and the client MUST undo the ones its owndrag_startmade when the drag ends, by either route of §6.1 and whether or not anything was emitted — a drop with no handler above it, or one the server answers with no diff, sends back no batch, and an application that had to wait for one would leave the ghost under the hand for ever. - It does not announce. Announcing is prose, prose is content, and content
is the server's. What the client owes is that focus stays on the moved node,
that
pos_in_setandset_size(spec 03 §6.1) stay true, and that the node is brought into view. - It does not decide whether the drop is allowed. Groups match so the client knows which containers to light and which shape to draw. §4 still holds: the server re-derives everything.
6.4 Scrolling under the hand
A drag held near the edge of a scroller scrolls it, because the alternative is
a list you cannot reach the bottom of without letting go. Within
min(48 px, 0.2 × the viewport on that axis) of an edge the client scrolls
towards it, from nothing at the band's inner edge to a client-chosen maximum at
the outer. The fraction matters: a short list must not scroll from its middle.
The scroller is the one §3's scrolling keys would have chosen — the one under
the pointer that can still move that way, else its ancestor that can. Rows move
under a pointer that is standing still, so the client re-resolves the slot
after each step, and §6.1's change rule keeps that to one event per boundary
crossed. One scroll event is emitted when the movement stops, as a glide's is
and for the same reason.
Rendered from spec/06-events.md in the repository. The
normative text is spec/; where it and a prose page
disagree the spec wins, and that is a bug worth reporting.
Edit this page