The host tile gets a menu, and the start-of-stream banner becomes an About tab #209

Merged
enricobuehler merged 5 commits from worktree-gamepad-ui-host-mgmt-about into main 2026-08-13 21:46:21 +00:00
Owner

The Apple gamepad UI could add a host and connect to one, and that was all: a renamed machine or a fat-fingered address stayed wrong forever, because the only surface that could edit or remove one was the touch UI. The desktop console (pf-console-ui) and the Android console have both had a host menu on UP for a while — this is the Apple port of it, so the three consoles are learned once.

Host management

UP on a saved tile opens Wake / Copy link / Edit… / Forget pairing / Remove.

  • Wiring UP takes the whole vertical axis away from scrolling (down goes inert). A horizontal carousel has no vertical travel to spend, and one meaning per direction is what makes the gesture learnable across the three consoles.
  • Remove arms on the first press and only fires on the second, and disarms if focus wanders off the row. The touch grid gets a system confirmation dialog; a thumbstick from across a room is a good reason to be at least as strict.
  • A pinned profile card offers only Unpin — it is a shortcut, not a second host, and offering to remove the host from it would blur the distinction a pin exists to draw.
  • Vocabulary differs from the desktop console deliberately: it says one word, "Forget"; Apple has two actions (onForget drops the pinned fingerprint, onRemove deletes the record), and two different actions cannot share a name on the surface that offers both. The touch card's words win — a user meets both Apple surfaces, and only one of them is cross-platform.

Edit reuses GamepadAddHostView, seeded from the record and writing a copy back through HostStore.update, so the fingerprint, MACs, pinned cards and binding the form never shows survive a rename. It replaces the menu rather than stacking on it (the console's Nav::Replace), which keeps the shell's "depth ≤ 1 by construction" true.

The start-of-stream banner becomes a page

ContentView's 6-second showShortcutHint is retired. Telling someone the controls once, over the stream they have just connected to, answers the question at the one moment nobody is asking it — and it put a composited overlay above the stream to do it, which on that path costs about a refresh of display latency.

The words are now ShortcutsCatalog, rendered by an About page on both surfaces: the new gamepad one and the touch AboutView. The touch half is not a bonus — the banner fired in touch mode on a Mac too, so deleting it without that would have cost those users the only place the keys were written down.

About is its own tab, trailing the strip beside Profiles: both are built from something other than the settings store, and About is where the strip ends because it is the one section that changes nothing. Its two reading surfaces (shortcuts, licences) are in-place layers like the pin picker, and the licence wall reuses Licenses.chunked — the chunking that already exists so tvOS can page it by focus steps — so it scrolls with the stick and needs no machinery of its own.

The version sits under the rows as a quiet centred footer. An identity card with the app icon led the tab at first, but on tvOS that icon is a 400×240 rectangle meeting a layout built for square art; after three passes at framing it (aspect-correct frame, dropping a zero-radius clip that was still clipping to frame, then a max frame so it could shrink rather than overflow) it was still cut off on real hardware. A version string answers the only question anyone opens About to ask and has no aspect ratio to get wrong.

Divergence worth flagging

GamepadSettingsView's header states the contract "the tab names match the desktop console's and the Android client's". Apple now has an About tab neither of those has. They already diverge (the desktop console has an Input tab Apple doesn't), so this isn't a clean break — but if the three consoles should stay aligned, About is now owed to pf-console-ui and Android. Neither has an About page at all today, so that is new work there rather than a port.

Verification

  • swift build clean on macOS, arm64-apple-ios17.0 and arm64-apple-tvos17.0. The iOS pass is what typechecks the shell-layer code, which is #if os(iOS).
  • 288 tests pass.
  • On glass on a real Apple TV (0.29.0 build 100004) — which is how the icon problem was found in the first place, after it had passed three rounds of reasoning.

Known gap

On tvOS the host menu needs an actual game controller. The UP gesture is driven from the controller poll, which only reads GCExtendedGamepad; the Siri Remote is a micro gamepad, and the hint-bar cells are deliberately plain labels there (GamepadChrome.swift), so there is no fallback route to it. The About tab is fully reachable on the remote. Fixing this cleanly means making the Options cell focusable on tvOS — contained, but not in this PR.

