The Android console stops losing the pad, owns its Controllers page, and takes the whole phone panel #317

Merged
enricobuehler merged 4 commits from worktree-phone-console-ui into main 2026-08-18 21:50:59 +00:00
Owner

First adaptation pass of the shared Skia console on Android: the obvious bugs, then the phone-sized UI. Four commits, in dependency order — each builds on the previous, so they ship as one PR.

fix(android): the console's pad probes survive the Controllers/Licenses pages (a64a22cc)

The MainActivity pad probes were one last-writer-wins slot. The Skia shell installs its probes once; a Compose screen the console opened over itself (Controllers, Licenses) overwrote that slot and nulled it on the way out — the shell never re-installed, so every gamepad press after closing the page was silently dropped until the process died. The slot is now a stack: push on install, remove by identity on dispose, dispatch consults the top. Every ordering Compose produces (cross-fades composing both screens at once, non-LIFO disposal) now self-heals.

fix(android): a console that cannot draw yields to the touch UI (45453103)

Connecting a controller could swap in the console over a SurfaceView nothing would ever paint — native create failed, render thread died, or a GL context Android reclaimed — leaving a gray screen for the rest of the process (Kotlin only logged the Dead event). SkiaConsole.healthy is now observable state folded into the gamepad-UI gate (touch UI takes over), and the native render loop ends itself through the Quit release order after 3 consecutive GL setup failures instead of hot-spinning gray.

feat(console-ui): Connected controllers is the console's own screen (9cefa0a3)

The Kotlin ConsoleControllersScreen takeover is replaced by a native screen in pf-console-ui — the console keeps its own input on the page (which is what made the probe eviction so common). Only the five platform-only actions go back to the host as one ConsoleCmd::PadAction { action, pad_key }: rumble, SC2 Bluetooth/USB grants, DualSense USB grant, DualSense haptics self-test. PlatformScreen::Controllers is deleted; the D7 suspension mechanism survives for Licenses. The row stays Android-only; desktop ignores PadAction. Deliberately deferred (recorded as a ponytail: comment): the live input test stays on the touch screen — the console's aggregated MenuSample is nowhere near a per-device axis/trigger readout; porting it means widening the pad-sample bridge.

feat(console-ui): the console takes the whole phone panel (d59a1a96)

  • System bars hidden while the console fronts the app (transient by swipe, restored on dispose) — also the safe-area fix: hidden bars report zero insets, so scrolled content no longer slices off at the visible gesture-bar line.
  • Phone density floor of the design-unit scale raised 0.6 → 0.75 (phones-only: the couch term wins on tablets/TVs); still the documented on-glass knob.
  • The library's sort/view bar appears only while it holds the pad (▲ / "Sort & view" in the legend), Apple-client style; the field takes the band back as it fades.
  • The store/platform subtitle under the focused title is gone (the cover badge already says it); detail band 84 → 64 units.
  • Grid guard: the two-column minimum shrinks covers to fit rather than clipping when a narrow viewport × high density leaves less width than two full cells.

Verification

  • cargo ndk clippy -p punktfunk-client-android -- -D warnings (arm64): clean at each commit.
  • ./gradlew :app:testDebugUnitTest :kit:testDebugUnitTest -PskipRustBuild: green at each commit (incl. Roborazzi scenes; the console-controllers scene removed with its screen).
  • Honest caveat: pf-console-ui is cfg-gated to linux/windows/android, so its new unit tests (controllers screen, library) are compile-checked on macOS via the NDK clippy and first execute in Linux CI — watch the ci workflow on this PR.
  • On-glass: debug build installed to a Nothing Phone for the phone-UI pass.
