The console answers a mouse and a finger, and host cards get a menu #90

Merged
enricobuehler merged 2 commits from worktree-console-tabs-pointer into main 2026-08-07 11:38:43 +00:00
Owner

Two field reports on the shared Linux/Windows console UI (pf-console-ui).

1. "The new settings tabs are only navigateable via a gamepad"

The cause is much wider than the strip. SkiaOverlay::handle_event matched only KeyDown and TextInput, so every mouse button, wheel and touch contact fell straight past the console into the run loop — which routes pointer input exclusively at stream.capture, and that is None while you are browsing. Nothing in the console had ever been clickable or tappable. The tabs are simply where it became noticeable.

Making just the pills answer would not have helped either: the settings screen is opened with X from home, so a mouse could not reach that screen in the first place. Hence a real pointer path rather than a hit-test on the strip.

The keyboard half of the report was half true. The tabs did answer PgUp/PgDn — but HintKey::Shoulders spells "PgUp/PgDn" out only in GlyphStyle::Keyboard, i.e. only when no pad is attached. Plug a controller in and a keyboard user sees shoulder glyphs and has nothing to discover.

What changed

  • Overlay::handle_pointer(PointerInput) carries mouse/touch in swapchain pixels. The run loop does the conversion — it owns the window and therefore the display scale, and SDL reports mouse positions in logical window units while fingers arrive normalised 0..1; mixing those two puts every click off by the HiDPI factor. Only DIRECT touch devices are offered (an indirect trackpad already drives the mouse).
  • Widgets act on the PRESS, not the release. The menu list and both carousels scroll the focused item toward the centre, so the thing you pressed has slid out from under your finger by the time it lifts. Click-on-release would have to chase it, and on a touchscreen — where the finger stays put but the content moves — it would routinely land on the wrong row. There is no drag gesture for press-to-act to compete with.
  • The hint bar became the pointer's button bar. It is already the console's only on-screen statement of what the face buttons do, and a pointer has none — so its Confirm/Back/Secondary/Tertiary pills are clickable on every screen. This is what puts Settings and Library in reach of a mouse at all. Shoulders/Adjust stay legend-only: they name a direction, and the thing they steer is already directly clickable.
  • Hint hit boxes are published only when the shell is settled — mid-transition every layer is slid and scaled inside a save_layer, so recorded rects would not be where the pixels are.
  • Tab / Shift+Tab change section (PgUp/PgDn still do), and the keyboard legend now reads "Tab".
  • Right-click is Back everywhere except at the root, where B quits the launcher and a stray right-click must not. Quitting stays the legend's explicit "Quit".
  • Carousels: pressing the centre tile activates it, pressing any other only brings it front — activating on home means starting a session. The library coverflow overlaps and draws cards nearest the cursor on top, so a press picks by min_by_key(|i| (i - cursor).abs()), not the first rect hit.

2. "No context menu for the host cards"

Every other client hangs Wake / Copy link / Edit / Forget off a host card. The console could add a host and connect to one, and that was all — a renamed machine or a fat-fingered address stayed wrong forever unless you opened a desktop shell.

