From 242d7a7c42716f71953cc4c0bba09eac4e889184 Mon Sep 17 00:00:00 2001 From: weishaupt Date: Mon, 5 Oct 2026 14:07:02 -0400 Subject: [PATCH] Fade title-bar buttons in on hover, remove temp debug logger, bump to 0.1.6 - Nicklist, settings, minimize and close buttons in the channel window title bar now rest at alpha 0 and fade to full opacity on hover (graphicsLayer alpha, so they stay laid out and clickable). Resting alpha, hover alpha and fade duration are constants at the top of ChannelScreen.kt. Channel name and window dragging are unchanged. - Remove the unused temporary ircUiDebug helper now that the scrollback fix is confirmed. - Bump version to 0.1.6. Co-Authored-By: Claude Opus 5.5 --- build.gradle.kts | 4 +-- src/main/kotlin/ui/ChannelScreen.kt | 51 ++++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e898710..e712561 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.tpuahsiew.wirc" -version = "0.1.5" +version = "0.1.6" // No project-level repositories{} block here: settings.gradle.kts sets // dependencyResolutionManagement.repositoriesMode = FAIL_ON_PROJECT_REPOS and already @@ -38,7 +38,7 @@ compose.desktop { appResourcesRootDir.set(project.file("installerResources")) targetFormats(TargetFormat.Msi, TargetFormat.Exe) packageName = "wIRC" - packageVersion = "0.1.5" + packageVersion = "0.1.6" windows { menuGroup = "wIRC" upgradeUuid = "8f14e45f-ceea-4a5e-9c3b-5b1f0e8c2b7a" diff --git a/src/main/kotlin/ui/ChannelScreen.kt b/src/main/kotlin/ui/ChannelScreen.kt index a9f8ac0..75d3ddc 100644 --- a/src/main/kotlin/ui/ChannelScreen.kt +++ b/src/main/kotlin/ui/ChannelScreen.kt @@ -1,11 +1,15 @@ package ui +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.window.WindowDraggableArea import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -27,6 +31,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color @@ -68,11 +73,12 @@ import java.text.SimpleDateFormat import java.util.* import kotlin.math.floor -// TEMP DEBUG (see task: auto-scroll from-scratch audit) - remove once confirmed fixed. -// label identifies which window's log line this is, so multi-window repros are legible. -private fun ircUiDebug(label: String, msg: String) { - println("[ui-debug][$label] $msg") -} +// Title-bar button fade (nicklist / settings / minimize / close). The buttons sit at +// TITLE_BAR_BUTTON_REST_ALPHA until hovered, then animate to TITLE_BAR_BUTTON_HOVER_ALPHA +// over TITLE_BAR_BUTTON_FADE_MS. The channel name text is not affected. +private const val TITLE_BAR_BUTTON_REST_ALPHA = 0f +private const val TITLE_BAR_BUTTON_HOVER_ALPHA = 1f +private const val TITLE_BAR_BUTTON_FADE_MS = 150 // Single-space separator between the padded nick column and the message body. Previously // a 2-space trailing gap on CHAT lines (on top of the padEnd() column padding itself), @@ -406,6 +412,8 @@ fun WindowScope.ChannelScreen( // because manually accumulating dragAmount into windowState.position fights // with the OS's own window-move handling; WindowDraggableArea talks to that // directly instead of reimplementing it. + // The four buttons on the right fade in on hover (see FadingTitleBarButton); the + // channel name stays fully opaque. WindowDraggableArea(modifier = Modifier.fillMaxWidth()) { Row( modifier = Modifier @@ -416,20 +424,20 @@ fun WindowScope.ChannelScreen( ) { Text(channelName, color = textColor, fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.labelSmall) Row { - IconButton(onClick = { onShowUserListChange(!displaySettings.showUserList) }, modifier = Modifier.size(28.dp)) { + FadingTitleBarButton(onClick = { onShowUserListChange(!displaySettings.showUserList) }) { Icon(Icons.Default.Person, contentDescription = "Toggle user list", tint = textColor, modifier = Modifier.size(16.dp)) } - IconButton(onClick = { showSettings = !showSettings }, modifier = Modifier.size(28.dp)) { + FadingTitleBarButton(onClick = { showSettings = !showSettings }) { Icon(Icons.Default.Settings, contentDescription = "Window settings", tint = textColor, modifier = Modifier.size(16.dp)) } - IconButton(onClick = { windowState.isMinimized = true }, modifier = Modifier.size(28.dp)) { + FadingTitleBarButton(onClick = { windowState.isMinimized = true }) { Box( modifier = Modifier .size(width = 10.dp, height = 2.dp) .background(textColor) ) } - IconButton(onClick = onCloseWindow, modifier = Modifier.size(28.dp)) { + FadingTitleBarButton(onClick = onCloseWindow) { Icon(Icons.Default.Close, contentDescription = "Close window", tint = textColor, modifier = Modifier.size(16.dp)) } } @@ -796,6 +804,31 @@ fun WindowScope.ChannelScreen( } } +// A 28dp title-bar IconButton that rests invisible and fades in while hovered. The fade +// is a graphicsLayer alpha only - the button is always laid out, drawn and hit-tested, so +// it still gets hover and clicks at alpha 0. The alpha is read inside the graphicsLayer +// lambda so the animation only re-draws the layer instead of recomposing every frame. +// IconButton's own hover/press state layer is drawn inside this layer, so it still shows +// once the button has faded in. +@Composable +private fun FadingTitleBarButton(onClick: () -> Unit, content: @Composable () -> Unit) { + val hoverSource = remember { MutableInteractionSource() } + val hovered by hoverSource.collectIsHoveredAsState() + val alpha by animateFloatAsState( + targetValue = if (hovered) TITLE_BAR_BUTTON_HOVER_ALPHA else TITLE_BAR_BUTTON_REST_ALPHA, + animationSpec = tween(durationMillis = TITLE_BAR_BUTTON_FADE_MS) + ) + IconButton( + onClick = onClick, + modifier = Modifier + .size(28.dp) + .hoverable(hoverSource) + .graphicsLayer { this.alpha = alpha } + ) { + content() + } +} + // In-memory LRU cache of decoded image bitmaps, keyed by URL, so scrolling a channel // with image previews on doesn't re-fetch/re-decode images that already loaded. private object ImageCache {