First adaptation pass of the shared Skia console on Android: the obvious bugs, then the phone-sized UI. Four commits, in dependency order — each builds on the previous, so they ship as one PR. ## fix(android): the console's pad probes survive the Controllers/Licenses pages (`a64a22cc`) The MainActivity pad probes were one last-writer-wins slot. The Skia shell installs its probes once; a Compose screen the console opened over itself (Controllers, Licenses) overwrote that slot and nulled it on the way out — the shell never re-installed, so every gamepad press after closing the page was silently dropped until the process died. The slot is now a **stack**: push on install, remove **by identity** on dispose, dispatch consults the top. Every ordering Compose produces (cross-fades composing both screens at once, non-LIFO disposal) now self-heals. ## fix(android): a console that cannot draw yields to the touch UI (`45453103`) Connecting a controller could swap in the console over a SurfaceView nothing would ever paint — native create failed, render thread died, or a GL context Android reclaimed — leaving a gray screen for the rest of the process (Kotlin only logged the `Dead` event). `SkiaConsole.healthy` is now observable state folded into the gamepad-UI gate (touch UI takes over), and the native render loop ends itself through the Quit release order after 3 consecutive GL setup failures instead of hot-spinning gray. ## feat(console-ui): Connected controllers is the console's own screen (`9cefa0a3`) The Kotlin `ConsoleControllersScreen` takeover is replaced by a native screen in `pf-console-ui` — the console keeps its own input on the page (which is what made the probe eviction so common). Only the five platform-only actions go back to the host as one `ConsoleCmd::PadAction { action, pad_key }`: rumble, SC2 Bluetooth/USB grants, DualSense USB grant, DualSense haptics self-test. `PlatformScreen::Controllers` is deleted; the D7 suspension mechanism survives for Licenses. The row stays Android-only; desktop ignores `PadAction`. Deliberately deferred (recorded as a `ponytail:` comment): the **live input test** stays on the touch screen — the console's aggregated `MenuSample` is nowhere near a per-device axis/trigger readout; porting it means widening the pad-sample bridge. ## feat(console-ui): the console takes the whole phone panel (`d59a1a96`) - System bars hidden while the console fronts the app (transient by swipe, restored on dispose) — also the safe-area fix: hidden bars report zero insets, so scrolled content no longer slices off at the visible gesture-bar line. - Phone density floor of the design-unit scale raised 0.6 → 0.75 (phones-only: the couch term wins on tablets/TVs); still the documented on-glass knob. - The library's sort/view bar appears only while it holds the pad (▲ / "Sort & view" in the legend), Apple-client style; the field takes the band back as it fades. - The store/platform subtitle under the focused title is gone (the cover badge already says it); detail band 84 → 64 units. - Grid guard: the two-column minimum shrinks covers to fit rather than clipping when a narrow viewport × high density leaves less width than two full cells. ## Verification - `cargo ndk clippy -p punktfunk-client-android -- -D warnings` (arm64): clean at each commit. - `./gradlew :app:testDebugUnitTest :kit:testDebugUnitTest -PskipRustBuild`: green at each commit (incl. Roborazzi scenes; the console-controllers scene removed with its screen). - Honest caveat: `pf-console-ui` is cfg-gated to linux/windows/android, so its new unit tests (controllers screen, library) are compile-checked on macOS via the NDK clippy and first *execute* in Linux CI — watch the `ci` workflow on this PR. - On-glass: debug build installed to a Nothing Phone for the phone-UI pass.
enricobuehler added 4 commits 2026-08-18 21:33:23 +00:00
The MainActivity pad probes were one last-writer-wins slot. The Skia shell
installs its probes once (its effect keys never change); a Compose screen the
console opens over itself (Controllers, Licenses) overwrote that slot, and on
its way out nulled it — the shell never re-installed, so every gamepad press
after closing the page was silently dropped until the process died.

The slot is now a stack: each holder pushes its claim on install and removes
it BY IDENTITY on dispose, and dispatch consults the top. Whatever ordering
Compose produces — cross-fades composing both screens at once, non-LIFO
disposal — a leaving screen takes only its own entry, and the one underneath
resurfaces the moment it pops.
Connecting a controller could swap in the console shell over a SurfaceView
nothing would ever paint: the native create failing, the render thread dying,
or a GL context Android reclaimed all left the app on a gray screen for the
rest of the process — Kotlin only logged the Dead event.