UP on a saved tile now opens that host's menu: the one direction a horizontal carousel leaves free, and the gesture Android's GamepadHostOptionsDialog already uses (its own comment calls the console its counterpart).

  • ConsoleCmd::UpdateHost edits the store entry in place. Remove-and-re-add would silently drop the fingerprint, the learned MAC, pinned_profiles and the profile binding — a rename is not a re-pair.
  • ConsoleCmd::ForgetHost drops it; if it is still advertising it returns as a discovered, unpaired row, which is the honest state.
  • Forget arms on the first press and fires on the second. The other clients forget outright; a console is driven by a thumbstick from across a room.
  • A pinned profile card offers only Unpin. It is a shortcut, not a second host, and offering to forget the host from it would blur exactly the distinction a pin draws. (A pinned row's key is host_key\0profile_id, so every command strips past the NUL and addresses the host.)
  • "Edit…" uses a new Nav::Replace (pop + push under the push choreography) so Back from the editor doesn't land on a menu describing the host as it was before the edit.
  • Copy link reuses pf_client_core::deeplink::DeepLink::for_host — the desktop shells' own builder, so a link is identical whichever surface hands it to you — and rides out as OverlayAction::CopyText, because the clipboard belongs to SDL and only the run loop's thread touches it.

Verification

All in the pf-lxcheck2 container — this crate compiles to nothing on macOS, so a bare cargo check there is vacuous and proves nothing.

  • plain cargo build and clippy --all-targets, both clean under -D warnings
  • 72 tests pass. Seven are new and cover the report directly: a press on a pill selects that tab, each tab still keeps its own cursor when a pointer switched it, a press on a row focuses and cycles it, a press on empty space is refused, Tab/Shift+Tab walk and wrap the strip, right-click goes back, and UP opens the menu for saved tiles only.
  • a dump_console_screens eyeball pass. It earned its keep again: the new menu's rows sat hard against the pinned title until it got Add Host's explainer line — no test would have caught that.

Still owed

An on-glass pass: a real mouse, and a Deck/Ally touchscreen. Everything above is CPU renders and unit tests.

Two field reports on the shared Linux/Windows console UI (`pf-console-ui`). ## 1. "The new settings tabs are only navigateable via a gamepad" The cause is much wider than the strip. `SkiaOverlay::handle_event` matched only `KeyDown` and `TextInput`, so every mouse button, wheel and touch contact fell straight past the console into the run loop — which routes pointer input *exclusively* at `stream.capture`, and that is `None` while you are browsing. **Nothing in the console had ever been clickable or tappable.** The tabs are simply where it became noticeable. Making just the pills answer would not have helped either: the settings screen is opened with **X from home**, so a mouse could not reach that screen in the first place. Hence a real pointer path rather than a hit-test on the strip. The keyboard half of the report was half true. The tabs *did* answer PgUp/PgDn — but `HintKey::Shoulders` spells "PgUp/PgDn" out only in `GlyphStyle::Keyboard`, i.e. only when **no pad is attached**. Plug a controller in and a keyboard user sees shoulder glyphs and has nothing to discover. **What changed** - `Overlay::handle_pointer(PointerInput)` carries mouse/touch in **swapchain pixels**. The run loop does the conversion — it owns the window and therefore the display scale, and SDL reports mouse positions in *logical* window units while fingers arrive *normalised* 0..1; mixing those two puts every click off by the HiDPI factor. Only DIRECT touch devices are offered (an indirect trackpad already drives the mouse). - **Widgets act on the PRESS, not the release.** The menu list and both carousels scroll the focused item toward the centre, so the thing you pressed has slid out from under your finger by the time it lifts. Click-on-release would have to chase it, and on a touchscreen — where the finger stays put but the content moves — it would routinely land on the wrong row. There is no drag gesture for press-to-act to compete with. - **The hint bar became the pointer's button bar.** It is already the console's only on-screen statement of what the face buttons do, and a pointer has none — so its Confirm/Back/Secondary/Tertiary pills are clickable on every screen. This is what puts Settings and Library in reach of a mouse at all. Shoulders/Adjust stay legend-only: they name a *direction*, and the thing they steer is already directly clickable. - Hint hit boxes are published only when the shell is **settled** — mid-transition every layer is slid and scaled inside a `save_layer`, so recorded rects would not be where the pixels are. - `Tab` / `Shift+Tab` change section (PgUp/PgDn still do), and the keyboard legend now reads "Tab". - Right-click is Back everywhere **except at the root**, where B quits the launcher and a stray right-click must not. Quitting stays the legend's explicit "Quit". - Carousels: pressing the **centre** tile activates it, pressing any other only brings it front — activating on home means starting a session. The library coverflow overlaps and draws cards nearest the cursor on top, so a press picks by `min_by_key(|i| (i - cursor).abs())`, not the first rect hit. ## 2. "No context menu for the host cards" Every other client hangs Wake / Copy link / Edit / Forget off a host card. The console could add a host and connect to one, and that was all — a renamed machine or a fat-fingered address stayed wrong forever unless you opened a desktop shell. **UP on a saved tile** now opens that host's menu: the one direction a horizontal carousel leaves free, and the gesture Android's `GamepadHostOptionsDialog` already uses (its own comment calls the console its counterpart). - `ConsoleCmd::UpdateHost` edits the store entry **in place**. Remove-and-re-add would silently drop the fingerprint, the learned MAC, `pinned_profiles` and the profile binding — a rename is not a re-pair. - `ConsoleCmd::ForgetHost` drops it; if it is still advertising it returns as a discovered, unpaired row, which is the honest state. - **Forget arms on the first press and fires on the second.** The other clients forget outright; a console is driven by a thumbstick from across a room. - A pinned profile card offers only **Unpin**. It is a shortcut, not a second host, and offering to forget the host from it would blur exactly the distinction a pin draws. (A pinned row's key is `host_key\0profile_id`, so every command strips past the NUL and addresses the host.) - "Edit…" uses a new `Nav::Replace` (pop + push under the push choreography) so Back from the editor doesn't land on a menu describing the host as it was *before* the edit. - Copy link reuses `pf_client_core::deeplink::DeepLink::for_host` — the desktop shells' own builder, so a link is identical whichever surface hands it to you — and rides out as `OverlayAction::CopyText`, because the clipboard belongs to SDL and only the run loop's thread touches it. ## Verification All in the `pf-lxcheck2` container — this crate compiles to nothing on macOS, so a bare `cargo check` there is vacuous and proves nothing. - plain `cargo build` **and** `clippy --all-targets`, both clean under `-D warnings` - 72 tests pass. Seven are new and cover the report directly: a press on a pill selects that tab, each tab still keeps its own cursor when a *pointer* switched it, a press on a row focuses and cycles it, a press on empty space is refused, Tab/Shift+Tab walk and wrap the strip, right-click goes back, and UP opens the menu for saved tiles only. - a `dump_console_screens` eyeball pass. It earned its keep again: the new menu's rows sat hard against the pinned title until it got Add Host's explainer line — no test would have caught that. ## Still owed An on-glass pass: a real mouse, and a Deck/Ally touchscreen. Everything above is CPU renders and unit tests.
enricobuehler added 2 commits 2026-08-07 11:32:09 +00:00
Two gaps, both found on the shared Linux/Windows console UI.

**The settings tabs only moved for a gamepad.** They were bound to the shoulder
buttons and to PgUp/PgDn, and the legend spells PgUp/PgDn out only when NO pad is
attached — so with a controller plugged in a keyboard user had nothing to find,
and a mouse or a touchscreen could not change section at all.

The root cause was wider than the strip: `SkiaOverlay::handle_event` matched only
`KeyDown` and `TextInput`, so every mouse button, wheel and touch contact fell
past the console into the run loop, which routes pointer input exclusively at
`stream.capture` — `None` while you are browsing. Nothing in the console had ever
been clickable. Making just the pills answer would not have helped either: the
settings screen is opened with X from home, so a mouse could not reach it.

So the console gets a real pointer path:

- `Overlay::handle_pointer` carries mouse/touch in SWAPCHAIN PIXELS. The run loop
  converts (it owns the window, hence the display scale, and mouse coordinates are
  logical while fingers are normalised); the console then hit-tests the very rects
  it drew last frame. Only DIRECT touch devices are offered — an indirect trackpad
  already drives the mouse.
- Widgets act on the PRESS, not the release. The list and both carousels scroll the
  focused item toward the centre, so what you pressed has slid out from under your
  finger by the time it lifts; press-to-act has no such race and there is no drag
  gesture to compete with.
- The hint bar became the pointer's button bar. It is already the console's only
  on-screen statement of what the face buttons do, and a pointer has none — so its
  Confirm/Back/Secondary/Tertiary pills are clickable on every screen, which is what
  puts Settings and Library within reach of a mouse at all.
- Tab / Shift+Tab change section; PgUp/PgDn still do, and the keyboard legend now
  reads "Tab".
- Right-click is Back everywhere, EXCEPT at the root: B there quits the launcher and
  a right-click is far easier to fire by accident. Quitting stays explicit.

**Host cards had no menu.** Every other client hangs Wake / Copy link / Edit /
Forget off a host card; the console could add a host and connect to one, and that
was all — so a renamed machine or a fat-fingered address stayed wrong forever
unless you opened a desktop shell. UP on a saved tile now opens that host's menu,
the same gesture the Android console uses, on the one direction a horizontal
carousel leaves free.

- `ConsoleCmd::UpdateHost` edits the stored host IN PLACE. Removing and re-adding
  would silently drop the fingerprint, the learned MAC, the pinned cards and the
  profile binding — that is a rename, not a re-pair.
- `ConsoleCmd::ForgetHost` drops it; if it is still advertising it returns as a
  discovered, unpaired row, which is the honest state.
- Forget arms on the first press and fires on the second. The other clients forget
  outright; a console is driven by a thumbstick from across a room.
- A pinned profile card offers only Unpin. It is a shortcut, not a second host, and
  offering to forget the host from it would blur exactly the distinction a pin draws.
- "Edit…" REPLACES the menu on the stack rather than stacking over it, so Back from
  the editor doesn't land on a menu describing the host as it was before the edit.

Verified in the pf-lxcheck2 container (this crate compiles to nothing on macOS —
a bare `cargo check` there is vacuous): plain build and `clippy --all-targets`
clean under `-D warnings`, 72 tests pass. Seven are new, and cover the reported
bug directly — a press on a pill selects that tab, and each tab still keeps its
own cursor when a pointer is what switched it.
fix(console/host menu): the actions get their explainer line and room to breathe
ci / bun-nix (pull_request) Successful in 31s
ci / web (pull_request) Successful in 1m11s
ci / docs-site (pull_request) Successful in 1m39s
ci / rust-arm64 (pull_request) Successful in 1m41s
apple / swift (pull_request) Successful in 1m36s
apple / screenshots (pull_request) Skipped
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 3m2s
android / android (pull_request) Successful in 3m37s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m11s
ci / rust (pull_request) Successful in 5m6s
d49f955171
The rows sat hard against the pinned title — the menu is the one form screen
with no subtitle, so the list started at the very top of the content band. It
now wears Add Host's explainer, which both says what the menu is for and is the
air that keeps the first row off the title. A pinned card explains what unpinning
does and does not touch, the same wording the Android dialog uses.

Caught by the screenshot pass, not by a test.
enricobuehler merged commit 31bb3ca7f7 into main 2026-08-07 11:38:43 +00:00
enricobuehler deleted branch worktree-console-tabs-pointer 2026-08-07 11:38:48 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#90