The Apple gamepad UI could add a host and connect to one, and that was all: a renamed machine or a fat-fingered address stayed wrong forever, because the only surface that could edit or remove one was the touch UI. The desktop console (`pf-console-ui`) and the Android console have both had a host menu on UP for a while — this is the Apple port of it, so the three consoles are learned once. ## Host management UP on a saved tile opens **Wake / Copy link / Edit… / Forget pairing / Remove**. - Wiring UP takes the whole vertical axis away from scrolling (down goes inert). A horizontal carousel has no vertical travel to spend, and one meaning per direction is what makes the gesture learnable across the three consoles. - **Remove** arms on the first press and only fires on the second, and disarms if focus wanders off the row. The touch grid gets a system confirmation dialog; a thumbstick from across a room is a good reason to be at least as strict. - A pinned profile card offers only **Unpin** — it is a shortcut, not a second host, and offering to remove the host from it would blur the distinction a pin exists to draw. - Vocabulary differs from the desktop console deliberately: it says one word, "Forget"; Apple has *two* actions (`onForget` drops the pinned fingerprint, `onRemove` deletes the record), and two different actions cannot share a name on the surface that offers both. The touch card's words win — a user meets both Apple surfaces, and only one of them is cross-platform. **Edit** reuses `GamepadAddHostView`, seeded from the record and writing a *copy* back through `HostStore.update`, so the fingerprint, MACs, pinned cards and binding the form never shows survive a rename. It *replaces* the menu rather than stacking on it (the console's `Nav::Replace`), which keeps the shell's "depth ≤ 1 by construction" true. ## The start-of-stream banner becomes a page `ContentView`'s 6-second `showShortcutHint` is retired. Telling someone the controls once, over the stream they have just connected to, answers the question at the one moment nobody is asking it — and it put a composited overlay above the stream to do it, which on that path costs about a refresh of display latency. The words are now `ShortcutsCatalog`, rendered by an About page on **both** surfaces: the new gamepad one and the touch `AboutView`. The touch half is not a bonus — the banner fired in touch mode on a Mac too, so deleting it without that would have cost those users the only place the keys were written down. About is its own **tab**, trailing the strip beside Profiles: both are built from something other than the settings store, and About is where the strip ends because it is the one section that changes nothing. Its two reading surfaces (shortcuts, licences) are in-place layers like the pin picker, and the licence wall reuses `Licenses.chunked` — the chunking that already exists so tvOS can page it by focus steps — so it scrolls with the stick and needs no machinery of its own. The version sits under the rows as a quiet centred footer. An identity card with the app icon led the tab at first, but on tvOS that icon is a 400×240 rectangle meeting a layout built for square art; after three passes at framing it (aspect-correct frame, dropping a zero-radius clip that was still clipping to frame, then a max frame so it could shrink rather than overflow) it was *still* cut off on real hardware. A version string answers the only question anyone opens About to ask and has no aspect ratio to get wrong. ## Divergence worth flagging `GamepadSettingsView`'s header states the contract *"the tab names match the desktop console's and the Android client's"*. Apple now has an **About** tab neither of those has. They already diverge (the desktop console has an **Input** tab Apple doesn't), so this isn't a clean break — but if the three consoles should stay aligned, About is now owed to `pf-console-ui` and Android. Neither has an About page at all today, so that is new work there rather than a port. ## Verification - `swift build` clean on macOS, `arm64-apple-ios17.0` and `arm64-apple-tvos17.0`. The iOS pass is what typechecks the shell-layer code, which is `#if os(iOS)`. - 288 tests pass. - On glass on a real Apple TV (0.29.0 build 100004) — which is how the icon problem was found in the first place, after it had passed three rounds of reasoning. ## Known gap On tvOS the host menu needs an actual **game controller**. The UP gesture is driven from the controller poll, which only reads `GCExtendedGamepad`; the Siri Remote is a micro gamepad, and the hint-bar cells are deliberately plain labels there (`GamepadChrome.swift`), so there is no fallback route to it. The About tab is fully reachable on the remote. Fixing this cleanly means making the Options cell focusable on tvOS — contained, but not in this PR.
enricobuehler added 5 commits 2026-08-13 21:28:52 +00:00
The gamepad UI could add a host and connect to one, and that was all: a renamed
machine or a fat-fingered address stayed wrong forever, because the only surface
that could edit or remove one was the touch UI. The desktop console and the
Android console have both had a host menu on UP for a while — this is the Apple
port of it, so the three consoles are learned once.

UP on a saved tile opens Wake / Copy link / Edit… / Forget pairing / Remove.
Wiring UP takes the whole vertical axis away from scrolling (down goes inert): a
horizontal carousel has no vertical travel to spend, and one meaning per
direction is what makes the gesture learnable. Remove arms on the first press
and only fires on the second, and disarms if focus wanders off the row — the
touch grid gets a system confirmation dialog, and a thumbstick from across a
room is a good reason to be at least as strict. A pinned profile card offers
only Unpin: it is a shortcut, not a second host.

Edit reuses GamepadAddHostView, seeded from the record and writing a COPY back
through HostStore.update, so the fingerprint, MACs, pins and binding the form
never shows survive a rename. It REPLACES the menu rather than stacking on it,
which keeps the shell's "depth <= 1 by construction" true.

This also retires the start-of-stream shortcut banner. Telling someone the
controls for six seconds, over the stream they have just connected to, answers
the question at the one moment nobody is asking it — and it put a composited
overlay above the stream to do it. The words are now ShortcutsCatalog, rendered
by an About page on BOTH surfaces: the new gamepad one (icon, version, licenses,
shortcuts) and the touch AboutView. The touch half is not a bonus — the banner
fired in touch mode on a Mac too, so deleting it without that would have cost
those users the only place the keys were written down.

