Glide TV focus rings and key-driven scrolling #76

Closed
justinzeus wants to merge 0 commits from fm/polish-lane-e into fm/tv-round2
Owner

Changes

Implement the shared foundation, recovery-toast motion regression (P02), morphing card focus outline (E02), and spring-smoothed key scrolling (E05) against fm/tv-round2.

  • Immutable shared base: 74b737fb52bfbb2cc7afa0b0288ae59e67232320. Applied foundation.patch verbatim, compiled it before publishing, and made no compile repairs or history rewrites.
  • Recovery toasts retain their node through the tokenized fade-out; replacement keeps the current opacity. Native regressions cover fade-in/out, hold, replacement and interrupted re-entry, including reduced motion.
  • Hosted cards share one draw-only outline. Position, width, height and radius follow the navigation spring; card scale and shadow stay unchanged. Standalone cards keep their original outline. Host placement sampling leaves global-position observation only on the focused card.
  • Key scrolling carries bounded same-direction velocity and continues the preceding animation frame clock instead of inserting stationary restart frames. Immediate replacements claim the carry; completion and abandoned surfaces clear it. Reduced motion snaps. Shelf reconciliation still lands on the exact first-card line.
  • Home and Settings use the shared scroll helper. Tests exercise real Home, Library, shelf and native drawing paths, not private state or mocked forwarding.
Idea Production files in wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/ Native regression in the matching test package
P02 retained toast fade RecoveryToast.kt in the shared base RecoveryToastMotionTest.kt
E02 gliding, shape-morphing outline Focus.kt, FocusRingHost.kt FocusGlideTest.kt
E05 spring scrolling FrameScroll.kt, ShelfScroll.kt, HomeScreen.kt, SettingsList.kt ScrollMomentumTest.kt

Approved decisions

  • P02: “the row keeps the last text and fades out over base 160 ms, ease”; the existing 3200 ms caller timer is unchanged. Both fades cap at 80 ms under reduced motion.
  • E02: “the delta springs to 0 on the nav spring” and “Card scale is unchanged”. Card-to-card alpha stays at 1; entry/exit use focusMove 180 ms easeOut. Geometry snaps under reduced motion, with alpha capped at 80 ms.
  • E02 scope: “Home shelves, Discover lanes and the Library grid” plus “See all grids, title-page shelves (episodes, More like this, cast circles) and the person filmography”. The shared shell-host integration supplies this scope; cast-circle radius morphing is exercised here.
  • E05: “one press is slightly softer at the end. Accepted: this is the approved nav feel”. Same-direction initial velocity is bounded by sqrt(navBlobStiffness) × |distance|; reverse-direction carry is zero. “The pinned-line and lookahead targets are unchanged; only the motion to them changes.”

Integration ownership

The agreed ShellSurface host wrap and toast caller substitutions belong to lanes A/B/D; title/person scroll callers and the central TV-spec updates belong to lane D. Those files are intentionally untouched here. This lane owns the shared host/primitives, Home/Settings callers and their regressions. Motion uses the shared tokens; no timing or approved copy changes beyond the agreed polish.

Verification

Final corrected 14-class batch: 87 tests passed, zero failures/errors. Gradle: BUILD SUCCESSFUL in 2m32s; wall time including shared-lock queue: 1138.58s. Published the before/after strips and visually inspected native toast fade-in/out, moving/settled shape-morphing outline, Home held-Right and Library held-Down frames.

Commands

Token generation check

python3 wolfskin-design/generate_tokens.py --check

Base compilation/smoke

bash docker/gradle.sh debug :wolfskin-design:testDebugUnitTest \
  --tests org.justinvisser.wolfskin.design.FocusRingTest

Three touched native classes

bash docker/gradle.sh debug :wolfskin-design:testDebugUnitTest \
  --tests org.justinvisser.wolfskin.design.RecoveryToastMotionTest \
  --tests org.justinvisser.wolfskin.design.FocusGlideTest \
  --tests org.justinvisser.wolfskin.design.ScrollMomentumTest

