Remove the unfocused watched control ring #69
4 changed files with 39 additions and 1 deletions
|
|
@ -24,6 +24,8 @@ import org.json.JSONObject
|
|||
import org.junit.Before
|
||||
import org.junit.After
|
||||
import org.junit.Rule
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.justinvisser.wolfskin.design.LocalTitleBitmaps
|
||||
|
|
@ -225,4 +227,27 @@ class TitleCaptureTest {
|
|||
@Test fun round2NotHeldSeries() = capture(123, "90 Day Fiancé: Before the 90 Days")
|
||||
@Test fun round2HeldPosterFallback() = capture(124, "The Girl Who Leapt Through Time", transform = { it.copy(item = it.item.copy(backdrop = null, isPosterBackdrop = true, overview = "Short overview.")) })
|
||||
@Test fun round2NotHeldShortOverview() = capture(125, "The Amazing Spider-Man", transform = { it.copy(item = it.item.copy(backdrop = null, isPosterBackdrop = true, overview = "Short overview.")) })
|
||||
|
||||
@Test fun watchedRingBelongsOnlyToFocus() {
|
||||
lateinit var state: TitleState
|
||||
capture(126) { current, _ -> state = current; state.watched = false; option(state, "tracks") }
|
||||
val edges = mutableListOf<List<Int>>()
|
||||
for ((index, configuration) in listOf(false to false, true to false, false to true, true to true).withIndex()) {
|
||||
val (watched, focused) = configuration
|
||||
rule.runOnIdle { state.watched = watched; option(state, if (focused) "watched" else "tracks") }
|
||||
save(126 + index)
|
||||
val bounds = rule.onNodeWithTag("title-option-watched").fetchSemanticsNode().boundsInRoot
|
||||
val file = File(requireNotNull(System.getProperty("wolfskin.captureDirectory")), "st-${126 + index}.png")
|
||||
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
|
||||
val edge = (-8..8).map { offset -> bitmap.getPixel(bounds.left.toInt() + offset, bounds.center.y.toInt()) }
|
||||
edges.add(edge)
|
||||
bitmap.recycle()
|
||||
}
|
||||
val accent = WolfskinTokens.Color.accent.toArgb()
|
||||
assertEquals("Unwatched at rest has no ring", 0, edges[0].count { it == accent })
|
||||
assertEquals("Watched at rest has no ring", 0, edges[1].count { it == accent })
|
||||
assertEquals("Toggling watched must not change the resting edge", edges[0], edges[1])
|
||||
assertTrue("Keyboard focus keeps its accent ring", edges[2].any { it == accent })
|
||||
assertEquals("Watched keeps exactly the same focused ring", edges[2], edges[3])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,6 +140,12 @@ Justin's additional correction is binding even where a reference frame leaks: **
|
|||
|
||||
Native same-fixture comparisons and the exhaustive real-art bottom-row proof are published in [the card metadata build gallery](https://pages.justinvisser.org/wolfskin/tv-card-meta-build/). `CardBottomPixelTest` checks white Annie Hall, dark Locke, colourful Apocalypse Now and Avatar Aang at rest and individually focused; every pixel across each full bottom row must equal an art-free rendering of the same clipped backing/ring, while straight-edge and fill/track pixels additionally match exact state colours. It also checks Home/library parity when progress reaches or exceeds completion. `CardGeometryTest` measures narrow fitting, the 24 px check, grid/shelf pitches, pinned rows and ring clearances to 0.1 px, and asserts every pixel in reserved loading caption bands remains background-coloured. `CardCaptionMotionTest` compares native glyph pixels after changing a focused title and moving across a focused Home shelf. Reference comparisons retain documented renderer/fixture artifacts; unapproved visible design changes remain merge blockers.
|
||||
|
||||
## Details watched control
|
||||
|
||||
The on state has no unfocused ring: Justin (2026-11-07) read it as active/selected. This overrides the older IconButton “on = inset 3 px accent ring” and Watched “inset accent ring” descriptions in the written specification. The existing open-eye versus crossed-eye glyph, “Watched” header text, and “Mark as unwatched” action caption distinguish the state without a new colour or token. The focus ring, scale, fill and selected accessibility semantics are unchanged.
|
||||
|
||||
Source — Justin (2026-11-07): “the watches icon on details pages has a border, making it look like it's active/selected.”
|
||||
|
||||
## Sources and verification
|
||||
|
||||
Authoritative changed values live in `wolfskin-design/tokens.json`, `title-tokens.json` and `discover-tokens.json`; regenerate Kotlin, CSS and the drift manifest with `python3 wolfskin-design/generate_tokens.py`.
|
||||
|
|
|
|||
|
|
@ -819,6 +819,14 @@ appear only in the header facts line, not again beside progress. Explicit episod
|
|||
pages use the opened episode's own set, including its `Ep` score; movie and series
|
||||
pages keep their own sets.
|
||||
|
||||
The watched on state has no unfocused ring: Justin (2026-11-07) read it as
|
||||
active/selected — “the watches icon on details pages has a border, making it
|
||||
look like it's active/selected.” The open-eye/crossed-eye glyph, Watched header
|
||||
and Mark as unwatched caption still communicate the state. The shared focus
|
||||
ring is unchanged. `TitleCaptureTest.watchedRingBelongsOnlyToFocus` records the
|
||||
same Details row in all four watched/focus combinations and checks its rendered
|
||||
edge pixels.
|
||||
|
||||
Episode shelf skeletons use the same width, height and pitch as ready cards.
|
||||
Person filmography and related-title shelves remain posters; Home continuation
|
||||
tiles are a separate component and retain their series-name context.
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ fun TitleControl(option: TitleOption, focused: Boolean, modifier: Modifier = Mod
|
|||
.semantics { if (option.icon) contentDescription = option.label; role = Role.Button; selected = option.on }
|
||||
.focusRing(focused, WolfskinTokens.Radius.button, WolfskinTokens.Focus.ringControlRing, FocusLift.Button)
|
||||
.background(background, shape)
|
||||
.then(if (option.on && !focused) Modifier.border(D.radioBorder, C.accent, shape) else Modifier)
|
||||
.clickable { focusOwner?.invoke(); onClick() }.then(dimensions),
|
||||
horizontalArrangement = Arrangement.spacedBy(D.iconGap, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (option.glyph.isNotEmpty()) Glyph(option.glyph, foreground, Modifier.size(if (option.icon) D.iconSize else L.buttonIcon))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue