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 {