Fourteen-class touched/neighbor batch

bash docker/gradle.sh debug :wolfskin-design:testDebugUnitTest \
  --tests org.justinvisser.wolfskin.design.RecoveryToastMotionTest \
  --tests org.justinvisser.wolfskin.design.FocusGlideTest \
  --tests org.justinvisser.wolfskin.design.ScrollMomentumTest \
  --tests org.justinvisser.wolfskin.design.FocusRingTest \
  --tests org.justinvisser.wolfskin.design.CardBottomPixelTest \
  --tests org.justinvisser.wolfskin.design.CardGeometryTest \
  --tests org.justinvisser.wolfskin.design.HomeViewportTest \
  --tests org.justinvisser.wolfskin.design.HomeKeysTest \
  --tests org.justinvisser.wolfskin.design.HomeNavigationTest \
  --tests org.justinvisser.wolfskin.design.DiscoverKeysTest \
  --tests org.justinvisser.wolfskin.design.LibraryKeysTest \
  --tests org.justinvisser.wolfskin.design.NavSpringTest \
  --tests org.justinvisser.wolfskin.design.RecoveryCaptureTest \
  --tests org.justinvisser.wolfskin.design.ShellKeysTest

Aligned legacy-source capture

bash docker/gradle.sh debug :wolfskin-design:testDebugUnitTest \
  --tests org.justinvisser.wolfskin.design.FocusGlideTest.shelfRightAndRowDownGlideAsOneShape \
  --tests org.justinvisser.wolfskin.design.FocusGlideTest.reducedShelfGeometrySnapsInOneFrame \
  --tests org.justinvisser.wolfskin.design.ScrollMomentumTest.heldRightCarriesVelocityAndSettlesOnShelfFirst \
  --tests org.justinvisser.wolfskin.design.ScrollMomentumTest.heldDownCarriesVelocityAndSettlesOnLibraryRow \
  --tests org.justinvisser.wolfskin.design.ScrollMomentumTest.reducedShelfScrollSnapsInOneFrame \
  --tests org.justinvisser.wolfskin.design.ScrollMomentumTest.reducedLibraryScrollSnapsInOneFrame \
  --tests org.justinvisser.wolfskin.design.RecoveryToastMotionTest.captureLegacyToastFrames

The legacy capture method was temporary and is removed. Six production files were replayed from verified exact base blobs solely for before captures, then restored; the published base commit was never changed.

Run history

Run Command above Observed result
Token generation Token generation check Passed
Shared-base compile Base compilation/smoke Passed
Initial baseline Three touched native classes 10 tests, 8 failed: legacy motion plus initial capture-clock synchronization issues
First implementation Fourteen-class batch 85 tests, 76 passed, 9 failed; all 11 unchanged neighbors passed
Queued retry Fourteen-class batch Timed out at 1800 seconds; no fresh result, no surviving private build container
Corrected native cases Three touched native classes 16 tests, 13 passed, 3 failed; reproduced stale-timestamp re-entry and corrected toast reference-frame sampling
Aligned exact-base replay Aligned legacy-source capture 7 tests, 3 passed, 4 expected legacy-motion failures
First cancellation cleanup Fourteen-class batch 87 tests, 85 passed, 2 held-scroll failures; cleanup reset velocity before the replacement claimed it
Corrected frame-boundary cleanup Fourteen-class batch 87 tests passed; zero failures/errors

The first implementation exposed a real frame-clock restart gap. The re-entry regression exposed stale animation time after unmount. The corrected cleanup waits independently until the next frame, without suspending the old mutation. Held-scroll assertions were not weakened. Native settling permits two pixels for the spring tail; fully settled poster/tile/See all images separately require maximum channel difference ≤2 from the original rendering.

Native proof

Root: /root/.treehouse/firstmate-7bab20/2/firstmate/data/ui-polish-build/lane-e/.

  • P02/before/ and P02/after/: fade-in/out strips with matching approved copy; reduced-motion after strips and replacement/re-entry frames.
  • E02/before/ and E02/after/: Right then row-Down position/shape strips, normal and reduced motion; native samples.
  • E05/before/ and E05/after/: held-Right Home shelf and held-Down Library strips, normal and reduced motion; frame offsets/velocity samples.

Full run notes and retained XML: /root/.treehouse/firstmate-7bab20/2/firstmate/data/polish-lane-e/validation.md and adjacent evidence directories.

Proof uses native Compose/Robolectric rendering, not an installed APK or physical-device session. All Gradle runs used the shared-lock wrapper. No full suite, APK build, TV/ADB/emulator access, merges, or shared-infrastructure administration.

AI assistance: implementation, native regressions and this review description were produced by the firstmate-managed coding agent.

  • Integration correction: merged current origin/fm/tv-round2 without rewriting published history; UpdatesKeysTest.classicRequiresEveryRetainedRowAndTheRealScroller now waits for real list idle with at most an additional 320 ms spring-tail allowance, preserving the exact final index 4 assertion and all product scroll behavior. bash docker/gradle.sh debug :app:testDefaultDebugUnitTest --tests org.justinvisser.wolfskin.tv.update.UpdatesKeysTest :wolfskin-design:testDebugUnitTest --tests org.justinvisser.wolfskin.design.ScrollMomentumTest passed 12/12 (6 + 6), zero failures/errors, Gradle 4m29s / wall 275.68s; no full suite. The initial identical command using :app:testDebugUnitTest stopped before tests because that app task is flavor-ambiguous.
