fix(chat): tint composer send/stop/mic button with theme primary (#1410)
This commit is contained in:
@@ -91,3 +91,6 @@ fun parseProjectColor(raw: String?): Color? {
|
|||||||
if (h !in 0f..360f || s !in 0f..1f || l !in 0f..1f) return null
|
if (h !in 0f..360f || s !in 0f..1f || l !in 0f..1f) return null
|
||||||
return Color.hsl(h % 360f, s, l)
|
return Color.hsl(h % 360f, s, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Pure white glyph for filled accent controls (composer action button) when it is legible on the preset primary. */
|
||||||
|
val GlyphWhite = Color(0xFFFFFFFF)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.compositeOver
|
import androidx.compose.ui.graphics.compositeOver
|
||||||
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
import com.m57.hermescontrol.theme.GlyphWhite
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolved colors for the chat composer card and its controls.
|
* Resolved colors for the chat composer card and its controls.
|
||||||
@@ -15,7 +17,8 @@ import androidx.compose.ui.graphics.compositeOver
|
|||||||
* (Nord's High and Highest are the same color), which made the flat controls
|
* (Nord's High and Highest are the same color), which made the flat controls
|
||||||
* vanish into the card. A fixed onSurface tint keeps them visible in every
|
* vanish into the card. A fixed onSurface tint keeps them visible in every
|
||||||
* preset and under dynamic color. Text and icon contrast is gated per preset
|
* preset and under dynamic color. Text and icon contrast is gated per preset
|
||||||
* in ComposerPaletteTest.
|
* in ComposerPaletteTest. The send / stop / mic action button is the one
|
||||||
|
* themed control: it takes the preset's primary so it follows the theme tint.
|
||||||
*/
|
*/
|
||||||
internal data class ComposerPalette(
|
internal data class ComposerPalette(
|
||||||
val card: Color,
|
val card: Color,
|
||||||
@@ -30,10 +33,30 @@ internal data class ComposerPalette(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private const val CONTROL_TINT_ALPHA = 0.12f
|
private const val CONTROL_TINT_ALPHA = 0.12f
|
||||||
|
private const val ACTION_MIN_CONTRAST = 3f
|
||||||
|
|
||||||
|
private fun contrastRatio(
|
||||||
|
a: Color,
|
||||||
|
b: Color,
|
||||||
|
): Float {
|
||||||
|
val l1 = a.luminance()
|
||||||
|
val l2 = b.luminance()
|
||||||
|
return (maxOf(l1, l2) + 0.05f) / (minOf(l1, l2) + 0.05f)
|
||||||
|
}
|
||||||
|
|
||||||
internal fun composerPalette(scheme: ColorScheme): ComposerPalette {
|
internal fun composerPalette(scheme: ColorScheme): ComposerPalette {
|
||||||
val card = scheme.surfaceContainer
|
val card = scheme.surfaceContainer
|
||||||
val tint = scheme.onSurface.copy(alpha = CONTROL_TINT_ALPHA)
|
val tint = scheme.onSurface.copy(alpha = CONTROL_TINT_ALPHA)
|
||||||
|
// Pastel primaries (Nord light) vanish on the card; keep the neutral fill there.
|
||||||
|
val tinted = contrastRatio(scheme.primary, card) >= ACTION_MIN_CONTRAST
|
||||||
|
val action = if (tinted) scheme.primary else scheme.onSurface
|
||||||
|
// White glyph wherever it clears 3:1 on the primary; pastel primaries keep their own dark onPrimary.
|
||||||
|
val onAction =
|
||||||
|
when {
|
||||||
|
!tinted -> scheme.surface
|
||||||
|
contrastRatio(GlyphWhite, scheme.primary) >= ACTION_MIN_CONTRAST -> GlyphWhite
|
||||||
|
else -> scheme.onPrimary
|
||||||
|
}
|
||||||
return ComposerPalette(
|
return ComposerPalette(
|
||||||
card = card,
|
card = card,
|
||||||
cardBorder = tint.compositeOver(scheme.background),
|
cardBorder = tint.compositeOver(scheme.background),
|
||||||
@@ -42,8 +65,8 @@ internal fun composerPalette(scheme: ColorScheme): ComposerPalette {
|
|||||||
control = tint.compositeOver(card),
|
control = tint.compositeOver(card),
|
||||||
onControl = scheme.onSurface,
|
onControl = scheme.onSurface,
|
||||||
onControlVariant = scheme.onSurfaceVariant,
|
onControlVariant = scheme.onSurfaceVariant,
|
||||||
action = scheme.onSurface,
|
action = action,
|
||||||
onAction = scheme.surface,
|
onAction = onAction,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.chat.components
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.luminance
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
import com.m57.hermescontrol.theme.GlyphWhite
|
||||||
import com.m57.hermescontrol.theme.ThemePreset
|
import com.m57.hermescontrol.theme.ThemePreset
|
||||||
import com.m57.hermescontrol.theme.resolveColorScheme
|
import com.m57.hermescontrol.theme.resolveColorScheme
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
@@ -66,6 +67,20 @@ class ComposerPaletteTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun actionButtonFollowsThePresetPrimaryWhenItIsLegible() {
|
||||||
|
forEveryPresetMode { mode, p, _ ->
|
||||||
|
val scheme = resolveColorScheme(ThemePreset.valueOf(mode.substringBefore(" ")), mode.endsWith("true"))
|
||||||
|
if (contrast(scheme.primary, p.card) >= 3f) {
|
||||||
|
assertTrue("$mode action must be primary", p.action == scheme.primary)
|
||||||
|
assertTrue(
|
||||||
|
"$mode glyph must be white or onPrimary",
|
||||||
|
p.onAction == scheme.onPrimary || p.onAction == GlyphWhite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun controlsAndCardEdgeStayVisibleInEveryPresetMode() {
|
fun controlsAndCardEdgeStayVisibleInEveryPresetMode() {
|
||||||
forEveryPresetMode { mode, p, background ->
|
forEveryPresetMode { mode, p, background ->
|
||||||
|
|||||||
Reference in New Issue
Block a user