6.2 KiB
TASK 37 — "Back" as a first-class verb
Status: open, opened 2026-07-27. Prompted by @wiredwireless's xdg_navigation
sketch (Matrix, 2026-07-24) and Casey's ask to work out how it fits here.
Size: our half is small and worth doing now; the protocol half is upstream's
problem and should not block us.
The outside idea, in one line
An app declares "my current view has somewhere to go back to"; the compositor decides how the user expresses "back" (edge swipe, key, mouse button 8, accessibility) and invokes it. Later: gesture progress with commit/cancel, so the app can drag the outgoing page as Android's predictive back does.
Verdict: the concept is sound. The framing needs work before it ships anywhere.
The idea is right and the pieces exist. Three things would decide whether it survives contact with freedesktop, and they are worth getting right regardless of whether we ever send it upstream.
1. The killer objection is "just send XF86Back." Apps already handle that
keysym and mouse button 8. A compositor can synthesise it today, with no
protocol at all. Any proposal that leads with the discrete back() call walks
straight into this and loses.
2. The answer is can_go_back — the state channel, not the verb. A
synthesised key is fire-and-forget: the compositor learns nothing about whether
anything happened. It therefore cannot decide what to do when the app has
nowhere to go — close the window? switch workspace? nothing? — so in practice
it does nothing, and "back" is unreliable, which is exactly why it never caught
on. Declared availability is the new capability, and it is what lets the
compositor own the fallback policy while the app owns only its own stack.
Android is the precedent, and it cuts in this proposal's favour: onBackPressed()
was the key-shaped model, and Android 13 replaced it with OnBackInvokedCallback
— a registered callback with declared availability — precisely because the
key model could not express delegation or predictive back. The platform being
cited already tried the cheap version and migrated off it.
3. Lead with predictive back, not with back. Progress + commit + cancel cannot ride a key event under any encoding. That makes it a mechanism argument ("here is a thing input events structurally cannot do") rather than a policy argument ("here is a nicer way to do a thing you can already do"). Discrete back then falls out as the degenerate case: progress 0 → 1 in one step. Mechanism arguments are the register Wayland review accepts; policy arguments are the ones that summon the metaphysics thread.
Two design questions to answer before posting, not after
- What happens at the root, when
can_go_backis false. This is where the "what does back mean" fight actually lives, and the design already answers it — the compositor knows, so the compositor decides (close / switch / do nothing), and no app ever needs an opinion. Saying so up front removes a whole class of objection. - Scope. One verb, one state, one progress channel. No
forward, noup, nohome. Each extra verb multiplies the semantic argument and none of them have back's near-universal agreement. Also: this belongs onxdg_toplevel(precedent: xdg-dialog, xdg-toplevel-icon), not on barewl_surface, and dialogs/popups need a stated answer.
How it fits here — we have already built the compositor half, twice
This is not an analogy; the shapes line up exactly.
services/Gestures.qmlis the routing table in the sketch. It already maps gesture → named semantic action (squeezeAction,edgeAction, abind(gesture, action)IPC) rather than gesture → hardcoded behaviour, whichINTERFACE-ARCHITECTURE§4 requires of every input. Addingbackas an action is one table entry.can_go_backis the dial'senabled, and its refusal reason is the dial'sreason. TASK-30's verb tables and TASK-31's radial dial already carry "this verb exists but is not available right now, and here is why, in the words the refusal used". A back verb is that shape with a one-bit state. Whatever we build for back should feed the same table, or we will have three places that enumerate capabilities instead of one — the exact drift TASK-31 was written to prevent.
What we do not have is the app half, and that is the whole point of doing this ourselves: our first-party apps are ours. culver already pushes a stack on narrow layouts (contacts, conversation) and pops it; souveraine-settings is a page tree; the player has views. Every one of them already knows its own answer to "where does back go" and has no way to say so.
The move
Implement the semantics over our own IPC now, shaped like the protocol so a later swap is a transport change and not a redesign:
- A
navigationservice in the shell holding, per focused app:canGoBack, andback()/backProgress(f)/backCommit()/backCancel(). - First-party apps report their stack depth into it. culver first — it is the app where the missing gesture is felt daily.
backbecomes a gesture-table action like any other (TASK-13's squeeze, the pill, an edge swipe), never a hardcoded binding.- Predictive back last, and only after the discrete version feels right.
Third-party apps (Firefox, GTK apps) cannot participate and will not until
something lands upstream. They get the legacy XF86Back key. This is a
declared two-tier policy, not a silent fallback: the compositor knows exactly
which tier a surface is in, and in tier 2 it does not claim to know whether
anything happened. It must never present tier-2 behaviour as if it were tier 1.
The by-product matters as much as the feature: doing this first means that if the idea does go to freedesktop, it arrives as a working implementation with a device behind it, not as a proposal. That is the difference between being argued with and being eaten.
Connects to
TASK-30 (verb tables — back is a verb and belongs in them), TASK-31 (the dial —
same enabled/reason shape, same routing table), TASK-13 (squeeze — another
gesture wanting a semantic destination), INTERFACE-ARCHITECTURE.md §4,
TASK-32 (pill and OSK as apps — the pill is one place a back gesture lands).