Zum Inhalt

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 Cycle Button above 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 / overlayTimeout field, the tick decay block, and the if overlayVisible { overlayIdle = 0 } in apply). The overlay closes only on .dismissOverlay (Menu).
  • TVPlayerView: .onTapGesture → controller.apply(.showOverlay) (was .togglePlaylist). Keep .onPlayPauseCommand → .togglePlaylist and 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 testOverlayAutoDismisses with testOverlayStaysOpenUntilDismissed (a long tick does NOT close it; .dismissOverlay does). 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.