diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbolsFont.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbolsFont.kt index 27e58f724..882dd1e90 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbolsFont.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbolsFont.kt @@ -47,24 +47,31 @@ private const val TEXT_MEASURER_CACHE_SIZE = 64 /** * Builds the Material Symbols FontFamily and a shared TextMeasurer once for the subtree and * exposes them via CompositionLocal. Wrap app roots (AmethystTheme, desktop MaterialTheme) in this. + * + * The optional [weight] override lets callers pick a stroke thickness different from the + * library default — desktop uses per-OS weights (macOS likes the thinner SF-Symbols-ish + * look at 200, Windows Fluent icons sit closer to 400) while Android keeps the default. */ @Composable -fun ProvideMaterialSymbols(content: @Composable () -> Unit) { +fun ProvideMaterialSymbols( + weight: Int = MaterialSymbolsDefaults.WEIGHT, + content: @Composable () -> Unit, +) { val font = Font( resource = Res.font.material_symbols_outlined, - weight = FontWeight(MaterialSymbolsDefaults.WEIGHT), + weight = FontWeight(weight), variationSettings = FontVariation.Settings( - FontVariation.weight(MaterialSymbolsDefaults.WEIGHT), + FontVariation.weight(weight), FontVariation.Setting("FILL", MaterialSymbolsDefaults.FILL), FontVariation.Setting("opsz", MaterialSymbolsDefaults.OPTICAL_SIZE), FontVariation.Setting("GRAD", MaterialSymbolsDefaults.GRADE), ), ) - // Keyless remember: the Font wrapper identity changes every composition but the underlying - // resource is a compile-time constant, so one FontFamily for the lifetime of the subtree. - val fontFamily = remember { FontFamily(font) } + // Keyless remember is safe only when weight is stable; key on weight so a platform- + // preview override swap actually rebuilds the FontFamily. + val fontFamily = remember(weight) { FontFamily(font) } val textMeasurer = rememberTextMeasurer(cacheSize = TEXT_MEASURER_CACHE_SIZE) CompositionLocalProvider( LocalMaterialSymbolsFontFamily provides fontFamily, 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 a82aa523e..1c78e6f5c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -807,7 +808,9 @@ fun App( .rememberSystemDark(LocalAwtWindow.current) com.vitorpamplona.amethyst.desktop.platform.PlatformMaterialTheme(isDark = isDark) { - ProvideMaterialSymbols { + ProvideMaterialSymbols( + weight = com.vitorpamplona.amethyst.desktop.platform.PlatformIconWeight.current, + ) { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background, @@ -1328,8 +1331,9 @@ fun RelaySettingsScreen( Column( modifier = Modifier - .fillMaxSize() .widthIn(max = 720.dp) + .fillMaxWidth() + .fillMaxHeight() .verticalScroll(rememberScrollState()) .padding(horizontal = 12.dp), ) { @@ -1353,7 +1357,7 @@ fun RelaySettingsScreen( // Wallet Connect Section Text( "Wallet Connect (NWC)", - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.onBackground, ) Spacer(Modifier.height(8.dp)) @@ -1462,7 +1466,7 @@ fun RelaySettingsScreen( Text( "Relay Settings", - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.onBackground, ) Spacer(Modifier.height(8.dp)) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformIconWeight.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformIconWeight.kt new file mode 100644 index 000000000..e72ce429c --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformIconWeight.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop.platform + +/** + * Preferred Material Symbols stroke weight per host OS. Each desktop UI language + * draws icons at a different visual weight; matching them makes the in-app icons + * feel at home next to the OS's own. Values use the Material Symbols variation + * axis (100 = Thin, 200 = ExtraLight, 300 = Light, 400 = Regular, …, 700 = Bold). + * + * Reference calibration: + * - macOS: SF Symbols "Regular" is visually lighter than 400 Material Symbols; + * 200 (Thin/ExtraLight) matches the delicate stroke macOS users expect. + * - GNOME: libadwaita symbolic icons are a hair thinner than Regular — 300 matches. + * - KDE Breeze: Breeze icons are thin-to-medium, also 300. + * - Windows (Fluent): Fluent Icons have moderate weight, 400 is the sweet spot. + */ +object PlatformIconWeight { + val current: Int by lazy { + when (PlatformInfo.current) { + Platform.MACOS -> 200 + Platform.GNOME -> 300 + Platform.KDE -> 300 + Platform.WINDOWS -> 400 + Platform.LINUX_OTHER, Platform.UNKNOWN -> 300 + } + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadingColumn.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadingColumn.kt index 7f6bb82f5..645b6bd15 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadingColumn.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadingColumn.kt @@ -23,7 +23,9 @@ package com.vitorpamplona.amethyst.desktop.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.widthIn import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -59,8 +61,15 @@ fun ReadingColumn( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter, ) { + // Order matters: widthIn must come BEFORE fillMaxWidth, otherwise + // fillMaxWidth locks the Column to parent.width and the cap is ignored. + // fillMaxHeight is safe (it only constrains the other axis). Column( - modifier = modifier.fillMaxSize().widthIn(max = maxWidth), + modifier = + modifier + .widthIn(max = maxWidth) + .fillMaxWidth() + .fillMaxHeight(), content = content, ) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt index e980ccb2d..c1298df96 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -300,8 +301,9 @@ fun SearchScreen( Column( modifier = modifier - .fillMaxSize() .widthIn(max = DefaultReadingWidth) + .fillMaxWidth() + .fillMaxHeight() .onPreviewKeyEvent { event -> if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false when (event.key) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index dded5f020..7a501d60d 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -439,7 +440,13 @@ fun UserProfileScreen( } Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) { - Box(modifier = Modifier.fillMaxSize().widthIn(max = DefaultReadingWidth)) { + Box( + modifier = + Modifier + .widthIn(max = DefaultReadingWidth) + .fillMaxWidth() + .fillMaxHeight(), + ) { if (connectedRelays.isEmpty()) { LoadingState("Connecting to relays...") } else {