fix(desktop): macOS light-mode primary contrast + transparent window icon

Two separate readability fixes:

1. macOS light-mode primary was poor contrast on the gray background.
   - Background was #ECECEC (Apple's title-bar chrome gray), now #F5F5F7
     (Apple's secondarySystemBackgroundColor — used for main content).
   - Surface container tones shifted accordingly so the sidebar
     (surfaceContainer) is still visibly grayer than the content.
   - Accents with high luminance (Apple Yellow, Graphite) now darken to
     a readable shade in light mode via a luminance clamp that preserves
     hue — saturated blue is untouched, but yellow pulls toward amber
     so the primary color stays visible on near-white surfaces.

2. Window icon was the generic Java cup during `./gradlew :desktopApp:run`
   because the Window composable had no `icon` param. The 100x100 icon
   in resources also looked blocky in the dock. Now:
   - icon.png replaced with the 512x512 transparent logo from
     fastlane/metadata/android/en-US/images/icon.png.
   - Window(icon = ...) loads it via Skia (non-deprecated path), shown
     in the macOS dock / Windows taskbar / GNOME & KDE task switchers.

https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
This commit is contained in:
Claude
2026-04-24 13:44:06 +00:00
parent 7a9806e936
commit 6edc432377
3 changed files with 61 additions and 12 deletions
@@ -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.
@@ -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) ────────────────────────────────────────────────────
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 16 KiB