SkiaConsole now exposes an observable [healthy] flag (false on create failure
or a Dead event) and App folds it into the gamepad-UI gate, so the touch UI
takes over. On the native side, a run of consecutive GL setup failures
(window surface / Skia wrap) — previously logged and retried forever, a hot
spin with a live surface — now ends the render thread through the same
release order as Quit, which raises Dead and hands the screen back.
The Controllers row used to raise the D7 platform-screen mechanism: Android drew
the Compose ConsoleControllersScreen over the surface and suspended the console's
input until it closed. Now the page is a shared Skia screen
(screens/controllers.rs) pushed like any other settings sub-screen, so the console
keeps its own navigation, transitions and haptics on it — and a future desktop
build gets it for free (the row itself stays Android-only in row_on).

What genuinely cannot move into Rust stays with the host, asked for by ONE
parameterised command, ConsoleCmd::PadAction { action, pad_key }: the USB and
Bluetooth grant dialogs (sc2_bluetooth / sc2_usb / ds_usb), the rumble pulse on
the real InputDevice, and the DualSense pad-audio self test. SkiaConsoleShell
handles them with the same helpers the touch Controllers screen uses (testRumble,
the grant intents, nativePadAudioSelfTest), reporting through the notice toast, so
the support answer cannot drift between interfaces. PadInfo carries the three
fields the screen needed and the aggregated list already lacked (detail line,
forwarded, rumble), filled by ConsoleJson.pads from the same padInfoOf reader the
touch screen renders from.

PlatformScreen::Controllers is gone; the mechanism itself stays for Licenses,
which still suspends the console's input correctly (the probes gate on platformUp
as before). The Compose console variant and its screenshot scenes are deleted;
the touch ControllersScreen keeps the full page INCLUDING the live input test,
which deliberately did not move — the console only receives the aggregated
MenuSample, nowhere near a per-device axis/trigger readout (ponytail note at the
top of controllers.rs records the upgrade path).
feat(console-ui): the console takes the whole phone panel, and the library stops spending it
ci / rust-arm64 (pull_request) Failing after 2m26s
ci / web (pull_request) Successful in 1m29s
ci / bun-nix (pull_request) Successful in 43s
ci / rust (pull_request) Failing after 3m41s
ci / docs-site (pull_request) Successful in 2m4s
android / android (pull_request) Successful in 8m55s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 2m53s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Failing after 3m34s
d59a1a9606
Four phone-sized fixes, one theme — a small screen was paying for chrome it
never asked about:

- The Android shell hides the system bars while it fronts the app (transient
  by swipe, restored on dispose), the same contract as the stream. This is
  also the safe-area fix: hidden bars report zero insets, so scrolled content
  no longer slices off at the visible gesture-bar line with bare backdrop
  below — only the display cutout remains a real inset.
- The design-unit scale's phone density floor rises 0.6 -> 0.75: on a 460 dpi
  panel the floor is what sets the scale (the couch term only wins on tablets
  and TVs), and 0.6 read a step too small in the hand. Still the documented
  on-glass knob.
- The library's sort/view bar appears only while it holds the pad (up from
  the field / the legend's 'Sort & view'), the Apple client's behaviour; the
  field takes the band's height back as it fades.
- The store/platform subtitle under the focused title is gone — the cover
  badge already says it — and the detail band shrinks 84 -> 64 units, most of
  a grid row on a phone.

Plus a guard the new scale makes necessary: the grid's two-column minimum
shrinks its covers to fit rather than clipping at the edges when a narrow
viewport times a high density leaves less width than two full cells.
enricobuehler merged commit 8a4eac4c41 into main 2026-08-18 21:50:59 +00:00
enricobuehler deleted branch worktree-phone-console-ui 2026-08-18 21:51:07 +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#317