diff --git a/docker/README.md b/docker/README.md index f9c0cc17..583d4d4b 100644 --- a/docker/README.md +++ b/docker/README.md @@ -358,18 +358,28 @@ chips stay at y 252–320 and the first poster row starts at y 372 (52 px clear) Discover and season-chip rows retain their 104 px height. Cards and cast circles draw a single 4 px accent stroke centred on the art edge, -above the content, with no background gap and the existing card lift. The stroke -and art scale together at 1.08; buttons, avatars, chips and picker focus are -unchanged. Title shelves reserve vertical drawing room for the scaled ring. +above the content, with no background gap. Card lift parameters are unchanged; +cast portraits and episode tiles now receive that lift too. The stroke and art +scale together at 1.08; buttons, avatars, chips and picker focus are unchanged. +Title shelves reserve vertical drawing room for the scaled ring. Before scaling the stroke, ring-to-neighbour clearances round to 28/22/16 px for poster/tile/wide cards; including stroke scale they are 27.6/21.8/16.1 px. +Poster progress uses the independent 18 px title-card inset, not caption spacing: +the track remains 220 px wide on a 256 px poster. Library focus-follow restarts +when the effective column count, focused row or header geometry changes, retaining +deep focus across live snapshots that add or remove the letter rail. +`LibrarySpacingRegressionTest` renders both progress geometry and the unchanged +focused index across the 199↔200-title boundary. + `LibraryCaptureTest.spacingShowsFirstFocused`, `HomeCaptureTest.spacingNextUpFirstFocused` and `TitleCaptureTest.spacingCastFirstFocused` render the amended reference states at native resolution. Their files are `library/spacing-shows.png`, `home/spacing-next-up.png` and `st-114.png` under `build/wolfskin-captures/`. -Platform Roboto metrics and fixture ordering are not browser pixel parity. +These captures are recordings, not automated reference-pixel assertions; pair +measurements are a separate check. Platform Roboto metrics and fixture ordering +are not browser pixel parity. ## Title pages and people diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryPoster.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryPoster.kt index 27374461..41e1e053 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryPoster.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryPoster.kt @@ -41,8 +41,8 @@ fun LibraryPoster( if (title.ratings.isNotEmpty()) LibraryRatingPlate(title.ratings, visible = ratingsVisible) val progress = title.progress if (progress > 0f && progress < 1f) { - Box(Modifier.align(Alignment.BottomCenter).padding(horizontal = WolfskinTokens.Layout.captionOffset, vertical = WolfskinTokens.Layout.navItemGap) - .fillMaxWidth().height(WolfskinTokens.Layout.progressBar).background(C.progressTrack, CircleShape)) { + Box(Modifier.align(Alignment.BottomCenter).padding(horizontal = WolfskinTokens.Title.cardProgressInset, vertical = WolfskinTokens.Layout.navItemGap) + .fillMaxWidth().height(WolfskinTokens.Layout.progressBar).testTag("poster-progress-${title.id}").background(C.progressTrack, CircleShape)) { Box(Modifier.fillMaxHeight().fillMaxWidth(progress).background(C.accent, CircleShape)) } } diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryScreen.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryScreen.kt index 93fb47a0..fa339d43 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryScreen.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/LibraryScreen.kt @@ -88,13 +88,13 @@ fun LibraryScreen( } val headerCount = if (grid.hasToolbar && grid.status != LibraryStatus.Loading && grid.status != LibraryStatus.Unavailable) 2 else 1 val rowHeight = K.posterH + K.posterCaptionBand + L.gridRowGap - LaunchedEffect(grid.index, grid.toolbar, grid.inRail, grid.railLetter, shell.navFocused, view) { - val index = if (grid.inRail) items.indexOfFirst { it.letter == grid.railLetter } else grid.index - val row = index.coerceAtLeast(0) / grid.columns - if (grid.toolbar || row == 0 && active) { + val focusedRow = (if (grid.inRail) items.indexOfFirst { it.letter == grid.railLetter } else grid.index).coerceAtLeast(0) / grid.columns + LaunchedEffect(grid.index, grid.columns, focusedRow, headerCount, rowHeight, density, + grid.toolbar, grid.inRail, grid.railLetter, shell.navFocused, view) { + if (grid.toolbar || focusedRow == 0 && active) { animateLibraryScroll(view, headerCount, 0f, density) - } else if (row > 0) { - val target = with(density) { (L.row_lhead + (if (headerCount == 2) L.row_chipsGrid else WolfskinTokens.Shell.zero) + rowHeight * row - L.pinnedLine).toPx() } + } else if (focusedRow > 0) { + val target = with(density) { (L.row_lhead + (if (headerCount == 2) L.row_chipsGrid else WolfskinTokens.Shell.zero) + rowHeight * focusedRow - L.pinnedLine).toPx() } animateLibraryScroll(view, headerCount, target, density) } } diff --git a/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/LibrarySpacingRegressionTest.kt b/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/LibrarySpacingRegressionTest.kt new file mode 100644 index 00000000..79e89277 --- /dev/null +++ b/wolfskin-design/src/test/java/org/justinvisser/wolfskin/design/LibrarySpacingRegressionTest.kt @@ -0,0 +1,53 @@ +package org.justinvisser.wolfskin.design + +import android.app.Application +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** Caption spacing and live rail changes must preserve poster geometry and retained focus. */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi") +class LibrarySpacingRegressionTest { + @get:Rule val rule = createComposeRule() + + @Test fun partiallyWatchedPosterKeepsItsOriginalProgressInsets() { + val title = LibraryTitle("partial", "Partially watched", "movie", progress = .5f) + rule.setContent { WolfskinTheme { LibraryPoster(title, false, false, false) } } + val poster = rule.onNodeWithTag("poster-partial").fetchSemanticsNode().boundsInRoot + val track = rule.onNodeWithTag("poster-progress-partial").fetchSemanticsNode().boundsInRoot + assertEquals(256f, poster.width, .1f) + assertEquals(220f, track.width, .1f) + assertEquals(18f, track.left - poster.left, .1f) + assertEquals(18f, poster.right - track.right, .1f) + } + + @Test fun crossingRailThresholdKeepsTheSameDeepFocusedPosterVisible() { + val titles = List(200) { LibraryTitle("$it", "Title ${it.toString().padStart(3, '0')}", "tv") } + val shell = ShellState(fixturePeople(), "justin").apply { pickerVisible = false; enterDestination("shows") } + val grid = LibraryGridState("Shows", titles.take(199)).apply { index = 198 } + rule.setContent { WolfskinTheme { ShellSurface(shell) { LibraryScreen(shell, grid, it) } } } + rule.mainClock.advanceTimeBy(1000) + rule.waitForIdle() + val poster = rule.onNodeWithTag("poster-198") + poster.assertIsDisplayed() + val original = poster.fetchSemanticsNode().boundsInRoot + for (count in listOf(200, 199)) { + rule.runOnIdle { grid.source = titles.take(count) } + rule.mainClock.advanceTimeBy(1000) + rule.waitForIdle() + poster.assertIsDisplayed() + val current = poster.fetchSemanticsNode().boundsInRoot + assertEquals(198, grid.index) + assertEquals("198", grid.focusedTitle!!.id) + assertEquals(original.top, current.top, 1f) + assertEquals(original.height, current.height, 1f) + } + } +}