Gamepad UI: section tabs, background palettes, and a backdrop that moves everywhere #66

Merged
enricobuehler merged 1 commits from worktree-gamepad-ui-polish into main 2026-08-06 10:50:32 +00:00
Owner

Polish pass over all three gamepad UIs (pf-console-ui for Linux/Windows, Apple, Android).

Section tabs

The console settings were one 30-row scroll, which on a Deck meant thumbing past Video and Audio to reach the pad settings. They are now Stream · Video · Audio · Controller · Interface · Profiles, plus Input on the desktop console, which alone carries the touch/mouse rows. L1/R1 walks them, each section remembers where its cursor was, and the names are the same word on every client so a setting is where you looked for it last. The in-list group headers that just repeated the section name are gone.

Shoulders are not the only route, because a D-pad remote hasn't got any:

  • Android — Up from the first row moves onto the strip (left/right walks sections there, A drops back in). Pills are tappable too.
  • tvOS — the pills are focusable Buttons, so the focus engine handles it. A Siri Remote has no extended gamepad profile and never reaches GamepadMenuInput's poll at all.
  • Desktop console — needs neither; PageUp/PageDown already map to JumpBack/JumpForward.

Six background palettes

New "Background" row in Interface: Violet (the brand default), Tide, Forest, Ember, Rose, Graphite.

A palette is a hue rotation + a saturation scale over the one colour field each client already draws — not a second hand-tuned field. So every palette inherits the field's structure (dark corners, bright drifting pools) and Violet is the identity transform: existing installs see exactly what they see today. The backdrop recolours live as you step the row, which is why the picker is on a screen rather than in a dialog.

Stored under one shared ui_palette key (trust::Settings, DefaultsKey.uiPalette, Android Settings). Presentation only, so it's a device preference and deliberately never part of a stream profile. An unknown value reads as Violet on every client — a newer client may ship a palette an older one doesn't know.

⚠️ The tint maths is ported three times (Rust / Swift / Kotlin) and each language pins the same four assertions, so the three can't drift. The sin term must be cross(k, c) = (b-g, r-b, g-r)/√3; the opposite sign rotates hue backwards (Ember came out green the first time).

A backdrop that moves everywhere

Settings, add-host and pair used to sit on a still gradient on all three clients. They now wear the same living field at a calm mix — pools dimmed onto the palette's own corner colour, vignette halved so rows that run to the edges don't get crushed. On the desktop console that collapsed the old aurora-over-static crossfade into one shader pass with a chased uniform (theme::draw_form_background is gone).

Motion speed is identical in both modes on purpose: changing it would make the field jump mid-transition. Nothing in the gamepad UI is backed by a static image now, and Reduce Motion (Apple) / "remove animations" (Android, new) still freeze it.

Coverage

The settings screen had no raster coverage at all — the eyeball dump is #[ignore]d — so every_settings_tab_rasters now draws every tab on the CPU, and the Android screenshot set gains a console-settings scene. Both earned their keep immediately: the real renders showed the extra hint pushing "Done" off a 360 dp phone (the legend scrolls now, and the Section cell only appears where shoulders exist) and the form backdrop crushing its own edges.

Verification

Surface Result
console cargo build + clippy --all-targets -D warnings + 170 tests (pf-lxcheck2, amd64)
Apple swift build + 199 tests + a real arm64-apple-ios17.0 typecheck
Android :app:compileDebugKotlin + 61 tests
fmt cargo fmt --all clean

Plus eyeball passes on actual renders (console dump_console_screens PNGs: home, settings, the Interface tab, and Ember; Android Roborazzi phone-console-settings).

Not done

  • On-glass pass owed — Deck, a TV box, tvOS focus.
  • tvOS #if branches are typecheck-unverified from a Mac (needs a tvOS Rust slice: nightly + -Zbuild-std, tier 3). They mirror the .focused($x, equals:) pattern GamepadMenuList already ships on tvOS.
  • The focus/accent tint stays brand violet under every palette — the ask was background palettes. On Ember that reads violet-on-copper; an easy follow-up if it looks wrong in the hand.
