From f3ef3514b2e87b05c922584c7372fc894bbd0e13 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Apr 2026 14:07:39 +0000 Subject: [PATCH] feat(desktop): icon mark sized to match neighbors, add drop shadow, unify screen headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Icon: - Mark now fills ~85% of the squircle (was 80%) so the goose reads at the same visual weight as first-party dock icons. Apple's template specifies 7.5-10% padding inside the squircle; 7.5% lands closer to the average first-party icon. - Baked a drop shadow into the icon PNG: 8px offset, 18px Gaussian blur at ~28% black, rendered under the white squircle on its own layer so the blur doesn't leak into the mark. First-party macOS icons include this shadow in the PNG — the dock doesn't add one at render time. Separable Gaussian (2x 1D passes) keeps startup fast. Screen header consistency (match Messages pattern): - Bookmarks, Drafts, Search, Reads, Highlights: titles switched from headlineMedium to titleMedium. - All five header rows now pad horizontal = 12.dp, vertical = 8.dp (matching ConversationListPane's "Messages" header). - Removed the 16.dp outer wrapper padding from Bookmarks and the 16.dp bottom padding from Reads header — screens now sit edge-to-edge. - DeckColumnContainer's 12.dp outer padding around column content removed for the same reason SinglePaneLayout's was: creates a `#F5F5F7` frame around every screen that reads as inconsistent with Messages (and every native desktop app). https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo --- .../desktop/platform/PlatformAppIcon.kt | 83 +++++++++++++++---- .../amethyst/desktop/ui/BookmarksScreen.kt | 4 +- .../amethyst/desktop/ui/DraftsScreen.kt | 7 +- .../amethyst/desktop/ui/ReadsScreen.kt | 7 +- .../amethyst/desktop/ui/SearchScreen.kt | 7 +- .../desktop/ui/deck/DeckColumnContainer.kt | 8 +- .../ui/highlights/MyHighlightsScreen.kt | 5 +- 7 files changed, 93 insertions(+), 28 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformAppIcon.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformAppIcon.kt index cdfd76f34b..672a2a64e1 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformAppIcon.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformAppIcon.kt @@ -25,6 +25,8 @@ import java.awt.Color import java.awt.RenderingHints import java.awt.geom.RoundRectangle2D import java.awt.image.BufferedImage +import java.awt.image.ConvolveOp +import java.awt.image.Kernel /** * Adapts a transparent source logo to the host OS's app-icon conventions. @@ -74,10 +76,45 @@ object PlatformAppIcon { val squircleMargin = (canvas - squircleSize) / 2 // Apple's reference corner radius on the 824-box is ~185px (≈22.45%). val cornerDiameter = (squircleSize * 0.4490f) - // Mark padding inside the squircle: 10% of the squircle size. - val markPadding = (squircleSize * 0.10f).toInt() + // Mark padding inside the squircle: 7.5% each side (~85% fill) so the + // mark reads at roughly the same visual weight as first-party dock icons. + val markPadding = (squircleSize * 0.075f).toInt() val markOrigin = squircleMargin + markPadding val markSize = squircleSize - 2 * markPadding + // Drop shadow under the squircle — Apple's dock icons include a subtle + // shadow baked into the PNG; the dock doesn't add one at render time. + val shadowOffset = 8 + val shadowBlur = 18 + + val squircle = + RoundRectangle2D.Float( + squircleMargin.toFloat(), + squircleMargin.toFloat(), + squircleSize.toFloat(), + squircleSize.toFloat(), + cornerDiameter, + cornerDiameter, + ) + + // Rasterize the shadow onto its own layer so the blur doesn't leak + // into the white squircle or the mark. + val shadowLayer = BufferedImage(canvas, canvas, BufferedImage.TYPE_INT_ARGB) + shadowLayer.createGraphics().apply { + setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + color = Color(0, 0, 0, 72) // ~28% black + val shadowShape = + RoundRectangle2D.Float( + squircleMargin.toFloat(), + (squircleMargin + shadowOffset).toFloat(), + squircleSize.toFloat(), + squircleSize.toFloat(), + cornerDiameter, + cornerDiameter, + ) + fill(shadowShape) + dispose() + } + val blurredShadow = gaussianBlur(shadowLayer, shadowBlur) val out = BufferedImage(canvas, canvas, BufferedImage.TYPE_INT_ARGB) val g = out.createGraphics() @@ -86,21 +123,13 @@ object PlatformAppIcon { g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC) g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY) - val squircle = - RoundRectangle2D.Float( - squircleMargin.toFloat(), - squircleMargin.toFloat(), - squircleSize.toFloat(), - squircleSize.toFloat(), - cornerDiameter, - cornerDiameter, - ) - g.composite = AlphaComposite.Src + // Shadow first, then the white squircle on top, then the mark. + g.composite = AlphaComposite.SrcOver + g.drawImage(blurredShadow, 0, 0, null) + g.color = Color(0xFFFFFF) g.fill(squircle) - // Composite the source logo on top, scaled into the padded area. - g.composite = AlphaComposite.SrcOver g.clip = squircle g.drawImage(source, markOrigin, markOrigin, markSize, markSize, null) } finally { @@ -108,4 +137,30 @@ object PlatformAppIcon { } return out } + + /** + * Separable Gaussian blur via [ConvolveOp]. Splits the 2D kernel into two + * 1D passes (horizontal then vertical) — O(N) per pixel instead of O(N²) + * for a radius-N blur, which keeps startup snappy even at 1024x1024. + */ + private fun gaussianBlur( + src: BufferedImage, + radius: Int, + ): BufferedImage { + if (radius < 1) return src + val size = radius * 2 + 1 + val sigma = radius / 2f + val kernel = FloatArray(size) + var sum = 0f + for (i in 0 until size) { + val x = (i - radius).toFloat() + kernel[i] = kotlin.math.exp(-(x * x) / (2f * sigma * sigma)) + sum += kernel[i] + } + for (i in 0 until size) kernel[i] /= sum + + val horiz = ConvolveOp(Kernel(size, 1, kernel), ConvolveOp.EDGE_NO_OP, null) + val vert = ConvolveOp(Kernel(1, size, kernel), ConvolveOp.EDGE_NO_OP, null) + return vert.filter(horiz.filter(src, null), null) + } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/BookmarksScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/BookmarksScreen.kt index e692b64707..a3e4c144de 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/BookmarksScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/BookmarksScreen.kt @@ -252,12 +252,12 @@ fun BookmarksScreen( modifier = Modifier .fillMaxWidth() - .padding(16.dp), + .padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( text = "Bookmarks", - style = MaterialTheme.typography.headlineMedium, + style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onBackground, ) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DraftsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DraftsScreen.kt index 4f296d562a..22b2246a40 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DraftsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DraftsScreen.kt @@ -68,13 +68,16 @@ fun DraftsScreen( Column(modifier = Modifier.fillMaxSize()) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Text( "Drafts", - style = MaterialTheme.typography.headlineMedium, + style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onBackground, ) Button(onClick = { onOpenEditor(null) }) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadsScreen.kt index 618579e160..d779b3a77c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadsScreen.kt @@ -294,7 +294,10 @@ fun ReadsScreen( Column(modifier = Modifier.fillMaxSize()) { // Header — wraps on narrow columns FlowRow( - modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalArrangement = Arrangement.spacedBy(8.dp), ) { @@ -305,7 +308,7 @@ fun ReadsScreen( ) { Text( "Reads", - style = MaterialTheme.typography.headlineMedium, + style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onBackground, ) 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 2c80ef19ba..832f8dd2dd 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 @@ -331,13 +331,16 @@ fun SearchScreen( // Title row Row( - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Text( "Search", - style = MaterialTheme.typography.headlineMedium, + style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onBackground, ) Text( diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/DeckColumnContainer.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/DeckColumnContainer.kt index c870c2a8c3..d622e3c743 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/DeckColumnContainer.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/DeckColumnContainer.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme @@ -130,9 +129,10 @@ fun DeckColumnContainer( HorizontalDivider() - Box( - modifier = Modifier.fillMaxSize().padding(12.dp), - ) { + // Content runs edge-to-edge; each screen adds its own header padding + // to match the Messages pattern (padding(horizontal = 12, vertical = 8) + // on the title row, no outer wrapper). + Box(modifier = Modifier.fillMaxSize()) { // Always keep RootContent composed so state (e.g. search results) survives navigation RootContent( columnType = column.type, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/highlights/MyHighlightsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/highlights/MyHighlightsScreen.kt index 0c98354040..24d7c3c4cc 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/highlights/MyHighlightsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/highlights/MyHighlightsScreen.kt @@ -74,11 +74,12 @@ fun MyHighlightsScreen( Column(modifier = Modifier.fillMaxSize()) { Text( "Highlights", - style = MaterialTheme.typography.headlineMedium, + style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), ) - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(8.dp)) if (allHighlights.isEmpty()) { EmptyState(