Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Lx-204 <15377891490@163.com> Co-authored-by: AlexDev_ <56083016+alexdev03@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nikhil Nair <32665919+nikhilnair31@users.noreply.github.com> Co-authored-by: marciodps <95321123+marciodps@users.noreply.github.com> Co-authored-by: ThomasCrund <39323104+ThomasCrund@users.noreply.github.com>
225 lines
8.7 KiB
Kotlin
225 lines
8.7 KiB
Kotlin
package com.m57.hermescontrol.theme
|
|
|
|
import androidx.compose.material3.ColorScheme
|
|
import androidx.compose.material3.darkColorScheme
|
|
import androidx.compose.material3.lightColorScheme
|
|
import androidx.compose.ui.graphics.Color
|
|
|
|
/**
|
|
* Theme template — the ONE shape every preset follows.
|
|
*
|
|
* A theme preset is a single [ThemePalette] built from raw [PaletteColors]
|
|
* specs. Every preset file has the same skeleton: fill in the colors, done.
|
|
* The mapping from raw colors to the Material 3 [ColorScheme] slots and the
|
|
* [HermesStatusColors] model lives here — no per-preset behavior.
|
|
*
|
|
* A theme can declare which modes it ships via [ThemeMode]:
|
|
* - [ThemeMode.FULL] — both dark and light palettes.
|
|
* - [ThemeMode.DARK_ONLY] — dark palette only; the dispatcher falls back to
|
|
* the default light palette in light mode (e.g. AMOLED — a light AMOLED
|
|
* makes no sense).
|
|
* - [ThemeMode.LIGHT_ONLY] — light palette only; symmetric fallback.
|
|
*/
|
|
|
|
enum class ThemeMode { FULL, DARK_ONLY, LIGHT_ONLY }
|
|
|
|
/**
|
|
* Raw color spec for ONE mode (dark or light) of a theme.
|
|
*
|
|
* The Material error slots are derived from [status] — the theme author
|
|
* defines semantic colors once and the scheme inherits them.
|
|
* [primary] and the status error are also used directly for text on surfaces.
|
|
* [outline] must contrast with [surface] and [surfaceContainerHighest], which
|
|
* Material uses as the unchecked switch track.
|
|
*/
|
|
data class PaletteColors(
|
|
val primary: Color,
|
|
val onPrimary: Color,
|
|
val primaryContainer: Color,
|
|
val onPrimaryContainer: Color,
|
|
val secondary: Color,
|
|
val onSecondary: Color,
|
|
val secondaryContainer: Color,
|
|
val onSecondaryContainer: Color,
|
|
val tertiary: Color,
|
|
val onTertiary: Color,
|
|
val tertiaryContainer: Color,
|
|
val onTertiaryContainer: Color,
|
|
val background: Color,
|
|
val onBackground: Color,
|
|
val surface: Color,
|
|
val onSurface: Color,
|
|
val surfaceVariant: Color,
|
|
val onSurfaceVariant: Color,
|
|
val surfaceDim: Color,
|
|
val surfaceBright: Color,
|
|
val primaryFixed: Color,
|
|
val primaryFixedDim: Color,
|
|
val onPrimaryFixed: Color,
|
|
val onPrimaryFixedVariant: Color,
|
|
val secondaryFixed: Color,
|
|
val secondaryFixedDim: Color,
|
|
val onSecondaryFixed: Color,
|
|
val onSecondaryFixedVariant: Color,
|
|
val tertiaryFixed: Color,
|
|
val tertiaryFixedDim: Color,
|
|
val onTertiaryFixed: Color,
|
|
val onTertiaryFixedVariant: Color,
|
|
val surfaceContainerLowest: Color,
|
|
val surfaceContainerLow: Color,
|
|
val surfaceContainer: Color,
|
|
val surfaceContainerHigh: Color,
|
|
val surfaceContainerHighest: Color,
|
|
val inverseSurface: Color,
|
|
val inverseOnSurface: Color,
|
|
val inversePrimary: Color,
|
|
val outline: Color,
|
|
val outlineVariant: Color,
|
|
val scrim: Color,
|
|
val status: HermesStatusColors,
|
|
)
|
|
|
|
/** A complete theme — one per preset file, same shape everywhere. */
|
|
data class ThemePalette(
|
|
val mode: ThemeMode,
|
|
val dark: PaletteColors?,
|
|
val light: PaletteColors?,
|
|
) {
|
|
init {
|
|
require(mode != ThemeMode.FULL || (dark != null && light != null)) {
|
|
"FULL theme must provide both dark and light palettes"
|
|
}
|
|
require(mode != ThemeMode.DARK_ONLY || (dark != null && light == null)) {
|
|
"DARK_ONLY theme must provide a dark palette and no light palette"
|
|
}
|
|
require(mode != ThemeMode.LIGHT_ONLY || (dark == null && light != null)) {
|
|
"LIGHT_ONLY theme must provide a light palette and no dark palette"
|
|
}
|
|
}
|
|
|
|
val darkScheme: ColorScheme? by lazy { dark?.let(::darkSchemeOf) }
|
|
val lightScheme: ColorScheme? by lazy { light?.let(::lightSchemeOf) }
|
|
val darkStatus: HermesStatusColors? by lazy { dark?.status }
|
|
val lightStatus: HermesStatusColors? by lazy { light?.status }
|
|
|
|
fun schemeFor(dark: Boolean): ColorScheme? = if (dark) darkScheme else lightScheme
|
|
|
|
fun statusFor(dark: Boolean): HermesStatusColors? = if (dark) darkStatus else lightStatus
|
|
}
|
|
|
|
/** Full theme: bespoke dark + light palettes. */
|
|
fun buildTheme(
|
|
dark: PaletteColors,
|
|
light: PaletteColors,
|
|
): ThemePalette = ThemePalette(ThemeMode.FULL, dark, light)
|
|
|
|
/** Dark-only theme (e.g. AMOLED). Light mode falls back to the default theme. */
|
|
fun buildThemeDarkOnly(dark: PaletteColors): ThemePalette = ThemePalette(ThemeMode.DARK_ONLY, dark, null)
|
|
|
|
/** Light-only theme. Dark mode falls back to the default theme. */
|
|
fun buildThemeLightOnly(light: PaletteColors): ThemePalette = ThemePalette(ThemeMode.LIGHT_ONLY, null, light)
|
|
|
|
private fun darkSchemeOf(c: PaletteColors): ColorScheme =
|
|
darkColorScheme(
|
|
primary = c.primary,
|
|
onPrimary = c.onPrimary,
|
|
primaryContainer = c.primaryContainer,
|
|
onPrimaryContainer = c.onPrimaryContainer,
|
|
secondary = c.secondary,
|
|
onSecondary = c.onSecondary,
|
|
secondaryContainer = c.secondaryContainer,
|
|
onSecondaryContainer = c.onSecondaryContainer,
|
|
tertiary = c.tertiary,
|
|
onTertiary = c.onTertiary,
|
|
tertiaryContainer = c.tertiaryContainer,
|
|
onTertiaryContainer = c.onTertiaryContainer,
|
|
background = c.background,
|
|
onBackground = c.onBackground,
|
|
surface = c.surface,
|
|
onSurface = c.onSurface,
|
|
surfaceVariant = c.surfaceVariant,
|
|
onSurfaceVariant = c.onSurfaceVariant,
|
|
surfaceTint = c.primary,
|
|
surfaceDim = c.surfaceDim,
|
|
surfaceBright = c.surfaceBright,
|
|
primaryFixed = c.primaryFixed,
|
|
primaryFixedDim = c.primaryFixedDim,
|
|
onPrimaryFixed = c.onPrimaryFixed,
|
|
onPrimaryFixedVariant = c.onPrimaryFixedVariant,
|
|
secondaryFixed = c.secondaryFixed,
|
|
secondaryFixedDim = c.secondaryFixedDim,
|
|
onSecondaryFixed = c.onSecondaryFixed,
|
|
onSecondaryFixedVariant = c.onSecondaryFixedVariant,
|
|
tertiaryFixed = c.tertiaryFixed,
|
|
tertiaryFixedDim = c.tertiaryFixedDim,
|
|
onTertiaryFixed = c.onTertiaryFixed,
|
|
onTertiaryFixedVariant = c.onTertiaryFixedVariant,
|
|
surfaceContainerLowest = c.surfaceContainerLowest,
|
|
surfaceContainerLow = c.surfaceContainerLow,
|
|
surfaceContainer = c.surfaceContainer,
|
|
surfaceContainerHigh = c.surfaceContainerHigh,
|
|
surfaceContainerHighest = c.surfaceContainerHighest,
|
|
inverseSurface = c.inverseSurface,
|
|
inverseOnSurface = c.inverseOnSurface,
|
|
inversePrimary = c.inversePrimary,
|
|
error = c.status.error,
|
|
onError = c.status.onError,
|
|
errorContainer = c.status.errorContainer,
|
|
onErrorContainer = c.status.onErrorContainer,
|
|
outline = c.outline,
|
|
outlineVariant = c.outlineVariant,
|
|
scrim = c.scrim,
|
|
)
|
|
|
|
private fun lightSchemeOf(c: PaletteColors): ColorScheme =
|
|
lightColorScheme(
|
|
primary = c.primary,
|
|
onPrimary = c.onPrimary,
|
|
primaryContainer = c.primaryContainer,
|
|
onPrimaryContainer = c.onPrimaryContainer,
|
|
secondary = c.secondary,
|
|
onSecondary = c.onSecondary,
|
|
secondaryContainer = c.secondaryContainer,
|
|
onSecondaryContainer = c.onSecondaryContainer,
|
|
tertiary = c.tertiary,
|
|
onTertiary = c.onTertiary,
|
|
tertiaryContainer = c.tertiaryContainer,
|
|
onTertiaryContainer = c.onTertiaryContainer,
|
|
background = c.background,
|
|
onBackground = c.onBackground,
|
|
surface = c.surface,
|
|
onSurface = c.onSurface,
|
|
surfaceVariant = c.surfaceVariant,
|
|
onSurfaceVariant = c.onSurfaceVariant,
|
|
surfaceTint = c.primary,
|
|
surfaceDim = c.surfaceDim,
|
|
surfaceBright = c.surfaceBright,
|
|
primaryFixed = c.primaryFixed,
|
|
primaryFixedDim = c.primaryFixedDim,
|
|
onPrimaryFixed = c.onPrimaryFixed,
|
|
onPrimaryFixedVariant = c.onPrimaryFixedVariant,
|
|
secondaryFixed = c.secondaryFixed,
|
|
secondaryFixedDim = c.secondaryFixedDim,
|
|
onSecondaryFixed = c.onSecondaryFixed,
|
|
onSecondaryFixedVariant = c.onSecondaryFixedVariant,
|
|
tertiaryFixed = c.tertiaryFixed,
|
|
tertiaryFixedDim = c.tertiaryFixedDim,
|
|
onTertiaryFixed = c.onTertiaryFixed,
|
|
onTertiaryFixedVariant = c.onTertiaryFixedVariant,
|
|
surfaceContainerLowest = c.surfaceContainerLowest,
|
|
surfaceContainerLow = c.surfaceContainerLow,
|
|
surfaceContainer = c.surfaceContainer,
|
|
surfaceContainerHigh = c.surfaceContainerHigh,
|
|
surfaceContainerHighest = c.surfaceContainerHighest,
|
|
inverseSurface = c.inverseSurface,
|
|
inverseOnSurface = c.inverseOnSurface,
|
|
inversePrimary = c.inversePrimary,
|
|
error = c.status.error,
|
|
onError = c.status.onError,
|
|
errorContainer = c.status.errorContainer,
|
|
onErrorContainer = c.status.onErrorContainer,
|
|
outline = c.outline,
|
|
outlineVariant = c.outlineVariant,
|
|
scrim = c.scrim,
|
|
)
|