Clamp Discover text boxes and preserve recovery button height
All checks were successful
review/design-claude B1-B3 re-measured by firstmate from the lane measurement file against the locked numbers (all within 2 px); delta is the three scoped fixes plus an additive main merge; N1/N2 3-4 px offsets remain as listed follow-ups
All checks were successful
review/design-claude B1-B3 re-measured by firstmate from the lane measurement file against the locked numbers (all within 2 px); delta is the three scoped fixes plus an additive main merge; N1/N2 3-4 px offsets remain as listed follow-ups
This commit is contained in:
parent
bfd4002dd1
commit
130697c2eb
5 changed files with 56 additions and 7 deletions
|
|
@ -111,6 +111,12 @@ wired to a live server. Unknown request outcomes force a fresh title-requests re
|
||||||
instead of blindly submitting again, including after closing a sheet or choosing
|
instead of blindly submitting again, including after closing a sheet or choosing
|
||||||
the same uncertain recipient from another chooser. Whole-series requests require every season.
|
the same uncertain recipient from another chooser. Whole-series requests require every season.
|
||||||
|
|
||||||
|
Discover hero and Restore text use explicit per-line CSS allocations so native
|
||||||
|
paragraph overflow cannot enlarge the bottom-anchored stack. Restore subtitles
|
||||||
|
retain the sheet's negative top margin and bottom gap. Recovery panels may
|
||||||
|
overflow their fixed-height scroll row so sign-in copy never shrinks the retry
|
||||||
|
button. Native regressions cover those three layout boundaries.
|
||||||
|
|
||||||
Discover's native capture suite covers its 28 named acceptance states and emits
|
Discover's native capture suite covers its 28 named acceptance states and emits
|
||||||
text-layout metrics beside the PNGs. Data-driven Android key replays cover opening
|
text-layout metrics beside the PNGs. Data-driven Android key replays cover opening
|
||||||
Discover in 4 presses, requesting for yourself in 7, requesting for Mindel in 9 and
|
Discover in 4 presses, requesting for yourself in 7, requesting for Mindel in 9 and
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,10 @@ fun DiscoverOverlay(state: DiscoverState) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is DiscoverOverlayState.Restore -> {
|
is DiscoverOverlayState.Restore -> {
|
||||||
Text(overlay.title.title, color = C.fg, style = DiscoverType.sheetTitle)
|
Text(overlay.title.title, Modifier.discoverLineBox(DiscoverType.sheetTitle), color = C.fg, style = DiscoverType.sheetTitle)
|
||||||
Text("Hidden from ${state.shell.person.name}’s picks.", color = C.muted, style = DiscoverType.body)
|
Text("Hidden from ${state.shell.person.name}’s picks.",
|
||||||
|
Modifier.offset(y = D.sheetSubTop).padding(bottom = D.sheetSubGap + D.sheetSubTop).discoverLineBox(DiscoverType.body),
|
||||||
|
color = C.muted, style = DiscoverType.body)
|
||||||
Row(Modifier.padding(top = D.sheetButtonTop), horizontalArrangement = Arrangement.spacedBy(L.buttonGap)) {
|
Row(Modifier.padding(top = D.sheetButtonTop), horizontalArrangement = Arrangement.spacedBy(L.buttonGap)) {
|
||||||
DiscoverButton("Show again", overlay.focus == 0) { state.restore(overlay.title) }
|
DiscoverButton("Show again", overlay.focus == 0) { state.restore(overlay.title) }
|
||||||
DiscoverButton("Back", overlay.focus == 1, primary = false) { state.overlay = null }
|
DiscoverButton("Back", overlay.focus == 1, primary = false) { state.overlay = null }
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
|
@ -125,7 +126,7 @@ private fun DiscoverHero(state: DiscoverState) {
|
||||||
Text(if (state.snapshot.status == DiscoverStatus.Ready) "For you" else "Trending", color = C.accentText, style = DiscoverType.label)
|
Text(if (state.snapshot.status == DiscoverStatus.Ready) "For you" else "Trending", color = C.accentText, style = DiscoverType.label)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(D.heroKickerGap))
|
Spacer(Modifier.height(D.heroKickerGap))
|
||||||
Text(title.title, Modifier.width(L.heroCopyWidth), color = C.fg, style = DiscoverType.hero, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
Text(title.title, Modifier.width(L.heroCopyWidth).testTag("discover-hero-title-box").discoverLineBox(DiscoverType.hero), color = C.fg, style = DiscoverType.hero, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||||
Spacer(Modifier.height(D.heroTitleGap))
|
Spacer(Modifier.height(D.heroTitleGap))
|
||||||
Row(Modifier.height(D.heroFactsHeight), horizontalArrangement = Arrangement.spacedBy(D.heroFactsGap), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.height(D.heroFactsHeight), horizontalArrangement = Arrangement.spacedBy(D.heroFactsGap), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("${title.typeWord}${title.year?.let { " · $it" }.orEmpty()}", color = C.muted, style = DiscoverType.body)
|
Text("${title.typeWord}${title.year?.let { " · $it" }.orEmpty()}", color = C.muted, style = DiscoverType.body)
|
||||||
|
|
@ -166,7 +167,7 @@ private fun DiscoverChips(state: DiscoverState) {
|
||||||
private fun DiscoverStatePanel(state: DiscoverState) {
|
private fun DiscoverStatePanel(state: DiscoverState) {
|
||||||
val signIn = state.snapshot.status == DiscoverStatus.NeedsSignIn
|
val signIn = state.snapshot.status == DiscoverStatus.NeedsSignIn
|
||||||
Box(Modifier.fillMaxWidth().height(L.row_state)) {
|
Box(Modifier.fillMaxWidth().height(L.row_state)) {
|
||||||
Column(Modifier.padding(start = L.safeSide, top = D.stateTop).width(L.stateWidth)
|
Column(Modifier.padding(start = L.safeSide, top = D.stateTop).wrapContentHeight(Alignment.Top, unbounded = true).width(L.stateWidth)
|
||||||
.background(C.surface, RoundedCornerShape(WolfskinTokens.Radius.panel))
|
.background(C.surface, RoundedCornerShape(WolfskinTokens.Radius.panel))
|
||||||
.border(WolfskinTokens.Library.borderWidth, C.border, RoundedCornerShape(WolfskinTokens.Radius.panel))
|
.border(WolfskinTokens.Library.borderWidth, C.border, RoundedCornerShape(WolfskinTokens.Radius.panel))
|
||||||
.padding(WolfskinTokens.Library.borderWidth).padding(D.statePaddingX, D.statePaddingY)) {
|
.padding(WolfskinTokens.Library.borderWidth).padding(D.statePaddingX, D.statePaddingY)) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
// SPDX-License-Identifier: GPL-2.0-only
|
// SPDX-License-Identifier: GPL-2.0-only
|
||||||
package org.justinvisser.wolfskin.design
|
package org.justinvisser.wolfskin.design
|
||||||
|
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.FirstBaseline
|
||||||
|
import androidx.compose.ui.layout.LastBaseline
|
||||||
|
import androidx.compose.ui.layout.layout
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import kotlin.math.roundToInt
|
||||||
import androidx.compose.ui.text.PlatformTextStyle
|
import androidx.compose.ui.text.PlatformTextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.LineHeightStyle
|
import androidx.compose.ui.text.style.LineHeightStyle
|
||||||
|
|
@ -22,3 +28,14 @@ internal object DiscoverType {
|
||||||
val entry = WolfskinTokens.Discover.entryText.copy(lineHeightStyle = lineBox, platformStyle = platform)
|
val entry = WolfskinTokens.Discover.entryText.copy(lineHeightStyle = lineBox, platformStyle = platform)
|
||||||
val ratings = WolfskinTokens.Type.ratingPlate.copy(fontFeatureSettings = "tnum", lineHeightStyle = lineBox, platformStyle = platform)
|
val ratings = WolfskinTokens.Type.ratingPlate.copy(fontFeatureSettings = "tnum", lineHeightStyle = lineBox, platformStyle = platform)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Paragraph overflow must not expand the CSS block height. */
|
||||||
|
internal fun Modifier.discoverLineBox(style: TextStyle): Modifier = layout { measurable, constraints ->
|
||||||
|
val paragraph = measurable.measure(constraints)
|
||||||
|
val lineHeight = style.lineHeight.toPx()
|
||||||
|
val lines = ((paragraph[LastBaseline] - paragraph[FirstBaseline]) / lineHeight).roundToInt() + 1
|
||||||
|
val height = (lines * lineHeight).roundToInt().coerceIn(constraints.minHeight, constraints.maxHeight)
|
||||||
|
layout(paragraph.width, height) {
|
||||||
|
paragraph.placeRelative(0, (height - paragraph.height) / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.compose.ui.test.onRoot
|
import androidx.compose.ui.test.onRoot
|
||||||
import androidx.compose.ui.test.SemanticsMatcher
|
import androidx.compose.ui.test.SemanticsMatcher
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
import androidx.compose.ui.semantics.SemanticsProperties
|
import androidx.compose.ui.semantics.SemanticsProperties
|
||||||
import androidx.compose.ui.semantics.SemanticsActions
|
import androidx.compose.ui.semantics.SemanticsActions
|
||||||
import androidx.compose.ui.text.TextLayoutResult
|
import androidx.compose.ui.text.TextLayoutResult
|
||||||
|
|
@ -26,6 +28,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
|
@ -89,7 +92,14 @@ class DiscoverCaptureTest {
|
||||||
@Test fun st029_discover() = capture("st-029")
|
@Test fun st029_discover() = capture("st-029")
|
||||||
@Test fun st030_picksCollection() = capture("st-030") { state -> state.openCollection(state.lanes.first { it.id == "pk-movie" }) }
|
@Test fun st030_picksCollection() = capture("st-030") { state -> state.openCollection(state.lanes.first { it.id == "pk-movie" }) }
|
||||||
@Test fun st031_hidden() = capture("st-031") { hidden(it) }
|
@Test fun st031_hidden() = capture("st-031") { hidden(it) }
|
||||||
@Test fun st032_restore() = capture("st-032") { state -> hidden(state); state.overlay = DiscoverOverlayState.Restore(state.snapshot.picks[7]) }
|
@Test fun st032_restore() {
|
||||||
|
capture("st-032") { state -> hidden(state); state.overlay = DiscoverOverlayState.Restore(state.snapshot.picks[7]) }
|
||||||
|
val title = rule.onAllNodesWithText(fixture.snapshot("justin").picks[7].title, useUnmergedTree = true)
|
||||||
|
.fetchSemanticsNodes().maxBy { it.boundsInRoot.left }.boundsInRoot
|
||||||
|
val sub = rule.onNodeWithText("Hidden from Justin’s picks.", useUnmergedTree = true).fetchSemanticsNode().boundsInRoot
|
||||||
|
val gap = with(rule.density) { (WolfskinTokens.Discover.sheetGap + WolfskinTokens.Discover.sheetSubTop).toPx() }
|
||||||
|
assertTrue("Restore subtitle must retain the negative top margin", kotlin.math.abs(sub.top - title.bottom - gap) <= 1f)
|
||||||
|
}
|
||||||
@Test fun st033_failedBadge() = capture("st-033") { state ->
|
@Test fun st033_failedBadge() = capture("st-033") { state ->
|
||||||
val lane = state.lanes.first { it.id == "pk-movie" }
|
val lane = state.lanes.first { it.id == "pk-movie" }
|
||||||
state.requests[lane.titles[1].key] = listOf(DiscoverRequestResult(RequestPhase.Receipt, "Justin", "Failed", requester = "justin"))
|
state.requests[lane.titles[1].key] = listOf(DiscoverRequestResult(RequestPhase.Receipt, "Justin", "Failed", requester = "justin"))
|
||||||
|
|
@ -103,8 +113,21 @@ class DiscoverCaptureTest {
|
||||||
state.openCollection(DiscoverLane("trending", "Trending", titles, tail = DiscoverTail.ShowMore))
|
state.openCollection(DiscoverLane("trending", "Trending", titles, tail = DiscoverTail.ShowMore))
|
||||||
state.collection!!.index = state.collection!!.tailIndex
|
state.collection!!.index = state.collection!!.tailIndex
|
||||||
}
|
}
|
||||||
@Test fun st036_needsSignIn() = capture("st-036", snapshotTransform = { DiscoverSnapshot(status = DiscoverStatus.NeedsSignIn) })
|
@Test fun st036_needsSignIn() {
|
||||||
@Test fun st037_series() = capture("st-037", person = "mindel") { it.filter = "tv" }
|
capture("st-036", snapshotTransform = { DiscoverSnapshot(status = DiscoverStatus.NeedsSignIn) })
|
||||||
|
val button = rule.onNodeWithText("Try again").fetchSemanticsNode().boundsInRoot
|
||||||
|
val minimum = with(rule.density) { WolfskinTokens.Layout.buttonHeight.toPx() }
|
||||||
|
assertTrue("The sign-in button must not shrink to fit the scroll row", button.height >= minimum)
|
||||||
|
}
|
||||||
|
@Test fun st037_series() {
|
||||||
|
capture("st-037", person = "mindel") { it.filter = "tv" }
|
||||||
|
val title = rule.onNodeWithText("56 Days", useUnmergedTree = true).fetchSemanticsNode()
|
||||||
|
val layouts = mutableListOf<TextLayoutResult>()
|
||||||
|
title.config[SemanticsActions.GetTextLayoutResult].action?.invoke(layouts)
|
||||||
|
val layout = layouts.single()
|
||||||
|
val cssHeight = with(rule.density) { DiscoverType.hero.lineHeight.toPx() } * layout.lineCount
|
||||||
|
assertTrue("Natural font height must not inflate the hero CSS line box", kotlin.math.abs(title.boundsInRoot.height - cssHeight) <= 1f)
|
||||||
|
}
|
||||||
@Test fun st038_noSeeds() = capture("st-038", snapshotTransform = { it.copy(status = DiscoverStatus.NoSeeds) })
|
@Test fun st038_noSeeds() = capture("st-038", snapshotTransform = { it.copy(status = DiscoverStatus.NoSeeds) })
|
||||||
@Test fun st039_trendingCollection() = capture("st-039") { state -> state.openCollection(state.lanes.first { it.id == "trending" }) }
|
@Test fun st039_trendingCollection() = capture("st-039") { state -> state.openCollection(state.lanes.first { it.id == "trending" }) }
|
||||||
@Test fun st040_service() = capture("st-040") { state -> state.openService(state.snapshot.services.first()) }
|
@Test fun st040_service() = capture("st-040") { state -> state.openService(state.snapshot.services.first()) }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue