Skip to content

bugfix-navigation - The one about Navigation Drift (refactor) - #1414

Merged
RadicalMuffinMan merged 2 commits into
Moonfin-Client:mainfrom
mattsigal:fix/grid-focus-drift-issue-1356
Sep 5, 2026
Merged

bugfix-navigation - The one about Navigation Drift (refactor)#1414
RadicalMuffinMan merged 2 commits into
Moonfin-Client:mainfrom
mattsigal:fix/grid-focus-drift-issue-1356

Conversation

@mattsigal

Copy link
Copy Markdown
Collaborator

Pull Request

Summary

This is a pretty big swing but I think it is worth it. Prompted by issue #1356, I looked into what was responsible for the drift and found that, while the problem is magnified on tvOS (details below), it also impacts Desktop (with keyboard/remote navigation) and AndroidTV. The goal of this PR is to prevent focus indicator drift during vertical scrolling and viewport yank-back during alphabet letter jumps in the library browse grid. Since this is a pretty impactful PR, here are additional details about the root of the issue and why it should be addressed.

Background & Problem

When navigating vertical library grids, focused cards scale up to indicate selection (MediaCard.focusScale). To prevent focused cards from visually clipping neighboring elements or top screen bounds, the vertical sliver layout reserves spacing dynamically in library_browse_screen.dart:

  • focusOverhang = isMobile ? 0.0 : MediaCard.focusGap(cellHeight, minimum: 0.0)
  • rowSpacing = math.max(8.0, focusOverhang)
  • gridTopPadding = 8.0 + focusOverhang

On tvOS, Apple TV guidelines specify a larger focus expansion (focusScale = 1.12), producing a ~16.92px overhang for standard poster cards (cellHeight ≈ 282px). On Android TV and Desktop, focusScale = 1.05. For standard cards on those platforms, focusOverhang is 7.05px (clamped to 8.0px), but for "Large" poster preferences or "Banner" views (cellHeight > 320px), focusOverhang exceeds 8.0px (e.g., 9.0px at 360px).

However, the programmatic focus scrolling method _scrollToGridRow was not using these dynamic values. At its invocation in _buildGridCard, mainAxisSpacing was hardcoded to 8.0, and gridTopPadding defaulted to 8.0. This created a progressive mathematical divergence between where the sliver grid actually positioned cards on screen and where _scrollToGridRow thought cards were located:

  • On tvOS: An ~8.92px error accumulated per row. At row 30, the calculated offset was ~267px too high; at row 100, it was ~892px too high. As the user navigated downward, _scrollToGridRow falsely determined that the card was above the top viewport boundary and scrolled in reverse (upward), driving the focused card off the bottom of the screen.
  • On Android TV & Desktop: Although standard posters coincidentally matched the hardcoded 8.0px, viewing Large cards or Banner layouts suffered progressive drift (e.g. 109px drift over 100 rows). In addition, an ~7px top-padding offset error was present on all non-mobile platforms.
  • During Alphabet Letter Jumps: When jumping to a distant letter (e.g. 'M'), _jumpToLetter correctly computed the scroll destination using _gridGeometry and settled the viewport. However, 150ms later, the target card was focused, triggering onFocus and calling _scrollToGridRow with the faulty 8.0px formula. This immediately yanked the viewport backward, throwing the freshly selected letter target off screen.

Architectural Solution

To ensure long-term stability across all platforms (tvOS, Android TV, Desktop) and display configurations (Standard, Large, Banner), this PR refactors _scrollToGridRow to treat the active layout geometry (_gridGeometry) as the single source of truth:

  1. Unified Geometry Source: _scrollToGridRow now derives perLine, lineExtent, lineSpacing, and leadingPad directly from _gridGeometry. The scroll controller and SliverGridDelegateWithFixedCrossAxisCount now share identical mathematical coordinates without maintaining duplicated formulas.
  2. Dynamic Fallbacks: _buildGridCard now accepts and forwards dynamic rowSpacing and gridTopPadding (8 + focusOverhang) so that any fallback calculations also mirror the rendered layout.
  3. Alphabet Jump Mutual Exclusion: _scrollToGridRow and card onFocus now guard against execution while _isJumpingToLetter is active, preventing competing scroll animations when landing on letter targets.
  4. Top Edge Clearance: Adjusted viewport topPad in _scrollToGridRow to effectiveLeadingPad, ensuring the top-row card glow and 12% tvOS focus expansion are never clipped by the upper screen edge.

Related Issues

