From d73796742c1a4dee641576965320c0e086726c21 Mon Sep 17 00:00:00 2001 From: justinzeus Date: Sat, 7 Nov 2026 23:23:39 +0100 Subject: [PATCH] Remove the unfocused watched control ring --- .../wolfskin/tv/TitleCaptureTest.kt | 25 +++++++++++++++++++ data/tv-spec/spec.md | 6 +++++ docker/README.md | 8 ++++++ .../wolfskin/design/TitleControl.kt | 1 - 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleCaptureTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/TitleCaptureTest.kt index 01a166a0..beac9c16 100644 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/TitleCaptureTest.kt +++ b/app/src/test/java/org/justinvisser/wolfskin/tv/TitleCaptureTest.kt @@ -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>() + 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]) + } } diff --git a/data/tv-spec/spec.md b/data/tv-spec/spec.md index 742f81c3..0a22d47c 100644 --- a/data/tv-spec/spec.md +++ b/data/tv-spec/spec.md @@ -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`. diff --git a/docker/README.md b/docker/README.md index 72f7bf1d..34319a6f 100644 --- a/docker/README.md +++ b/docker/README.md @@ -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. diff --git a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/TitleControl.kt b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/TitleControl.kt index ee5054c5..2441c8ed 100644 --- a/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/TitleControl.kt +++ b/wolfskin-design/src/main/java/org/justinvisser/wolfskin/design/TitleControl.kt @@ -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))