package org.terst.doot.widget.ui import android.content.Context import android.content.res.Configuration import androidx.compose.ui.graphics.Color /** * Chrome colors (body text, dividers) for the widget grid, rendered directly * over the transparent home-screen background -- no card/background of its * own. Per-source accent colors (see sourceColor() usage) are separate and * not part of this palette. */ data class WidgetPalette( val textPrimary: Color, val textSecondary: Color, val textMuted: Color, val divider: Color, val nowLine: Color, // ShadowedText's halo needs to be the OPPOSITE tone of the text it's // outlining -- a black halo behind already-dark text (every light-mode // branch below: system_*_900/_700 shades) does essentially nothing, // since dark-on-dark blends into itself regardless of what's behind it. // Bug found 2026-08-06: this was hardcoded black in ShadowedText // (correct for dark-mode/CLASSIC's light text, silently ineffective for // every light-mode palette variant). val haloColor: Color ) enum class WidgetColorTheme { /** Mostly neutral (grayscale) text; the wallpaper's accent hue shows only on the "now" line. */ NEUTRAL, /** Primary/secondary text itself tinted by the wallpaper's dominant accent hue. */ ACCENT, /** Primary text stays neutral for readability; secondary/muted text picks up the wallpaper's two complementary accent hues. */ TONAL, /** * All three text roles pull from a DIFFERENT one of the wallpaper's three * accent slots (accent1/accent2/accent3), instead of anchoring primary to * neutral (TONAL) or one hue everywhere (ACCENT) -- the most saturated, * multi-hue option, added 2026-08-06 in response to "more diverse colors." */ VIVID, /** The widget's original fixed dark palette, regardless of wallpaper. */ CLASSIC; companion object { fun fromPref(raw: String?): WidgetColorTheme = raw?.let { name -> entries.find { it.name == name } } ?: NEUTRAL } } // Bumped 2026-08-06 (0.45/0.55 -> 0.65/0.7): "legibility is better but still // not great" after the corner-halo + luminance-matching fixes -- turning up // the one dial that's pure risk-free strength (opacity, not reach/geometry) // before reaching for a structurally different approach (true 8-neighbor // outline, or a backdrop behind the text). private val HALO_DARK = Color.Black.copy(alpha = 0.65f) private val HALO_LIGHT = Color.White.copy(alpha = 0.7f) // Grid lines (divider, now-line) are solid -- no alpha. Alpha over a // transparent widget blends unpredictably with whatever's under the // wallpaper at that spot instead of reading as a consistent line. private val CLASSIC_PALETTE = WidgetPalette( textPrimary = Color.White, textSecondary = Color.White.copy(alpha = 0.6f), textMuted = Color.White.copy(alpha = 0.5f), divider = Color(0xFF3D3D3D), nowLine = Color(0xFFD9D9D9), haloColor = HALO_DARK ) /** * Builds the widget's chrome palette from the wallpaper-derived * system_neutral/accent color resources (Material You / "Monet", API 31+ -- * minSdk is 36, so these always exist). No alpha tweaking on top of any * resource color -- a "muted" role means picking a lower-contrast shade * number, not adding transparency over a higher-contrast one, and grid * lines are always fully opaque for the same reason as CLASSIC's. Light vs. * dark shades are chosen from the system's current dark-theme setting so * text stays legible either way. * * Divider shade picked for LOW contrast against its own background, not * high visibility -- a hairline should read as subtle structure, not a * bold rule. neutral2_700 in dark mode sits close to the near-black * background; neutral2_100 in light mode sits close to the near-white one. * (Was neutral2_400/200, which read as too heavy against the background -- * corrected 2026-08-05.) */ fun buildWidgetPalette(context: Context, theme: WidgetColorTheme): WidgetPalette { if (theme == WidgetColorTheme.CLASSIC) return CLASSIC_PALETTE val isNight = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES fun sysColor(resId: Int): Color = Color(context.getColor(resId)) return when (theme) { WidgetColorTheme.ACCENT -> if (isNight) { WidgetPalette( textPrimary = sysColor(android.R.color.system_accent1_100), textSecondary = sysColor(android.R.color.system_accent1_300), textMuted = sysColor(android.R.color.system_neutral2_500), divider = sysColor(android.R.color.system_neutral2_700), nowLine = sysColor(android.R.color.system_accent2_200), haloColor = HALO_DARK ) } else { WidgetPalette( textPrimary = sysColor(android.R.color.system_accent1_900), textSecondary = sysColor(android.R.color.system_accent1_700), textMuted = sysColor(android.R.color.system_neutral2_500), divider = sysColor(android.R.color.system_neutral2_100), nowLine = sysColor(android.R.color.system_accent2_600), haloColor = HALO_LIGHT ) } WidgetColorTheme.TONAL -> if (isNight) { WidgetPalette( textPrimary = sysColor(android.R.color.system_neutral1_50), textSecondary = sysColor(android.R.color.system_accent2_200), textMuted = sysColor(android.R.color.system_accent3_300), divider = sysColor(android.R.color.system_neutral2_700), nowLine = sysColor(android.R.color.system_accent1_200), haloColor = HALO_DARK ) } else { WidgetPalette( textPrimary = sysColor(android.R.color.system_neutral1_900), textSecondary = sysColor(android.R.color.system_accent2_700), textMuted = sysColor(android.R.color.system_accent3_600), divider = sysColor(android.R.color.system_neutral2_100), nowLine = sysColor(android.R.color.system_accent1_600), haloColor = HALO_LIGHT ) } WidgetColorTheme.VIVID -> if (isNight) { WidgetPalette( textPrimary = sysColor(android.R.color.system_accent1_200), textSecondary = sysColor(android.R.color.system_accent2_200), textMuted = sysColor(android.R.color.system_accent3_300), divider = sysColor(android.R.color.system_neutral2_700), nowLine = sysColor(android.R.color.system_accent3_200), haloColor = HALO_DARK ) } else { WidgetPalette( textPrimary = sysColor(android.R.color.system_accent1_700), textSecondary = sysColor(android.R.color.system_accent2_700), textMuted = sysColor(android.R.color.system_accent3_600), divider = sysColor(android.R.color.system_neutral2_100), nowLine = sysColor(android.R.color.system_accent3_600), haloColor = HALO_LIGHT ) } // NEUTRAL, and the fallback for any unrecognized pref value (see fromPref). else -> if (isNight) { WidgetPalette( textPrimary = sysColor(android.R.color.system_neutral1_50), textSecondary = sysColor(android.R.color.system_neutral2_300), textMuted = sysColor(android.R.color.system_neutral2_500), divider = sysColor(android.R.color.system_neutral2_700), nowLine = sysColor(android.R.color.system_accent1_200), haloColor = HALO_DARK ) } else { WidgetPalette( textPrimary = sysColor(android.R.color.system_neutral1_900), textSecondary = sysColor(android.R.color.system_neutral2_700), textMuted = sysColor(android.R.color.system_neutral2_500), divider = sysColor(android.R.color.system_neutral2_100), nowLine = sysColor(android.R.color.system_accent1_600), haloColor = HALO_LIGHT ) } } }