Link related issues or tickets separated by commas.

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Performance improvement
  • UI/UX update
  • Documentation update
  • Build/CI change
  • Other (describe):

Changes Made

List the key changes included in this PR.

  • Refactored _scrollToGridRow in library_browse_screen.dart to consume _gridGeometry as the single source of truth for grid line calculations.
  • Forwarded dynamic rowSpacing and gridTopPadding from _buildVerticalGrid through _buildGridCard to replace legacy hardcoded 8.0px values.
  • Guarded _scrollToGridRow and card onFocus against firing while _isJumpingToLetter is true, preventing viewport yankback after letter navigation.
  • Updated top viewport boundary padding (topPad) to effectiveLeadingPad to preserve focus glow clearance at the top of the grid.
  • Added library_browse_grid_focus_test.dart covering row coordinate parity across 200 rows under tvOS (1.12 scale), Android TV, and Desktop (1.05 scale and Large card layouts).

Platform

  • Android
  • iOS
  • tvOS
  • Web
  • macOS
  • Windows
  • Linux
  • All / Shared code

Testing

Describe how this change was tested.

  • Tested on emulator / simulator
  • Tested on physical device
  • Manual testing completed
  • Not tested (explain why):

Test Steps

  1. Open a media library containing >150 items on tvOS, Android TV, or Desktop (with keyboard/remote navigation).
  2. Continuously scroll downward with D-pad/arrow keys through 100+ items; confirm the focused poster stays steadily centered within the safe-zone and does not drift off-screen.
  3. In Display Settings, switch poster size to "Large" and verify zero drift across 100+ rows.
  4. Navigate to the alphabet jump bar and select a distant letter (e.g. 'M'); confirm the viewport smoothly lands on the target letter and focuses the first item without being yanked backward.
  5. Run automated unit tests: flutter test test/ui/library_browse_grid_focus_test.dart and flutter test test/focus/.

Screenshots (if applicable)

Include screenshots or recordings for UI changes.

Focus indicactor remains on screen and movement is smooth (except for when I hit loading transitions):

2026-09-05_10-00-17_PotPlayer64.mp4

Checklist

  • Code builds successfully
  • Code follows project style and conventions
  • No unnecessary commented-out code
  • No new warnings introduced

…alignment

- Derive _scrollToGridRow positioning directly from _gridGeometry
  (perLine, lineExtent, lineSpacing, leadingPad) to maintain single
  source of truth with SliverGridDelegateWithFixedCrossAxisCount.
- Forward dynamic rowSpacing and gridTopPadding through _buildGridCard
  to account for focusOverhang across tvOS (1.12 scale), Android TV,
  and Desktop (Large card settings and Banner layouts).
- Guard _scrollToGridRow and card onFocus against running while
  _isJumpingToLetter is active, preventing viewport conflicts during
  alphabet jumps.
- Adjust viewport topPad to effectiveLeadingPad to prevent clipping
  of the scaled top-row card glow.
- Add test/ui/library_browse_grid_focus_test.dart covering row offset
  parity across 200 rows under multiple platform focus scales.

Fixes Moonfin-Client#1356
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

✅ Build Successful

All platform builds passed. You can download the test artifacts below.

Platform Status Artifact
Android ✅ Passed Moonfin_Android_v* + Moonfin_AndroidTV_v*
iOS ✅ Passed Moonfin_iOS_v*_unsigned.ipa
macOS ✅ Passed Moonfin_macOS_v*.dmg
Windows x64 ✅ Passed Moonfin_Windows_v*.exe
Windows ARM64 ✅ Passed Moonfin_WindowsARM64_v*.exe
Linux x64 ✅ Passed Moonfin_Linux_v* (deb/rpm/AppImage/snap/flatpak/tar.gz)
Linux ARM64 ✅ Passed Moonfin_LinuxARM64_v* (deb/rpm/AppImage/snap/flatpak/tar.gz)
Property Value
Commit af93634
Workflow run Build #1295

@mattsigal mattsigal self-assigned this Sep 5, 2026
…he grid actually drew, drop the letter jump guards that were already cleared by the time focus landed, and back it with tests that measure a real sliver instead of repeating the formula
@RadicalMuffinMan
RadicalMuffinMan merged commit 72b8288 into Moonfin-Client:main Sep 5, 2026
7 checks passed
@mattsigal
mattsigal deleted the fix/grid-focus-drift-issue-1356 branch September 6, 2026 07:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[UI] Highlighted/Selected poster gradually moves off the screen when scrolling through items

2 participants