diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index fe63d226f..56176a09f 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -61,6 +61,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyShortcut import androidx.compose.ui.unit.dp @@ -252,10 +254,24 @@ fun main() { // Callback set by App() for single pane navigation from MenuBar var navigateToScreen by remember { mutableStateOf<((DeckColumnType) -> Unit)?>(null) } + // Transparent 512x512 PNG shown in the macOS dock / Windows taskbar / GNOME + // & KDE task switchers while running via gradle (the packaged app uses the + // icon.icns / icon.ico configured in nativeDistributions). + val appIcon = + remember { + val bytes = Unit::class.java.getResourceAsStream("/icon.png")!!.readBytes() + val bitmap = + org.jetbrains.skia.Image + .makeFromEncoded(bytes) + .toComposeImageBitmap() + BitmapPainter(bitmap) + } + Window( onCloseRequest = ::exitApplication, state = windowState, title = "Amethyst", + icon = appIcon, ) { // macOS: transparent + full-window-content title bar so the deck/sidebar // shows through, with traffic lights still drawn on top. No-op elsewhere. diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformColorScheme.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformColorScheme.kt index 5018a7f2d..6a8c3557b 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformColorScheme.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformColorScheme.kt @@ -77,26 +77,59 @@ object PlatformColorScheme { outlineVariant = Color(0xFF3A3A3A), ) - private fun macOSLight(accent: Color) = - lightColorScheme( - primary = accent, - onPrimary = onAccent(accent), - secondary = accent, - tertiary = accent, - background = Color(0xFFECECEC), + private fun macOSLight(accent: Color): ColorScheme { + // Apple's accent palette includes Yellow (#F8BA00) and Graphite (#8C8C8C) + // which have terrible contrast on any light surface. Darken high-luminance + // accents so the primary color stays readable without disabling the user's + // preference — the hue is preserved, just pulled toward a usable lightness. + val readable = darkenForLight(accent) + return lightColorScheme( + primary = readable, + onPrimary = onAccent(readable), + secondary = readable, + tertiary = readable, + // Apple's light-mode main content background is near-white (#F5F5F7 / + // Apple's secondarySystemBackgroundColor); the #ECECEC gray is only + // used behind the title bar chrome — which we map to surfaceContainer. + background = Color(0xFFF5F5F7), onBackground = Color(0xFF1A1A1A), surface = Color(0xFFFFFFFF), onSurface = Color(0xFF1A1A1A), - surfaceVariant = Color(0xFFF2F2F2), + surfaceVariant = Color(0xFFECECEC), onSurfaceVariant = Color(0xFF555555), - surfaceContainer = Color(0xFFF6F6F6), - surfaceContainerHigh = Color(0xFFEDEDED), - surfaceContainerHighest = Color(0xFFE5E5E5), - surfaceContainerLow = Color(0xFFFAFAFA), + surfaceContainer = Color(0xFFECECEC), + surfaceContainerHigh = Color(0xFFE5E5E5), + surfaceContainerHighest = Color(0xFFDDDDDD), + surfaceContainerLow = Color(0xFFF0F0F0), surfaceContainerLowest = Color(0xFFFFFFFF), outline = Color(0xFFB0B0B0), outlineVariant = Color(0xFFD8D8D8), ) + } + + /** + * Scales an accent color toward black until its relative luminance is at + * most [maxLuminance]. Preserves hue (all RGB channels scale by the same + * factor). Only applied on light surfaces — on dark surfaces the raw accent + * already has good contrast. + * + * 0.38 was picked empirically: it keeps a saturated blue visible without + * darkening it, while pulling yellow/graphite down to a readable amber/gray. + */ + private fun darkenForLight( + c: Color, + maxLuminance: Float = 0.38f, + ): Color { + val lum = 0.2126f * c.red + 0.7152f * c.green + 0.0722f * c.blue + if (lum <= maxLuminance) return c + val scale = maxLuminance / lum + return Color( + red = (c.red * scale).coerceIn(0f, 1f), + green = (c.green * scale).coerceIn(0f, 1f), + blue = (c.blue * scale).coerceIn(0f, 1f), + alpha = c.alpha, + ) + } // ── GNOME (libadwaita) ──────────────────────────────────────────────────── diff --git a/desktopApp/src/jvmMain/resources/icon.png b/desktopApp/src/jvmMain/resources/icon.png index dbb5d7153..ffe89a16f 100644 Binary files a/desktopApp/src/jvmMain/resources/icon.png and b/desktopApp/src/jvmMain/resources/icon.png differ