rumb
← logbook

The page answers back

Who owns the keystroke

The rule rumb settled on is that the page is the default keyboard target and the address bar is the exception.

That inverts what the shell did before. The URL bar had been the only thing that ever asked for a keystroke, so it received all of them, and a text field on a page was decoration. Now url_focused is a single boolean, false at startup. It goes true on Cmd/Ctrl+L or a click on the bar, and false again on Enter, which navigates, or Escape, which abandons the edit. Everywhere else, keys go to Servo.

Getting even the old behaviour to fire took a fix worth remembering, because the symptom lied. The bug was reported as “backspace doesn't delete the URL”. The reality was that no keystroke did anything at all: GPUI delivers keyboard events only to an element in the focus chain, and that requires both track_focus(&FocusHandle) on a focusable element and the window having actually focused that handle. The on_key_down callback was never invoked. The shell now holds a focus handle, tracks it on the root, and re-focuses on every render if focus has drifted — window blur, a child element taking it, anything.

Turning a GPUI keystroke into a Servo one

map_key does the translation. A short list of named keys — the arrows, enter, escape, backspace, home and end among them — maps straight across. Anything else becomes a character taken from GPUI's key_char, which has already resolved shift and AltGr, so shift+a arrives as “A” and option+s as “ß”. When key_char is absent — GPUI suppresses it for Ctrl combinations — a single-character key name is used instead, which is how Ctrl+C survives as the character “c”. Anything else returns nothing and is dropped: bare modifiers, and every function key.

A second helper maps GPUI's four modifier booleans onto Servo's bitflags, with GPUI's platform modifier — Cmd on macOS, Super on Linux — becoming META.

Then the event is constructed with KeyboardEvent::new_without_event, and two of its arguments are admissions. GPUI does not give the shell a physical scan code, so code is always Unidentified and location is always standard. Servo drives text input off the key value plus modifiers, so typing works — but a page reading event.code, a game binding WASD by physical position or a shortcut meant to survive a Dvorak layout, gets nothing usable out of rumb. The last two arguments are repeat and is_composing, both hardcoded false, because the shell tracks neither.

Key-up is forwarded too, for pages that listen for it, and skipped while the address bar has focus since the bar acts on key-down only.

Where the pointer really is

Pointer events arrive from GPUI in logical pixels relative to the window. Servo wants a point in the WebView's own space, in device pixels. Two corrections stand between them.

The first is the offset: web content does not start at the top of the window, the chrome does. Before this was handled the mapping passed window-relative y straight through, shifting every pointer event down by exactly the toolbar height. What made it confusing is that the error was not uniform — because the vertical scale used the full window height rather than the content-area height, the error grew linearly with y. Clicks near the top of a page landed roughly where you aimed. Clicks near the bottom of a long page were off by several percent.

The fix is a pair of functions. content_metrics returns the content rectangle for the current layout — the URL bar is always a full-width band at the top, and the tab strip takes either a horizontal band or a vertical column depending on where the user has put it, so the offsets differ per layout. pointer_to_webview subtracts that offset, clamps into the rectangle, and rescales by Servo's render size over the content size.

The rescale is not redundant. GPUI's on-screen quad and Servo's render target can differ — a tiling WM forcing a size, fractional scaling — and when they agree it collapses to an identity multiply. The toolbar height lives in a named constant beside the layout that defines it, which is the only reason it stayed correct when a tab strip was later added above the URL bar and the offset changed.

Cursors, and the shapes GPUI does not have

Servo tells the embedder which pointer it wants through notify_cursor_changed on the WebViewDelegate. The shell stores it in a cell and — this is the part that is easy to miss — flips the dirty flag when the value changed, so a GPUI frame gets scheduled to apply it. Without that, hovering a link on an otherwise static page would set the cursor and nothing would repaint to show it.

cursor_to_gpui maps the CSS keywords onto GPUI's CursorStyle: pointer to a pointing hand, text to an I-beam, the eight resize directions to their equivalents. Two notes from that function. GPUI has no equivalent for wait, progress, help, zoom-in, zoom-out or all-scroll, so those fall back to the arrow — a page showing a busy cursor shows nothing at all in rumb. And the diagonal resize directions are matched by geometry, by what the variant names mean, rather than by GPUI's own doc comments on them, which are mislabelled.

Fullscreen landed in the same commit, on F11 and a toolbar button — fullscreen rather than maximize because every compositor honours it, _NET_WM_STATE_FULLSCREEN on X11 and xdg_toplevel.set_fullscreen on Wayland, including the tiling window managers that ignore a maximize request outright. The button exists because some compositors grab F11 first.

What is still not wired

No IME composition: is_composing is hardcoded false and there is no preedit path at all, so anyone typing Japanese, Chinese or Korean cannot use this browser. No drag and drop into the page, no touch, no gamepad.

Mouse buttons stop at left, right and middle; GPUI's navigate buttons — the side buttons that mean back and forward — are matched and then discarded, pending a decision about whether they belong to the page or to the shell. Wheel events are sent in pixel mode, and when GPUI reports a scroll in lines the shell multiplies by 16 as a stand-in for a line height it does not read from anywhere.

And every shortcut the shell claims, the page can never see. Cmd/Ctrl+T, W, L and R, Ctrl+Tab, Ctrl+Shift+D, F11 and F12 all return before the forwarding code runs, and every other function key is dropped by map_key for want of a mapping. A web application that wants F1 for help, or Ctrl+W as its own binding, is out of luck and has no way to ask.

The address bar is not a real text input either. Clicking it puts the caret at the end and nowhere else: you cannot click into the middle of a URL, and you cannot drag to select part of one.

pointer keys scroll GPUI window owns the drawable Servo WebView owns the page notify_input_event notify_cursor_changed element-relative physical pixels → webview coordinates
Events flow one way into the WebView, and the cursor shape comes back the other way.