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 <noreply@anthropic.com>
This commit is contained in:
weishaupt
2026-10-05 14:07:02 -04:00
co-authored by Claude Opus 5.5
parent 21b52477aa
commit 242d7a7c42
2 changed files with 44 additions and 11 deletions
+2 -2
View File
@@ -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"
+42 -9
View File
@@ -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 {