Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.kairo.reader.ui.rsvp

import android.graphics.Bitmap
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
Expand All @@ -9,7 +10,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performSemanticsAction
Expand All @@ -21,6 +25,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kairo.reader.TestActivity
import com.kairo.reader.core.model.ReaderTheme
import com.kairo.reader.core.model.RsvpConfig
import com.kairo.reader.core.model.RsvpFontFamily
import com.kairo.reader.core.model.RsvpProfile
import com.kairo.reader.core.model.Token
import com.kairo.reader.core.model.TokenType
Expand All @@ -30,8 +35,10 @@ import com.kairo.reader.core.rsvp.RsvpGenerationOptions
import com.kairo.reader.core.rsvp.RsvpLanguagePolicy
import com.kairo.reader.ui.theme.KairoTheme
import com.kairo.reader.ui.theme.colorContrast
import com.kairo.reader.ui.theme.composeFontFamily
import com.kairo.reader.ui.theme.materialColorScheme
import com.kairo.reader.ui.theme.rsvpAccentColors
import java.io.File
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
Expand All @@ -45,16 +52,19 @@ class RsvpDifficultyHighlightDeviceTest {
val composeRule = createAndroidComposeRule<TestActivity>()

@Test
fun darkThemesRenderAContrastingPivotLetterWithoutAHighlightBlock() {
fun lightAndDarkThemesRenderAContrastingPivotLetterWithoutAHighlightBlock() {
var theme by mutableStateOf(ReaderTheme.DARK)
composeRule.setContent {
KairoTheme(readerTheme = theme) {
val scheme = MaterialTheme.colorScheme
val accents = scheme.rsvpAccentColors()
Box(Modifier.size(FOCUS_STAGE_WIDTH_DP.dp, FOCUS_STAGE_HEIGHT_DP.dp).background(scheme.background)) {
Box(
Modifier.size(FOCUS_STAGE_WIDTH_DP.dp, FOCUS_STAGE_HEIGHT_DP.dp)
.background(scheme.background).testTag("rsvp-accent-stage"),
) {
OrpAlignedText(
tokens = listOf(Token("reading", TokenType.WORD)),
typography = OrpTypography(FOCUS_FONT_SIZE_SP, FontFamily.Monospace, FontWeight.Normal),
typography = OrpTypography(FOCUS_FONT_SIZE_SP, RsvpFontFamily.MERRIWEATHER.composeFontFamily(), FontWeight.Normal),
colors = OrpColors(scheme.onBackground, accents.pivot, scheme.onBackground, accents.wordPart),
layout = OrpTextLayout(
horizontalBias = FOCUS_HORIZONTAL_BIAS,
Expand All @@ -71,7 +81,7 @@ class RsvpDifficultyHighlightDeviceTest {
}
}

for (currentTheme in listOf(ReaderTheme.DARK, ReaderTheme.INK)) {
for (currentTheme in ReaderTheme.entries.filter { it != ReaderTheme.CUSTOM }) {
composeRule.runOnIdle { theme = currentTheme }
val scheme = currentTheme.materialColorScheme()
val accents = scheme.rsvpAccentColors()
Expand All @@ -82,7 +92,8 @@ class RsvpDifficultyHighlightDeviceTest {
assertEquals(Color.Unspecified, pivot.item.background)
assertNull(pivot.item.fontWeight)
assertNull(pivot.item.textDecoration)
assertTrue(colorContrast(pivot.item.color, scheme.background) >= WCAG_AA_TEXT_CONTRAST)
assertTrue(colorContrast(pivot.item.color, scheme.background) >= PIVOT_TEXT_CONTRAST)
captureAccent(currentTheme)
}
}

Expand Down Expand Up @@ -211,6 +222,14 @@ class RsvpDifficultyHighlightDeviceTest {
return layouts.single()
}

private fun captureAccent(theme: ReaderTheme) {
val bitmap = composeRule.onNodeWithTag("rsvp-accent-stage").captureToImage().asAndroidBitmap()
val directory = requireNotNull(composeRule.activity.getExternalFilesDir("rsvp-accent-review"))
File(directory, "${theme.name.lowercase()}.png").outputStream().use {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
}
}

private fun highlightedText(layout: TextLayoutResult): String {
val text = layout.layoutInput.text
return text.spanStyles.filter { it.item.color == Color.Blue }.joinToString("") { text.text.substring(it.start, it.end) }
Expand All @@ -223,6 +242,6 @@ class RsvpDifficultyHighlightDeviceTest {
const val FOCUS_HORIZONTAL_BIAS = 0.5f
const val FOCUS_GUIDE_THICKNESS = 1f
const val PIVOT_CHARACTER_COUNT = 1
const val WCAG_AA_TEXT_CONTRAST = 4.5f
const val PIVOT_TEXT_CONTRAST = 6f
}
}
29 changes: 15 additions & 14 deletions app/src/main/java/com/kairo/reader/ui/theme/RsvpAccentColors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,22 @@ import kotlin.math.min

internal data class RsvpAccentColors(val pivot: Color, val wordPart: Color)

/** Tertiary is usually the palette's hue-separated accent; reserve primary for word parts. */
/** Keep the palette's tertiary hue, but give the single-letter focus a richer tone than UI accents. */
internal fun ColorScheme.rsvpAccentColors(): RsvpAccentColors {
val themedPivot = contrastingColor(tertiary, listOf(background))
val pivot = if (colorChroma(background) >= MIN_BACKGROUND_CHROMA &&
hueDistance(background, themedPivot) < MIN_PIVOT_HUE_DISTANCE
val pivotHue = if (colorChroma(background) >= MIN_BACKGROUND_CHROMA &&
hueDistance(background, tertiary) < MIN_PIVOT_HUE_DISTANCE
) {
contrastingColor(
Color.hsl(
(colorHue(background) + HUE_HALF_TURN) % FULL_CIRCLE_DEGREES,
PIVOT_SATURATION,
accentLightness(background),
),
listOf(background),
)
(colorHue(background) + HUE_HALF_TURN) % FULL_CIRCLE_DEGREES
} else {
themedPivot
colorHue(tertiary)
}
// Pale dark-theme accents can meet background contrast yet disappear among near-white letters.
// A controlled saturation and tone separate the pivot from both the page and the reading text.
val pivot = contrastingColor(
Color.hsl(pivotHue, PIVOT_SATURATION, min(accentLightness(background), MAX_PIVOT_LIGHTNESS)),
listOf(background),
minimum = PIVOT_TEXT_CONTRAST,
)
val themedWordPart = contrastingColor(primary, listOf(background))
val wordPart = if (channelDistance(pivot, themedWordPart) >= MIN_ACCENT_DISTANCE) {
themedWordPart
Expand Down Expand Up @@ -83,8 +82,10 @@ private const val MIN_ACCENT_DISTANCE = 0.16f
private const val MIN_BACKGROUND_CHROMA = 0.04f
private const val MIN_PIVOT_HUE_DISTANCE = 45f
private const val MIN_TEXT_CONTRAST = 4.5f
private const val PIVOT_TEXT_CONTRAST = 6f
private const val WORD_PART_SATURATION = 0.9f
private const val PIVOT_SATURATION = 0.8f
private const val PIVOT_SATURATION = 0.78f
private const val MAX_PIVOT_LIGHTNESS = 0.68f
private const val LIGHT_BACKGROUND_ACCENT_LIGHTNESS = 0.38f
private const val DARK_BACKGROUND_ACCENT_LIGHTNESS = 0.72f
private const val NEUTRAL_CHROMA_THRESHOLD = 0.02f
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.kairo.reader.core.model.CustomTheme
import com.kairo.reader.core.model.ReaderTheme
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.random.Random
import org.junit.Assert.assertTrue
import org.junit.Test
Expand All @@ -14,7 +15,17 @@ class RsvpAccentColorsTest {
@Test
fun everyBuiltInThemeHasLegibleDistinctRsvpAccents() {
ReaderTheme.entries.filter { it != ReaderTheme.CUSTOM }.forEach { theme ->
assertAccents(theme.materialColorScheme())
val scheme = theme.materialColorScheme()
assertAccents(scheme)
val pivot = scheme.rsvpAccentColors().pivot
val chroma = max(pivot.red, max(pivot.green, pivot.blue)) - min(pivot.red, min(pivot.green, pivot.blue))
val textDistance = max(
abs(pivot.red - scheme.onBackground.red),
max(abs(pivot.green - scheme.onBackground.green), abs(pivot.blue - scheme.onBackground.blue)),
)
assertTrue("$theme pivot should remain vivid among reading text: $pivot", chroma >= MIN_PIVOT_CHROMA)
assertTrue("$theme pivot blends into reading text: $pivot", textDistance >= MIN_PIVOT_TEXT_DISTANCE)
assertTrue("$theme pivot needs strong page contrast: $pivot", colorContrast(pivot, scheme.background) >= MIN_PIVOT_CONTRAST)
}
}

Expand Down Expand Up @@ -61,5 +72,8 @@ class RsvpAccentColorsTest {
const val WCAG_AA_TEXT_CONTRAST = 4.5f
const val CONTRAST_TOLERANCE = 0.0001f
const val MIN_VISIBLE_CHANNEL_DELTA = 0.05f
const val MIN_PIVOT_CHROMA = 0.35f
const val MIN_PIVOT_TEXT_DISTANCE = 0.3f
const val MIN_PIVOT_CONTRAST = 6f
}
}
Loading