An OLED palette, and split WHETHER the gamepad UI is offered from WHEN it appears #116

Merged
enricobuehler merged 1 commits from worktree-oled-theme-gamepad-ui-split into main 2026-08-08 11:22:35 +00:00
Owner

Three requested client changes, plus a field-reported tvOS bug that the first of them would have made far more visible.

A thirteenth ui_palette entry, oled

The palette table is hand-mirrored in three languages (pf-console-ui's library.rs, GamepadPalette.swift, GamepadPalette.kt), so it goes into all three at index 1 — directly after the brand default, which keeps PALETTES[0] the unknown-id fallback and keeps the dark→pale cycling order intact.

What earns the name is arithmetic, not a darker shade of violet. The ramp's first two stops are literally (0,0,0) and the ground is pure black, so the shaded half of the field is pixels switched off rather than "very dark grey", and the calm mix the form screens sit under lifts toward nothing at all. Mean cell luminance is 0.019 against Violet's 0.254. The bright corner keeps a faint indigo→violet ember so the backdrop is still a field with somewhere to go, and that ember carries enough chroma at that luminance (60° of hue travel across 13 of the 16 cells) to satisfy the existing multi-tone assertion without adding oled to the near-neutral exemption Graphite and Opal take.

Each port gains an oled_is_actually_black test that measures the claim — pure-black corner cells, a mean under half the darkest other field's — rather than restating the table.

A new device key, gamepad_ui_mode

The gamepad-UI switch had been deciding two things at once: whether to offer the controller-optimized interface at all, and that it appears only while a pad is attached. A user asked for the second half to stop applying.

"connected" (the default, and exactly what the lone Bool meant) and "always" separate them, surfaced as a "Show it" row directly under the switch on all five settings surfaces — and built only while that switch is on, because a picker whose every option decides nothing is worse than no picker.

GamepadUIEnvironment.isActive takes the mode with no default argument on purpose: a call site that forgot it would silently strand everyone who chose Always back on "only with a controller", which is the one bug this parameter exists to make impossible. An unrecognized value waits for a controller, so a mode a newer client wrote can never trap an older one in a layout it has no way back out of. It stays a device preference on both platforms, never part of a profile — which interface this device wears has nothing to do with how a host streams to it.

The smoothness buffer is hidden under Lowest latency, not dimmed

Everywhere else already hid it — the GTK and WinUI shells, the Apple touch and tvOS screens, the Android touch screen — because under that intent it names a quantity that does not exist. Two surfaces disagreed: Apple's gamepad settings screen left the row live and steppable, and the desktop console dimmed it, having no way to drop a row from a fixed list.

That list is now rebuilt each frame through a row_applies filter. The original concern — a vanishing row moving everything under the cursor — does not apply here, and the new test says why: the row it drops sits directly below the row that drops it, so the only cursor that can be present when the list shrinks is the one on the intent row, which does not move.

Two latent hazards went with it: apply_row had been indexing the row list on the assumption the cursor is always in range, and nothing re-clamped that cursor when another writer changed the intent behind the screen's back.

Pale palettes were unreadable on tvOS

Reported from the field: "on tvOS the bright colour themes are broken — the UI does not adapt and stays bright, so the text is basically not readable."

GamepadInk was never the problem. It flips correctly for a pale field, it is not platform-gated, and every tvOS gamepad entry point already published it — verified one by one.

The cause is that this app sets preferredColorScheme nowhere and declares no UIUserInterfaceStyle. So every system-derived colour landing on those screens — a .secondary placeholder, a .bordered button's chrome, a NavigationStack title, a material's frost — resolved against the device appearance, which the palette cannot reach. On iPhone, iPad and Mac a great many users sit in Light mode, so under a pale palette those colours came out dark and the theme looked correct by accident; an Apple TV is Dark essentially always, so every one of them rendered white on a light field. The mirror image was broken too and had simply never been reported: a dark palette on a Light-mode iPhone was already drawing dark on dark.

The scheme is now published beside the ink, once, in GamepadInkModifier — the two are halves of one decision, and publishing only the ink silently loses every colour the frameworks draw on the app's behalf. Two structural amplifiers went with it:

  • ConsoleGlass scoped the scheme to the fill inside its .background {} on the tvOS and pre-26 branches while the 26 branch put it on the content, so no console row's own content ever saw it on tvOS.
  • LibraryView's navigation chrome and its loading/error/empty states sit above LibraryCoverflowView and so were never inked at all on tvOS and macOS, where that view is presented directly rather than through the iOS-only GamepadLibraryScreen wrapper.

That exposed a second tvOS gap worth closing in the same breath: ui_palette had no row in tvOS's ordinary Settings, and the gamepad settings screen that owns it everywhere else needs an extended-profile controller to open on tvOS. An Apple TV driven by the Siri Remote alone could not reach the palettes at all — which would now include the OLED one. SettingsView.tvBody carries a Background row.

Verification

pf-console-ui builds, clippy --all-targets -D warnings clean, 74 tests pass, cargo fmt --check clean (with pf-client-core) — under linux/amd64
Android :app 80 tests, 0 failures — incl. 4 new gamepadUiActive cases, the palette parity table, and 2 new row-visibility tests
Apple package builds for macOS and tvOS; 9 palette / gamepad-UI tests pass

A Mac cargo check of pf-console-ui is vacuous (every module is cfg'd to linux/windows), hence the container. The tvOS typecheck is possible because the checked-in xcframework already carries a tvos-arm64 slice.

Known gaps

  • The tvOS rendering fix is compile-verified only. An on-glass Apple TV check under a pale palette is the one thing here a build cannot answer.
  • Android's gamepad settings screen has no Prioritize or Smoothness buffer rows at all (pre-existing). On Android TV those two settings are unreachable without turning the console UI off. Left alone as out of scope.
  • Linux/Windows/Deck have no gamepad-UI toggle — the console home is a separate entry point — so always does not apply there.
  • One low-confidence tvOS candidate left untouched: a drawingGroup() on the settings value drum, only implicated if row values read wrong while their labels read right.
  • consoleGlassBackground in GlassStyle.swift is dead code (no call sites left); not removed here.
Three requested client changes, plus a field-reported tvOS bug that the first of them would have made far more visible. ## A thirteenth `ui_palette` entry, `oled` The palette table is hand-mirrored in three languages (`pf-console-ui`'s `library.rs`, `GamepadPalette.swift`, `GamepadPalette.kt`), so it goes into all three at index 1 — directly after the brand default, which keeps `PALETTES[0]` the unknown-id fallback and keeps the dark→pale cycling order intact. What earns the name is arithmetic, not a darker shade of violet. The ramp's first two stops are literally `(0,0,0)` and the ground is pure black, so the shaded half of the field is pixels *switched off* rather than "very dark grey", and the calm mix the form screens sit under lifts toward nothing at all. Mean cell luminance is **0.019 against Violet's 0.254**. The bright corner keeps a faint indigo→violet ember so the backdrop is still a field with somewhere to go, and that ember carries enough chroma at that luminance (60° of hue travel across 13 of the 16 cells) to satisfy the existing multi-tone assertion *without* adding `oled` to the near-neutral exemption Graphite and Opal take. Each port gains an `oled_is_actually_black` test that measures the claim — pure-black corner cells, a mean under half the darkest other field's — rather than restating the table. ## A new device key, `gamepad_ui_mode` The gamepad-UI switch had been deciding two things at once: whether to offer the controller-optimized interface at all, and that it appears only while a pad is attached. A user asked for the second half to stop applying. `"connected"` (the default, and exactly what the lone Bool meant) and `"always"` separate them, surfaced as a **"Show it"** row directly under the switch on all five settings surfaces — and built only while that switch is on, because a picker whose every option decides nothing is worse than no picker. `GamepadUIEnvironment.isActive` takes the mode with **no default argument** on purpose: a call site that forgot it would silently strand everyone who chose Always back on "only with a controller", which is the one bug this parameter exists to make impossible. An unrecognized value waits for a controller, so a mode a newer client wrote can never trap an older one in a layout it has no way back out of. It stays a device preference on both platforms, never part of a profile — which interface this device wears has nothing to do with how a host streams to it. ## The smoothness buffer is hidden under Lowest latency, not dimmed Everywhere else already hid it — the GTK and WinUI shells, the Apple touch and tvOS screens, the Android touch screen — because under that intent it names a quantity that does not exist. Two surfaces disagreed: Apple's gamepad settings screen left the row live and steppable, and the desktop console dimmed it, having no way to drop a row from a fixed list. That list is now rebuilt each frame through a `row_applies` filter. The original concern — a vanishing row moving everything under the cursor — does not apply here, and the new test says why: **the row it drops sits directly below the row that drops it**, so the only cursor that can be present when the list shrinks is the one on the intent row, which does not move. Two latent hazards went with it: `apply_row` had been indexing the row list on the assumption the cursor is always in range, and nothing re-clamped that cursor when another writer changed the intent behind the screen's back. ## Pale palettes were unreadable on tvOS Reported from the field: *"on tvOS the bright colour themes are broken — the UI does not adapt and stays bright, so the text is basically not readable."* `GamepadInk` was never the problem. It flips correctly for a pale field, it is not platform-gated, and every tvOS gamepad entry point already published it — verified one by one. The cause is that **this app sets `preferredColorScheme` nowhere** and declares no `UIUserInterfaceStyle`. So every *system*-derived colour landing on those screens — a `.secondary` placeholder, a `.bordered` button's chrome, a `NavigationStack` title, a material's frost — resolved against the **device appearance**, which the palette cannot reach. On iPhone, iPad and Mac a great many users sit in Light mode, so under a pale palette those colours came out dark and the theme looked correct *by accident*; an Apple TV is Dark essentially always, so every one of them rendered white on a light field. The mirror image was broken too and had simply never been reported: a dark palette on a Light-mode iPhone was already drawing dark on dark. The scheme is now published beside the ink, once, in `GamepadInkModifier` — the two are halves of one decision, and publishing only the ink silently loses every colour the frameworks draw on the app's behalf. Two structural amplifiers went with it: - `ConsoleGlass` scoped the scheme to the fill inside its `.background {}` on the tvOS and pre-26 branches while the 26 branch put it on the content, so no console row's own content ever saw it on tvOS. - `LibraryView`'s navigation chrome and its loading/error/empty states sit above `LibraryCoverflowView` and so were never inked at all on tvOS and macOS, where that view is presented directly rather than through the iOS-only `GamepadLibraryScreen` wrapper. That exposed a second tvOS gap worth closing in the same breath: `ui_palette` had **no row** in tvOS's ordinary Settings, and the gamepad settings screen that owns it everywhere else needs an extended-profile controller to open on tvOS. An Apple TV driven by the Siri Remote alone could not reach the palettes at all — which would now include the OLED one. `SettingsView.tvBody` carries a **Background** row. ## Verification | | | |---|---| | `pf-console-ui` | builds, `clippy --all-targets -D warnings` clean, **74 tests pass**, `cargo fmt --check` clean (with `pf-client-core`) — under `linux/amd64` | | Android `:app` | **80 tests, 0 failures** — incl. 4 new `gamepadUiActive` cases, the palette parity table, and 2 new row-visibility tests | | Apple | package builds for **macOS and tvOS**; **9 palette / gamepad-UI tests pass** | A Mac `cargo check` of `pf-console-ui` is vacuous (every module is cfg'd to linux/windows), hence the container. The tvOS typecheck is possible because the checked-in xcframework already carries a `tvos-arm64` slice. ## Known gaps - ⏳ **The tvOS rendering fix is compile-verified only.** An on-glass Apple TV check under a pale palette is the one thing here a build cannot answer. - Android's gamepad settings screen has **no Prioritize or Smoothness buffer rows at all** (pre-existing). On Android TV those two settings are unreachable without turning the console UI off. Left alone as out of scope. - Linux/Windows/Deck have no gamepad-UI *toggle* — the console home is a separate entry point — so `always` does not apply there. - One low-confidence tvOS candidate left untouched: a `drawingGroup()` on the settings value drum, only implicated if row *values* read wrong while their labels read right. - `consoleGlassBackground` in `GlassStyle.swift` is dead code (no call sites left); not removed here.
enricobuehler added 1 commit 2026-08-08 11:12:32 +00:00
feat(clients): an OLED palette, and split WHETHER the gamepad UI is offered from WHEN it appears
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m39s
ci / rust-arm64 (pull_request) Successful in 4m7s
android / android (pull_request) Successful in 5m1s
ci / docs-site (pull_request) Successful in 1m47s
ci / bun-nix (pull_request) Successful in 42s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m19s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m23s
ci / rust (pull_request) Successful in 14m32s
30bd10e301
Four changes to the client interface, kept together because two of them touch the same rows
and the last is a bug the first would have made far more visible.

A thirteenth `ui_palette` entry, `oled`. The palette table is hand-mirrored in three languages
(`pf-console-ui`'s `library.rs`, `GamepadPalette.swift`, `GamepadPalette.kt`), so it goes into
all three at index 1, directly after the brand default — which keeps `PALETTES[0]` the unknown-id
fallback and keeps the dark-to-pale cycling order intact. What earns the name is arithmetic, not
a darker shade of violet: the ramp's first two stops are literally (0,0,0) and the ground is pure
black, so the shaded half of the field is pixels switched off rather than "very dark grey", and
the calm mix the form screens sit under lifts toward nothing at all. Mean cell luminance is 0.019
against Violet's 0.254. The bright corner keeps a faint indigo-to-violet ember so the backdrop is
still a field with somewhere to go, and that ember carries enough chroma at that luminance
(60 degrees of hue travel across 13 of the 16 cells) to satisfy the existing multi-tone assertion
without adding `oled` to the near-neutral exemption Graphite and Opal take. Each port gains an
`oled_is_actually_black` test that measures the claim — pure-black corner cells, a mean under half
the darkest other field's — rather than restating the table.

A new device key, `gamepad_ui_mode`. The gamepad-UI switch had been deciding two things at once:
whether to offer the controller-optimized interface at all, and that it appears only while a pad
is attached. A user asked for the second half to stop applying. `"connected"` (the default, and
exactly what the lone Bool meant) and `"always"` separate them, surfaced as a "Show it" row
directly under the switch on all five settings surfaces and built only while that switch is on —
a picker whose every option decides nothing is worse than no picker. `GamepadUIEnvironment.isActive`
takes the mode with NO default argument on purpose: a call site that forgot it would silently
strand everyone who chose Always back on "only with a controller", which is the one bug this
parameter exists to make impossible. An unrecognized value waits for a controller, so a mode a
newer client wrote can never trap an older one in a layout it has no way back out of. It stays a
device preference on both platforms, never part of a profile: which interface this device wears
has nothing to do with how a host streams to it.

The smoothness buffer is hidden under Lowest latency, not dimmed. Everywhere else already hid it
— the GTK and WinUI shells, the Apple touch and tvOS screens, the Android touch screen — because
under that intent it names a quantity that does not exist. Two surfaces disagreed: Apple's gamepad
settings screen left the row live and steppable, and the desktop console dimmed it, having no way
to drop a row from a fixed list. That list is now rebuilt each frame through a `row_applies`
filter. The concern about a vanishing row moving everything under the cursor does not apply here
and the new test says why: the row it drops sits directly BELOW the row that drops it, so the only
cursor that can be present when the list shrinks is the one on the intent row, which does not
move. Two latent hazards went with it — `apply_row` had been indexing the row list on the
assumption the cursor is always in range, and nothing re-clamped that cursor when another writer
changed the intent behind the screen's back.

Pale palettes were unreadable on tvOS, reported from the field. `GamepadInk` was never the
problem: it flips correctly for a pale field, it is not platform-gated, and every tvOS gamepad
entry point already published it. The cause is that this app sets `preferredColorScheme` nowhere
and declares no `UIUserInterfaceStyle`, so every SYSTEM-derived colour landing on those screens —
a `.secondary` placeholder, a `.bordered` button's chrome, a NavigationStack title, a material's
frost — resolved against the DEVICE appearance, which the palette cannot reach. On iPhone, iPad
and Mac a great many users sit in Light mode, so under a pale palette those colours came out dark
and the theme looked correct by accident; an Apple TV is Dark essentially always, so every one of
them rendered white on a light field. The mirror image was broken too and had simply never been
reported: a dark palette on a Light-mode iPhone was already drawing dark on dark. The scheme is
now published beside the ink, once, in `GamepadInkModifier`, because the two are halves of one
decision and publishing only the ink silently loses every colour the frameworks draw on the app's
behalf. Two structural amplifiers went with it: `ConsoleGlass` had been scoping the scheme to the
fill inside its `.background {}` on the tvOS and pre-26 branches while the 26 branch put it on the
content, so no console row's own content ever saw it on tvOS; and `LibraryView`'s navigation
chrome and its loading, error and empty states sit above `LibraryCoverflowView` and so were never
inked at all on tvOS and macOS, where that view is presented directly rather than through the
iOS-only `GamepadLibraryScreen` wrapper.

That last one exposed a second tvOS gap worth closing in the same breath: `ui_palette` had no row
in tvOS's ordinary Settings, and the gamepad settings screen that owns it everywhere else needs an
extended-profile controller to open on tvOS. An Apple TV driven by the Siri Remote alone could not
reach the palettes at all, which would now include the OLED one. `SettingsView.tvBody` carries a
Background row.

Verified: pf-console-ui builds, passes `clippy --all-targets -D warnings` and runs 74 tests clean
under linux/amd64 (a Mac `cargo check` of that crate is vacuous — every module is cfg'd to
linux/windows); `cargo fmt --check` clean for it and pf-client-core. Android `:app` runs 80 tests
with 0 failures, including four new `gamepadUiActive` cases and the palette parity table. The
Apple package builds for macOS AND tvOS and its 9 palette/gamepad-UI tests pass — the tvOS
typecheck is possible because the checked-in xcframework already carries a `tvos-arm64` slice. The
tvOS RENDERING fix is compile-verified only; an on-glass Apple TV check under a pale palette is
still owed, and is the one thing here that a build cannot answer.
enricobuehler merged commit 4b514cc07c into main 2026-08-08 11:22:35 +00:00
enricobuehler deleted branch worktree-oled-theme-gamepad-ui-split 2026-08-08 11:22:37 +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#116