Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
6.5 kB · 167 lines
Kotlin
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168package social.grain
import android.graphics.Bitmapimport android.graphics.Canvasimport android.graphics.Paintimport androidx.compose.material3.ColorSchemeimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.darkColorSchemeimport androidx.compose.material3.lightColorSchemeimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.graphics.toArgbimport androidx.compose.ui.test.junit4.v2.createComposeRuleimport androidx.test.ext.junit.runners.AndroidJUnit4import androidx.test.platform.app.InstrumentationRegistryimport org.junit.Assert.assertTrueimport org.junit.Ruleimport org.junit.Testimport org.junit.runner.RunWithimport social.grain.ui.theme.GrainThemeimport java.io.File
/** * The fallback palettes — what Android 11 and below get, and anyone with * dynamic colour off. * * A ColorScheme role that is never passed silently keeps Material's baseline * value, and the baseline neutrals are tinted toward Material's own purple. So * leaving surfaceContainer unset didn't fail anything; it just made the * navigation bar and the comment sheet faintly lavender against surfaces that * are otherwise pure white or near-black. Nothing catches that but eyes on an * old device, which is what this test replaces. */@RunWith(AndroidJUnit4::class)class FallbackPaletteTest {
@get:Rule val composeRule = createComposeRule()
/** * The surface and outline roles Grain means to own. Every one of these * draws a real surface in the app — surfaceContainer is the navigation * bar, surfaceContainerLow the comment sheet, surfaceContainerHighest a * filled text field, outline the chip and outlined-button borders. */ private val ownedRoles: List<Pair<String, ColorScheme.() -> Color>> = listOf( "surfaceContainerLowest" to { surfaceContainerLowest }, "surfaceContainerLow" to { surfaceContainerLow }, "surfaceContainer" to { surfaceContainer }, "surfaceContainerHigh" to { surfaceContainerHigh }, "surfaceContainerHighest" to { surfaceContainerHighest }, "surfaceBright" to { surfaceBright }, "surfaceDim" to { surfaceDim }, "inverseSurface" to { inverseSurface }, "inverseOnSurface" to { inverseOnSurface }, "outline" to { outline }, )
/** * The subset that paints a surface. `outline` is excluded on purpose: it * is a border, not a background, and Grain's own is a deliberately cool * grey well past the neutrality bar below. */ private val surfaceRoles = ownedRoles.filterNot { it.first == "outline" }
/** Both fallback schemes, captured in one composition. */ private fun schemes(): Pair<ColorScheme, ColorScheme> { lateinit var light: ColorScheme lateinit var dark: ColorScheme composeRule.setContent { GrainTheme(darkTheme = false, dynamicColor = false) { light = MaterialTheme.colorScheme } GrainTheme(darkTheme = true, dynamicColor = false) { dark = MaterialTheme.colorScheme } } composeRule.waitForIdle() return light to dark }
/** * How far apart a colour's strongest and weakest channels are — 0 is a * true grey. A cheap stand-in for chroma, and unlike HSV saturation it * stays meaningful down at the near-black end of the dark palette. */ private fun Color.channelSpread(): Int { val argb = toArgb() val r = (argb shr 16) and 255 val g = (argb shr 8) and 255 val b = argb and 255 return maxOf(r, g, b) - minOf(r, g, b) }
private fun assertSurfacesAreNeutral(scheme: ColorScheme, label: String) { val tinted = surfaceRoles .map { (name, role) -> name to scheme.role().channelSpread() } .filter { (_, spread) -> spread > MAX_SPREAD } assertTrue( "$label surfaces carry a colour cast: " + tinted.joinToString { "${it.first} spread ${it.second}" }, tinted.isEmpty(), ) }
@Test fun fallbackSurfacesAreNeutral() { val (light, dark) = schemes() assertSurfacesAreNeutral(light, "light") assertSurfacesAreNeutral(dark, "dark") }
/** * Dumps both palettes beside Material's baseline so the difference can be * looked at rather than only asserted. Pull it with: * * adb shell run-as social.grain.debug cat files/fallback_palette.png */ @Test fun dumpPaletteComparison() { val (lightScheme, darkScheme) = schemes() val rows = listOf( "light grain" to lightScheme, "light baseline" to lightColorScheme(), "dark grain" to darkScheme, "dark baseline" to darkColorScheme(), ) val swatch = 120 val labelWidth = 420 val bitmap = Bitmap.createBitmap( labelWidth + swatch * ownedRoles.size, swatch * rows.size, Bitmap.Config.ARGB_8888, ) val canvas = Canvas(bitmap) canvas.drawColor(android.graphics.Color.DKGRAY) val text = Paint().apply { color = android.graphics.Color.WHITE textSize = 32f isAntiAlias = true } rows.forEachIndexed { row, (label, scheme) -> val top = row * swatch canvas.drawText(label, 16f, top + swatch / 2f + 12f, text) ownedRoles.forEachIndexed { col, (_, role) -> val left = labelWidth + col * swatch canvas.drawRect( left.toFloat(), top.toFloat(), (left + swatch).toFloat(), (top + swatch).toFloat(), Paint().apply { color = scheme.role().toArgb() }, ) } } val context = InstrumentationRegistry.getInstrumentation().targetContext File(context.filesDir, "fallback_palette.png").outputStream() .use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) } }
private companion object { /** * Grain's surface roles measure 0-6 apart; Material's baseline ones * measure 8-10 in light (surfaceContainer #F3EDF7, surfaceBright * #FEF7FF) and 5-9 in dark. Six separates the two, so dropping any of * the overrides fails this rather than merely looking slightly wrong * on a device nobody tests on. */ const val MAX_SPREAD = 6 }}