Decision
Anything Cordial draws over the running engine — the text editor on a focusedTextBox, a web-view dialog, and every plugin overlay ADR-027 proposes — is
three independent pieces of compositor state, not one:
- Stacking. Is the engine’s
wl_subsurfaceabove or below the GTK toplevel?WaylandWindow::set_engine_stacking. - Paint. Does GTK paint over the canvas rectangle, and does the parent
surface claim to be opaque there?
HostWindow::set_canvas_see_throughandrefresh_opaque_region. - Input. Does the parent’s input region cover the canvas rectangle?
HostWindow::refresh_input_region.
The difference between the two rows is the whole of “can you click past the
overlay into the game”. For an inset overlay, yes, and deliberately — the
editor is a widget on one
TextBox and the rest of the canvas is still the
game’s. For a modal, no, and deliberately — that is what modal means, and
it is the same statement wayland::dialog_in_front already makes on the
event-forwarding side.
And one prohibition, which is the only thing here that is a rule rather than a
choice:
Never make the engine’s canvas input-transparent. Do not call
wl_surface.set_input_region on the subsurface. An empty input region does
not mean “give this click to the parent”; it means no surface here wants it,
and because the parent’s region already has the canvas punched out of it,
nothing in the window claims the point and the compositor gives the click to
the next window down.
Why
The engine owns a surface, so Cordial cannot simply stack a widget over it
ADR-011 put the engine’s canvas in awl_subsurface of Cordial’s GTK toplevel, because ANativeWindow has to be a
real surface and the engine drives its own Vulkan swapchain against it. The
consequence is the thing this ADR exists to write down: the canvas is
composited by the compositor, not laid out by GTK. It is not in the widget
tree. GTK cannot stack anything above it, because from GTK’s point of view
there is nothing there — the canvas slot is a GtkDrawingArea with no draw
function.
So “draw something over the game” is not a widget operation. It is: put the
engine below the toplevel, stop GTK painting over the hole, and decide who
gets the clicks. Three knobs, one intent.
ADR-027 states this too simply and is
corrected by this ADR. It says “a GTK widget placed above it is drawn above it,
by the compositor, with no cooperation from the engine”. The second half is
true and is the good news. The first half is not: the subsurface is placed
above the parent in the ordinary case, so a GTK widget put on top of it is
drawn underneath until somebody lowers the engine. Any plugin overlay built
on that sentence would have rendered nothing and looked like a GTK bug.
Each knob has been shipped alone, and each failure looked like something else
This is the expensive part, and it is why the table above is normative rather than advisory. Stacking alone. Lowering the canvas without touching the opaque region: the compositor was told the parent is opaque over the canvas, so it skipped compositing the subsurface underneath and the window went a flat#222226.
Measured with the toplevel and every descendant forced transparent, still
that exact colour, while the engine presented at sixty frames a second and the
game reappeared the instant the canvas was raised. Every attempt to fix it with
CSS failed, because it was never a painting problem. It looked
state-dependent — it happened to work on the landing page, where moving between
pages resizes often enough that a geometry sync lands soon after the lower.
Paint alone. The opaque region punched but the background left opaque:
canvas completely black. Recorded in set_engine_stacking’s own comment,
because the two have to be paired or the window is see-through with nothing
behind it.
Input, forgotten. The editor’s hole in the parent’s input region was
punched by set_text_overlay and then immediately overwritten by
refresh_input_region running off the geometry sync, roughly twenty times a
second. The hole existed for a few milliseconds at a time, so a click into the
text field essentially never landed — reported as “you cant click to move the
caret … you cant drag to select text”. It survived a first fix that corrected
the hole’s coordinates and left the overwrite in place.
Input, done from the wrong end. 07564e2 gave the canvas an empty input
region while a dialog was up, on the theory that the canvas was eating the
dialog’s clicks. Every click over the canvas then fell through Cordial’s window
and raised whatever was behind it: “as soon as we press anything it selects the
window behind and focuses it … my terminal gets focused”. Reverted in
73c74eb. The prohibition above exists because this is not obviously wrong
from the protocol’s wording, and somebody will reach for it again.
Input, the actual dialog bug. The parent’s region is the whole window minus
the canvas rectangle, and an AdwDialog draws inside that same toplevel,
centred — so its buttons sit inside the hole and every click on them went to
the subsurface rather than to GTK. Reported as “I cant click on the webview’s
items”, diagnosed twice as stacking and once as pointer lock before anybody
read refresh_input_region’s own doc comment, which had the measurement in it
the whole time: “two synthetic clicks produced four nativePassMouseButton
calls into the engine and without it zero”.
Five failures, four of them attributed to the wrong knob at first. That is the
case for writing the three down together.
What this rules out
- An overlay that does not say what it wants from input. There is no default. A new overlay picks Inset or Modal, or it argues for a third shape in a new ADR.
set_input_regionon the engine’s subsurface, for the reason above.- Treating the opaque region and the input region as the same shape. They
are not and must not be: the editor is drawn over the canvas with a
transparent background, so it stays part of the opaque cut-out while being
unioned back into the input region.
host_window.rssays so at the function that would otherwise conflate them. - Deciding any of the three from CSS. Two of them are compositor state that GTK does not model.
The alternative, and why it is not this ADR
The shape that removes most of this is: put the engine’s canvas permanently below the toplevel, leave GTK’s background transparent over it always, and put a real input-accepting widget in the canvas slot that forwards toinput::pass_*. GTK’s own hit-testing then decides what reaches the engine —
a click that lands on no overlay widget falls through to the forwarding widget
and is forwarded; a click on an overlay is GTK’s. Overlays become ordinary
GtkOverlay children and none of the three knobs moves at runtime.
That deletes the restack, the see-through toggle, POINTER_ON_CANVAS,
dialog_in_front, and most of Cordial’s second wl_pointer. It is the right
architecture and it is not adopted here, for three reasons worth stating rather
than leaving as an omission:
-
It has now been spiked, and the spike broke input.
CORDIAL_GTK_INPUTdid exactly what this section describes — canvas below, background transparent, no input hole, pointer forwarded from GTK controllers — and the report was immediate: “gtk input 1 doesnt even work … that one just breaks the input”. Removed the same hour. The finding is the point, and it sizes the real job. Forwarding motion, buttons and scroll is the easy third.POINTER_ON_CANVASis the other two: it gates the drag-side pointer lock insync_pointer_lock, and it gatesrelative_pointer_motionoutright. With no input hole, pointer focus never reaches the engine’s subsurface, so that flag is never true and the camera — relative motion under a lock — stops. A pointer that moves over a game nobody can look around in is a fair description of broken. So this is not three event types. It is the lock, the relative-motion path and the focus flag moving together, with a GTK-side answer to “is the pointer over the game” replacing a Wayland-side one. Anyone attempting it should start there rather than with the forwarding, which is the part that already works. - It moves the riskiest code in the client. Text entry is the one area this project has regressed repeatedly and the one users notice immediately.
- The compositor stops being able to skip work. A permanently transparent parent means the parent can never declare an opaque region over the canvas, which is the hint that lets a compositor avoid compositing what is underneath. Nobody has measured what that costs.