Verified: swift build clean on macOS, arm64-apple-ios17.0 and arm64-apple-tvos17.0
(the iOS pass is what typechecks the shell-layer code, which is #if os(iOS));
288 tests pass. NOT verified on glass — screen capture is unavailable in this
environment, so the new screens have been compiled and reasoned about but not
seen.
Reachable, but wrong: About sat at the bottom of the Interface tab, under the
palette and the overlay position — a page about the app filed among the settings
that change how it looks, found only by scrolling past them.

It is a tab now, trailing the strip beside Profiles. Both are built from
something other than the settings store, and About is where the strip ends
because it is the one section that changes nothing.

The standalone GamepadAboutView goes away with it. Its content is the tab's rows,
its two reading surfaces (shortcuts, licences) are in-place layers like the pin
picker, and the identity card — icon, name, version, tagline — rides in the
header under the tab strip. In the header rather than as a first row so the list
holds no focus stop that does nothing when pressed; laid out sideways rather than
centred like the touch page, because this header already carries a title and a
strip and a centred icon-name-version-tagline stack would leave no room for the
rows under it.

Row grows a `kind`, so the About tab can draw a heading and a block of prose
without every other tab's rows pretending to be one.

swift build clean on macOS, arm64-apple-ios17.0 and arm64-apple-tvos17.0;
288 tests pass.
Both found on glass on an Apple TV.

The card was laid out against the SCREEN while everything under it is laid out
against a centred column of `rowMaxWidth` — 920pt against a 1920-wide TV. So it
began a few hundred points to the left of every row it introduced and read as a
separate banner rather than the head of the list. It now takes the same column
and the same inner inset as a row's contents, so the icon sits directly above
the row icons.

And it was drawing the "P" monogram, never the app's mark. That fallback exists
because tvOS ships its icon as a parallax image STACK (Back/Circle1/Circle2/
Front) with no single image to load, so `AppIconView.bundleIcon` returned nil
there and always had. `AboutAppIcon` is those four layers flattened into one
asset, generated from the SAME art the stack uses so the two cannot drift into
being subtly different icons. A TV icon is a 400x240 rectangle rather than a
squircle, so `side` means HEIGHT on tvOS and the width follows the real 5:3 art
— framed square it would have sat in a box two thirds empty.

Verified the asset actually survives compilation (`assetutil` finds AboutAppIcon
in the built Assets.car at both scales) — a missing imageset would silently fall
back to the monogram again, which is exactly the bug being fixed.

swift build clean on macOS, arm64-apple-ios17.0 and arm64-apple-tvos17.0.
`cornerRadius: 0` reads as "no rounding", but a RoundedRectangle clip is not a
no-op at zero — it still clips to the layout frame, so any art whose aspect ratio
isn't the frame's loses its ends. The TV's 400x240 icon did exactly that as soon
as there was a real icon to draw instead of the square monogram. The mask now
applies only where it is wanted: iOS, whose icon ships unmasked because the
springboard rounds it at draw time.

The frame goes from fixed to MAX for the same failure one step further out: at a
fixed width the image cannot shrink when its row is tight, so it overflows and is
cropped by whatever is above it. `.fit` inside a max frame gives the whole icon
back, just smaller. And the icon takes layout priority in the identity card — the
tagline beside it is happy to wrap, and a 5:3 icon is what suffers first if the
text is given the width it asks for.

swift build clean on macOS, arm64-apple-ios17.0 and arm64-apple-tvos17.0.
fix(apple/about): drop the identity card for a version line under the rows
ci / bun-nix (pull_request) Successful in 42s
ci / web (pull_request) Successful in 1m9s
ci / docs-site (pull_request) Successful in 1m13s
ci / rust-arm64 (pull_request) Successful in 1m20s
ci / rust (pull_request) Successful in 13m50s
apple / swift (pull_request) Successful in 2m0s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
9c2c8d1643
The card led the About tab with the app icon, and on tvOS that icon is a 400x240
rectangle meeting a layout built for square art. Three passes at framing it —
aspect-correct frame, then dropping the zero-radius clip that was cropping it,
then a max frame so it could shrink instead of overflow — and it was still cut
off on real hardware.

So the card goes. A version string answers the only question anyone opens About
to ask, it has no aspect ratio to get wrong, and it belongs under the rows rather
than over them: quiet and centred, reading as a footer instead of a row you
failed to press. `Row.Kind.footer` draws it.

swift build clean on macOS, arm64-apple-ios17.0 and arm64-apple-tvos17.0;
on glass on an Apple TV as 0.29.0 (100004).
enricobuehler merged commit 1b167f8e35 into main 2026-08-13 21:46:21 +00:00
enricobuehler deleted branch worktree-gamepad-ui-host-mgmt-about 2026-08-13 21:46:24 +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#209