Inc 12c — Phase 3: Focus-native tvOS controls¶
Date: 2026-06-16
Status: Approved (interaction model chosen by Johannes), ready to implement
Scope: apple/PhysarumApp/Sources/{Ambient,TV} + Tests. Engine/Renderer/WGSL untouched.
Motivation¶
Device test with the iPhone Remote (imprecise trackpad): the overlay is frustrating —
(1) the Cycle button is unreachable, (2) the overlay closes unwantedly, (3) left/right is flaky.
Root cause: we layered a custom onMoveCommand swipe layer over the tvOS focus engine, plus
auto-dismiss + swipe-down-to-dismiss. The fix is to go focus-engine-native (HIG).
Goal (approved model: "click opens menu")¶
- Overlay = focus-driven control panel. The focus engine navigates it (◀▶ moves the preset carousel, ▲▼ moves between carousel and the Cycle button, click selects/toggles). It closes only via Menu/Back — no auto-dismiss, no swipe-down-close.
- Player (overlay closed): click → open the overlay; Play/Pause button → toggle cycle (+HUD); swipe ◀▶ → prev/next (quick shortcut, retained); swipe ▲ → open overlay.
Changes¶
TVOverlayView: remove the container.onMoveCommand { .down → dismiss }(it swallows the directional moves the focus engine needs → unreachable button + dead ◀▶). Keep the focusable CycleButtonabove the focusable carousel (.focusSection()); initial focus on the current preset card (.onAppear). Card click →jumpTo; button click →togglePlaylist. Update doc.AmbientController: remove the auto-dismiss (overlayIdle/overlayTimeoutfield, thetickdecay block, and theif overlayVisible { overlayIdle = 0 }inapply). The overlay closes only on.dismissOverlay(Menu).TVPlayerView:.onTapGesture→controller.apply(.showOverlay)(was.togglePlaylist). Keep.onPlayPauseCommand → .togglePlaylistand the player.onMoveCommand(◀▶ prev/next, ▲ showOverlay, ▼ no-op when closed)..onExitCommand(Menu → dismiss when open) unchanged.TVHintView: text → "Click for presets" (the reliable primary gesture; swipe-up still works).
Tests & Verification¶
- Host: replace
testOverlayAutoDismisseswithtestOverlayStaysOpenUntilDismissed(a longtickdoes NOT close it;.dismissOverlaydoes). Other AmbientController tests stay green. - Empirical (device sign-off, Johannes): open menu via click; reach the Cycle button by swiping up from the carousel; ◀▶ moves the highlight; click selects; Menu closes it and nothing else does (no surprise close); swipe ◀▶ on the player still quick-browses.
Risk¶
- Focus reachability (carousel ↔ button) is tvOS-empirical; removing the move-command interception is the standard fix, but final confirmation is on-device. The dedicated Play/Pause button + click remain independent control paths, so the Cycle button is never the only way to toggle.