Unify focus rings on the approved accent straddle style

This commit is contained in:
justinzeus 2026-11-06 15:52:02 +01:00
parent 93a41ea2d3
commit ef3ae3f21b
18 changed files with 173 additions and 54 deletions

View file

@ -90,6 +90,7 @@ class DiscoverCaptureTest {
}
@Test fun st029_discover() = capture("st-029")
@Test fun focusFilterChip() = capture("st-120") { it.focus(1, 0) }
@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 st032_restore() {

View file

@ -0,0 +1,84 @@
package org.justinvisser.wolfskin.design
import android.app.Application
import android.graphics.BitmapFactory
import android.view.View
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.unit.dp
import com.github.takahirom.roborazzi.captureRoboImage
import java.io.File
import org.junit.Assert.assertTrue
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
import org.robolectric.annotation.GraphicsMode
/** Opaque content must not cover either half of the shared accent focus stroke. */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class FocusRingTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test fun ringStraddlesOpaqueSurfaceAndDisappearsWithoutFocus() {
rule.activity.window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
@Suppress("DEPRECATION")
rule.activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
val focused = mutableStateOf(true)
val radius = mutableStateOf(WolfskinTokens.Radius.button)
rule.setContent {
Box(Modifier.requiredSize(120.dp).background(WolfskinTokens.Color.bg)) {
Box(Modifier.offset(40.dp, 40.dp).size(60.dp)
.focusRing(focused.value, radius.value, WolfskinTokens.Focus.ringControlRing)
.background(WolfskinTokens.Color.surface, RoundedCornerShape(radius.value)))
}
}
val file = File.createTempFile("focus-ring-", ".png")
try {
for (corner in listOf(WolfskinTokens.Radius.button, 30.dp, WolfskinTokens.Radius.card)) {
rule.runOnIdle { radius.value = corner }
rule.mainClock.advanceTimeBy(1000)
rule.waitForIdle()
rule.onRoot().captureRoboImage(file.absolutePath)
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
for ((x, expected) in listOf(77 to WolfskinTokens.Color.bg, 78 to WolfskinTokens.Color.accent,
79 to WolfskinTokens.Color.accent, 80 to WolfskinTokens.Color.accent,
81 to WolfskinTokens.Color.accent, 82 to WolfskinTokens.Color.surface)) {
assertPixel("Left edge x=$x, radius=$corner", expected.toArgb(), bitmap.getPixel(x, 140))
assertPixel("Top edge y=$x, radius=$corner", expected.toArgb(), bitmap.getPixel(140, x))
}
bitmap.recycle()
}
rule.runOnIdle { focused.value = false }
rule.mainClock.advanceTimeBy(1000)
rule.waitForIdle()
rule.onRoot().captureRoboImage(file.absolutePath)
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
assertEquals(WolfskinTokens.Color.bg.toArgb(), bitmap.getPixel(79, 140))
assertEquals(WolfskinTokens.Color.surface.toArgb(), bitmap.getPixel(80, 140))
bitmap.recycle()
} finally { file.delete() }
}
private fun assertPixel(label: String, expected: Int, actual: Int) {
// Circular edge coverage can differ by a few channel values at pixel centres.
for (shift in listOf(0, 8, 16, 24)) {
val difference = kotlin.math.abs((expected ushr shift and 255) - (actual ushr shift and 255))
assertTrue("$label: channel $shift differs by $difference", difference <= 8)
}
}
}

View file

@ -116,5 +116,6 @@ class HomeCaptureTest {
@Test fun st102_filmTile() = capture("st-102", person = "mindel", row = "new", column = 4)
@Test fun st103_heroRatings() = capture("st-103")
@Test fun spacingNextUpFirstFocused() = capture("spacing-next-up", row = "nu")
@Test fun focusHeroDetails() = capture("focus-hero-details", column = 1)
@Test fun xHomeDiscoverStart() = capture("x-home-discover-start", row = "pk-m")
}

View file

@ -83,6 +83,7 @@ class LibraryCaptureTest {
@Test fun st113_uncachedWhileDown() = capture("st-113", reachable = false)
@Test fun unsignedRatingsHidden() = capture("library-unsigned", ratings = false)
@Test fun spacingShowsFirstFocused() = capture("spacing-shows", "mindel", "shows")
@Test fun focusToolbarChip() = capture("focus-toolbar-chip") { it.toolbar = true; it.tool = 0 }
@Test fun spacingLetterRailClearsFocusedFifthColumn() = capture("spacing-letter-rail") { grid ->
val original = grid.source
grid.source = List(200) { index ->

View file

@ -54,6 +54,7 @@ class ShellCaptureTest {
}
@Test fun st001_launch() = capture("st-001")
@Test fun focusNavAvatar() = capture("focus-nav-avatar") { it.pickerVisible = false; it.focusNav(it.tabs.size) }
@Test fun st002_gear() {
capture("st-002") { it.pickerIndex = 2; it.gearFocused = true }
val layouts = mutableListOf<TextLayoutResult>()