Polish pass over all three gamepad UIs (`pf-console-ui` for Linux/Windows, Apple, Android). ## Section tabs The console settings were one 30-row scroll, which on a Deck meant thumbing past Video and Audio to reach the pad settings. They are now **Stream · Video · Audio · Controller · Interface · Profiles**, plus **Input** on the desktop console, which alone carries the touch/mouse rows. L1/R1 walks them, each section remembers where its cursor was, and the names are the same word on every client so a setting is where you looked for it last. The in-list group headers that just repeated the section name are gone. Shoulders are not the only route, because **a D-pad remote hasn't got any**: - **Android** — Up from the first row moves onto the strip (left/right walks sections there, A drops back in). Pills are tappable too. - **tvOS** — the pills are focusable Buttons, so the focus engine handles it. A Siri Remote has no extended gamepad profile and never reaches `GamepadMenuInput`'s poll at all. - **Desktop console** — needs neither; PageUp/PageDown already map to `JumpBack`/`JumpForward`. ## Six background palettes New "Background" row in Interface: **Violet** (the brand default), **Tide**, **Forest**, **Ember**, **Rose**, **Graphite**. A palette is a hue rotation + a saturation scale over the *one* colour field each client already draws — not a second hand-tuned field. So every palette inherits the field's structure (dark corners, bright drifting pools) and **Violet is the identity transform**: existing installs see exactly what they see today. The backdrop recolours live as you step the row, which is why the picker is on a screen rather than in a dialog. Stored under one shared **`ui_palette`** key (`trust::Settings`, `DefaultsKey.uiPalette`, Android `Settings`). Presentation only, so it's a device preference and deliberately never part of a stream profile. An unknown value reads as Violet on every client — a newer client may ship a palette an older one doesn't know. ⚠️ **The `tint` maths is ported three times** (Rust / Swift / Kotlin) and each language pins the same four assertions, so the three can't drift. The `sin` term must be `cross(k, c)` = `(b-g, r-b, g-r)/√3`; the opposite sign rotates hue backwards (Ember came out green the first time). ## A backdrop that moves everywhere Settings, add-host and pair used to sit on a **still** gradient on all three clients. They now wear the same living field at a `calm` mix — pools dimmed onto the palette's own corner colour, vignette halved so rows that run to the edges don't get crushed. On the desktop console that collapsed the old aurora-over-static crossfade into **one shader pass** with a chased uniform (`theme::draw_form_background` is gone). Motion speed is identical in both modes on purpose: changing it would make the field jump mid-transition. Nothing in the gamepad UI is backed by a static image now, and Reduce Motion (Apple) / "remove animations" (Android, new) still freeze it. ## Coverage The settings screen had **no raster coverage at all** — the eyeball dump is `#[ignore]`d — so `every_settings_tab_rasters` now draws every tab on the CPU, and the Android screenshot set gains a `console-settings` scene. Both earned their keep immediately: the real renders showed the extra hint pushing "Done" off a 360 dp phone (the legend scrolls now, and the Section cell only appears where shoulders exist) and the form backdrop crushing its own edges. ## Verification | Surface | Result | | --- | --- | | console | `cargo build` + `clippy --all-targets -D warnings` + **170 tests** (`pf-lxcheck2`, amd64) | | Apple | `swift build` + **199 tests** + a real `arm64-apple-ios17.0` typecheck | | Android | `:app:compileDebugKotlin` + **61 tests** | | fmt | `cargo fmt --all` clean | Plus eyeball passes on actual renders (console `dump_console_screens` PNGs: home, settings, the Interface tab, and Ember; Android Roborazzi `phone-console-settings`). ## Not done - **On-glass pass owed** — Deck, a TV box, tvOS focus. - **tvOS `#if` branches are typecheck-unverified** from a Mac (needs a tvOS Rust slice: nightly + `-Zbuild-std`, tier 3). They mirror the `.focused($x, equals:)` pattern `GamepadMenuList` already ships on tvOS. - The focus/accent tint **stays brand violet under every palette** — the ask was background palettes. On Ember that reads violet-on-copper; an easy follow-up if it looks wrong in the hand.
enricobuehler added 1 commit 2026-08-06 10:42:42 +00:00
feat(clients/gamepad-ui): section tabs, background palettes, and a backdrop that moves everywhere
ci / web (pull_request) Successful in 1m17s
ci / docs-site (pull_request) Successful in 1m42s
ci / rust-arm64 (pull_request) Successful in 2m36s
android / android (pull_request) Successful in 3m33s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 8m37s
ci / rust (pull_request) Successful in 8m58s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 3m47s
apple / swift (pull_request) Successful in 1m29s
apple / screenshots (pull_request) Skipped
5a7f7f0fc5
The console settings were one 30-row scroll, which on a Deck meant thumbing past
Video and Audio to reach the pad settings. They are now split across sections —
Stream · Video · Audio · Controller · Interface · Profiles, plus Input on the
desktop console, which alone carries the touch/mouse rows. L1/R1 walks them,
each section remembers where its cursor was, and the names are the same word on
every client so a setting is where you looked for it last.

Shoulders are not the only route, because a D-pad remote hasn't got any: on
Android, Up from the first row moves onto the strip (left/right walks sections
there, A drops back in), and on tvOS the pills are focusable, so the focus
engine handles it — a Siri Remote has no extended gamepad profile and never
reaches the input poll at all. The desktop console needs neither; PageUp and
PageDown already map to the same events.

New "Background" row, six palettes: Violet (the brand default), Tide, Forest,
Ember, Rose, Graphite. A palette is a hue rotation plus a saturation scale over
the ONE colour field each client already draws, so every palette inherits its
structure and Violet is the identity transform — existing installs see exactly
what they see today. The maths is ported three times (Rust/Swift/Kotlin) under
one shared `ui_palette` key, with the same assertions pinned in each language.
It is presentation only, so it is a device preference and never part of a
profile.

The form screens no longer have a backdrop of their own. Settings, add-host and
pair used to sit on a still gradient; they now wear the same living field at a
calm mix — pools dimmed onto the palette's own corner colour, vignette halved so
rows that run to the edges don't get crushed. On the desktop console that
collapsed the old aurora-over-static crossfade into one shader pass with a
chased uniform. Motion speed is identical in both modes on purpose: changing it
would make the field jump mid-transition. Nothing in the gamepad UI is backed by
a static image now, and Reduce Motion (Apple) / "remove animations" (Android)
still freeze it.

Also: the settings screen had no raster coverage at all — the eyeball dump is
`#[ignore]`d — so a new test draws every tab, and the Android screenshot set
gains a console-settings scene. Both earned their keep immediately: the renders
showed the extra hint pushing "Done" off a 360 dp phone (the legend scrolls now,
and the Section cell only appears where shoulders exist) and the form backdrop
crushing its own edges.
enricobuehler merged commit 3edb01f1b8 into main 2026-08-06 10:50:32 +00:00
enricobuehler deleted branch worktree-gamepad-ui-polish 2026-08-06 10:50:44 +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#66