## Changes Implement the shared foundation, recovery-toast motion regression (P02), morphing card focus outline (E02), and spring-smoothed key scrolling (E05) against `fm/tv-round2`. - Immutable shared base: `74b737fb52bfbb2cc7afa0b0288ae59e67232320`. Applied `foundation.patch` verbatim, compiled it before publishing, and made no compile repairs or history rewrites. - Recovery toasts retain their node through the tokenized fade-out; replacement keeps the current opacity. Native regressions cover fade-in/out, hold, replacement and interrupted re-entry, including reduced motion. - Hosted cards share one draw-only outline. Position, width, height and radius follow the navigation spring; card scale and shadow stay unchanged. Standalone cards keep their original outline. Host placement sampling leaves global-position observation only on the focused card. - Key scrolling carries bounded same-direction velocity and continues the preceding animation frame clock instead of inserting stationary restart frames. Immediate replacements claim the carry; completion and abandoned surfaces clear it. Reduced motion snaps. Shelf reconciliation still lands on the exact first-card line. - Home and Settings use the shared scroll helper. Tests exercise real Home, Library, shelf and native drawing paths, not private state or mocked forwarding. | Idea | Production files in `wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/` | Native regression in the matching test package | |---|---|---| | P02 retained toast fade | `RecoveryToast.kt` in the shared base | `RecoveryToastMotionTest.kt` | | E02 gliding, shape-morphing outline | `Focus.kt`, `FocusRingHost.kt` | `FocusGlideTest.kt` | | E05 spring scrolling | `FrameScroll.kt`, `ShelfScroll.kt`, `HomeScreen.kt`, `SettingsList.kt` | `ScrollMomentumTest.kt` | ### Approved decisions - P02: “the row keeps the last text and fades out over `base` 160 ms, `ease`”; the existing 3200 ms caller timer is unchanged. Both fades cap at 80 ms under reduced motion. - E02: “the delta springs to 0 on the nav spring” and “Card scale is unchanged”. Card-to-card alpha stays at 1; entry/exit use `focusMove` 180 ms `easeOut`. Geometry snaps under reduced motion, with alpha capped at 80 ms. - E02 scope: “Home shelves, Discover lanes and the Library grid” plus “See all grids, title-page shelves (episodes, More like this, cast circles) and the person filmography”. The shared shell-host integration supplies this scope; cast-circle radius morphing is exercised here. - E05: “one press is slightly softer at the end. **Accepted**: this is the approved nav feel”. Same-direction initial velocity is bounded by `sqrt(navBlobStiffness) × |distance|`; reverse-direction carry is zero. “The pinned-line and lookahead targets are unchanged; only the motion to them changes.” ### Integration ownership The agreed `ShellSurface` host wrap and toast caller substitutions belong to lanes A/B/D; title/person scroll callers and the central TV-spec updates belong to lane D. Those files are intentionally untouched here. This lane owns the shared host/primitives, Home/Settings callers and their regressions. Motion uses the shared tokens; no timing or approved copy changes beyond the agreed polish. ## Verification Final corrected 14-class batch: **87 tests passed, zero failures/errors**. Gradle: **BUILD SUCCESSFUL in 2m32s**; wall time including shared-lock queue: **1138.58s**. Published the before/after strips and visually inspected native toast fade-in/out, moving/settled shape-morphing outline, Home held-Right and Library held-Down frames. ### Commands **Token generation check** ```sh python3 wolfskin-design/generate_tokens.py --check ``` **Base compilation/smoke** ```sh bash docker/gradle.sh debug :wolfskin-design:testDebugUnitTest \ --tests org.justinvisser.wolfskin.design.FocusRingTest ``` **Three touched native classes** ```sh bash docker/gradle.sh debug :wolfskin-design:testDebugUnitTest \ --tests org.justinvisser.wolfskin.design.RecoveryToastMotionTest \ --tests org.justinvisser.wolfskin.design.FocusGlideTest \ --tests org.justinvisser.wolfskin.design.ScrollMomentumTest ``` **Fourteen-class touched/neighbor batch** ```sh bash docker/gradle.sh debug :wolfskin-design:testDebugUnitTest \ --tests org.justinvisser.wolfskin.design.RecoveryToastMotionTest \ --tests org.justinvisser.wolfskin.design.FocusGlideTest \ --tests org.justinvisser.wolfskin.design.ScrollMomentumTest \ --tests org.justinvisser.wolfskin.design.FocusRingTest \ --tests org.justinvisser.wolfskin.design.CardBottomPixelTest \ --tests org.justinvisser.wolfskin.design.CardGeometryTest \ --tests org.justinvisser.wolfskin.design.HomeViewportTest \ --tests org.justinvisser.wolfskin.design.HomeKeysTest \ --tests org.justinvisser.wolfskin.design.HomeNavigationTest \ --tests org.justinvisser.wolfskin.design.DiscoverKeysTest \ --tests org.justinvisser.wolfskin.design.LibraryKeysTest \ --tests org.justinvisser.wolfskin.design.NavSpringTest \ --tests org.justinvisser.wolfskin.design.RecoveryCaptureTest \ --tests org.justinvisser.wolfskin.design.ShellKeysTest ``` **Aligned legacy-source capture** ```sh bash docker/gradle.sh debug :wolfskin-design:testDebugUnitTest \ --tests org.justinvisser.wolfskin.design.FocusGlideTest.shelfRightAndRowDownGlideAsOneShape \ --tests org.justinvisser.wolfskin.design.FocusGlideTest.reducedShelfGeometrySnapsInOneFrame \ --tests org.justinvisser.wolfskin.design.ScrollMomentumTest.heldRightCarriesVelocityAndSettlesOnShelfFirst \ --tests org.justinvisser.wolfskin.design.ScrollMomentumTest.heldDownCarriesVelocityAndSettlesOnLibraryRow \ --tests org.justinvisser.wolfskin.design.ScrollMomentumTest.reducedShelfScrollSnapsInOneFrame \ --tests org.justinvisser.wolfskin.design.ScrollMomentumTest.reducedLibraryScrollSnapsInOneFrame \ --tests org.justinvisser.wolfskin.design.RecoveryToastMotionTest.captureLegacyToastFrames ``` The legacy capture method was temporary and is removed. Six production files were replayed from verified exact base blobs solely for before captures, then restored; the published base commit was never changed. ### Run history | Run | Command above | Observed result | |---|---|---| | Token generation | Token generation check | Passed | | Shared-base compile | Base compilation/smoke | Passed | | Initial baseline | Three touched native classes | 10 tests, 8 failed: legacy motion plus initial capture-clock synchronization issues | | First implementation | Fourteen-class batch | 85 tests, 76 passed, 9 failed; all 11 unchanged neighbors passed | | Queued retry | Fourteen-class batch | Timed out at 1800 seconds; no fresh result, no surviving private build container | | Corrected native cases | Three touched native classes | 16 tests, 13 passed, 3 failed; reproduced stale-timestamp re-entry and corrected toast reference-frame sampling | | Aligned exact-base replay | Aligned legacy-source capture | 7 tests, 3 passed, 4 expected legacy-motion failures | | First cancellation cleanup | Fourteen-class batch | 87 tests, 85 passed, 2 held-scroll failures; cleanup reset velocity before the replacement claimed it | | Corrected frame-boundary cleanup | Fourteen-class batch | **87 tests passed; zero failures/errors** | The first implementation exposed a real frame-clock restart gap. The re-entry regression exposed stale animation time after unmount. The corrected cleanup waits independently until the next frame, without suspending the old mutation. Held-scroll assertions were not weakened. Native settling permits two pixels for the spring tail; fully settled poster/tile/See all images separately require maximum channel difference ≤2 from the original rendering. ## Native proof Root: `/root/.treehouse/firstmate-7bab20/2/firstmate/data/ui-polish-build/lane-e/`. - `P02/before/` and `P02/after/`: fade-in/out strips with matching approved copy; reduced-motion after strips and replacement/re-entry frames. - `E02/before/` and `E02/after/`: Right then row-Down position/shape strips, normal and reduced motion; native samples. - `E05/before/` and `E05/after/`: held-Right Home shelf and held-Down Library strips, normal and reduced motion; frame offsets/velocity samples. Full run notes and retained XML: `/root/.treehouse/firstmate-7bab20/2/firstmate/data/polish-lane-e/validation.md` and adjacent evidence directories. Proof uses native Compose/Robolectric rendering, not an installed APK or physical-device session. All Gradle runs used the shared-lock wrapper. No full suite, APK build, TV/ADB/emulator access, merges, or shared-infrastructure administration. AI assistance: implementation, native regressions and this review description were produced by the firstmate-managed coding agent. - Integration correction: merged current `origin/fm/tv-round2` without rewriting published history; `UpdatesKeysTest.classicRequiresEveryRetainedRowAndTheRealScroller` now waits for real list idle with at most an additional 320 ms spring-tail allowance, preserving the exact final index `4` assertion and all product scroll behavior. `bash docker/gradle.sh debug :app:testDefaultDebugUnitTest --tests org.justinvisser.wolfskin.tv.update.UpdatesKeysTest :wolfskin-design:testDebugUnitTest --tests org.justinvisser.wolfskin.design.ScrollMomentumTest` passed **12/12** (6 + 6), zero failures/errors, Gradle 4m29s / wall 275.68s; no full suite. The initial identical command using `:app:testDebugUnitTest` stopped before tests because that app task is flavor-ambiguous.
justinzeus added 2 commits 2026-11-08 03:00:21 +01:00
justinzeus added 2 commits 2026-11-08 08:03:05 +01:00
Author
Owner

Landed through the round 2 candidate #82 (merged into fm/tv-round2 as c21d17df). Closing as superseded; nothing from this branch is lost.

Landed through the round 2 candidate https://git.justinvisser.org/justinzeus/wolfskin/pulls/82 (merged into fm/tv-round2 as c21d17df). Closing as superseded; nothing from this branch is lost.
justinzeus closed this pull request 2026-11-08 13:59:33 +01:00

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
exploring
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: justinzeus/wolfskin#76
No description provided.