Skip to content

internal/kawa: separate pointer routing, focus, and drawing - #27

Open
DeedleFake wants to merge 8 commits into
masterfrom
input-boundaries
Open

DeedleFake wants to merge 8 commits into
masterfrom
input-boundaries

Conversation

@DeedleFake

@DeedleFake DeedleFake commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Why

One InputMode any value owned the interaction phase, pointer delivery to clients, the cursor image, what got drawn on top, and which window was highlighted. Render and input reached into it with type assertions, so changing how a menu or a resize behaves meant reading input.go, mode.go, render.go, and view.go together. Hit testing and keyboard focus were also decided in several places with slightly different rules.

What changed

Each of those jobs now has one owner in internal/kawa:

  • pointer.go is the seat router. It is the only code that calls seat.PointerNotify*.
  • interaction.go holds the interactions (menu, window picker, Resize and New boxes, move, border resize) behind a small closed interface. They never talk to clients.
  • render.go has an overlay that interactions write (open menu and selection, box, target window) and onFrame only reads.
  • cursor.go is the only code that sets the cursor image.
  • hit.go answers every "what is under the pointer" question with one hitAt that returns a tagged result. Each caller passes the planes it can see.
  • focus.go is the only code that moves the keyboard: focus takes a window or layer surface through a small interface, and clearFocus clears it.

InputMode, its six optional interfaces, and all eight type assertions on it are gone. The New box is now a value in newViews keyed by pid instead of aliased mode memory.

Scope

Behavior is unchanged, including known bugs, which are left for follow-ups. A few of them: the status bar click uses the layout Y, menus all use the main menu's last selection, axis events reach clients during grabs, move and resize end on any release, and the window picker ignores layer surfaces. Each now sits in one owner, usually on one line.

Blast Radius

All pointer, keyboard focus, and drawing paths in internal/kawa. No other packages, dependencies, or the wlr bindings.

Verification

  • Every commit builds and passes go vet, golangci-lint, go fix -diff, and go test. New unit tests cover the border edge test and the New box overlay.
  • A scripted session on Xvfb (hover, border move and resize, every menu path, Resize and New boxes, tiling, a client move with scrolling, a stuck-button release, a fuzzel overlay, a swaybg background) gives identical screenshots, cursor image log, and per-client WAYLAND_DEBUG pointer traces on master and on this branch.
  • Deliberate mutations (sending frames during an interaction, keeping hover state after one ends) make that comparison fail.

moveCursor had no users, and the move mode carried a commented-out
status bar clamp.
Hover, presses, the window picker, and tiled swaps each had their own
query, and presses tested the status bar on their own. hitAt walks the
stack once and returns a tagged hit. Each caller passes the planes that
it can see, so what a caller skips is one constant.
Windows and layer surfaces each had their own focus function, restoring
focus cleared it on its own, and destroying a window picked the next one
with its own rule. focus takes a focusTarget for all three, and destroy
and restoreFocus share topView.
onFrame type-asserted the mode to draw its menu or box and to find the
window to highlight. Modes write an overlay that onFrame only reads.

The New box was mode memory aliased into newViews. newViews holds
values keyed by the pid that exec returns, and the drag updates that
entry for as long as it exists.
InputMode was any, with optional interfaces for motion, buttons, cursor
requests, and drawing, so one object owned the phase, client delivery,
and the cursor image. An interaction is a small closed interface that
never talks to clients. pointer.go is the only code that calls
seat.PointerNotify*, and cursor.go is the only code that sets the cursor
image.
focusTarget was a struct of optional fields that focus switched on to
find out what it was focusing. Windows, the surfaces inside them, and
layer surfaces implement it now, each deciding whether it can take the
keyboard and what changes once it has it. Clearing the keyboard has no
target, so it's clearFocus.
onDestroyView read overlay.target to decide whether to end the
interaction, so window lifecycle depended on what gets drawn. begin
records the window an interaction acts on next to it, and overlay.target
is only the highlight.
A window's focusSurface saved prevFocus when an exclusive layer
surface blocked it, so asking a target for its surface could change
state. Targets now only report their surface and window, and focus
makes every decision: the exclusive layer check, saving prevFocus,
and whether the target already has the keyboard.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant