4 Commits
Author SHA1 Message Date
weishauptandClaude Sonnet 5.5 d58b5772df Bump to 0.1.17
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 14:12:59 -04:00
weishauptandClaude Sonnet 5.5 fff75700ac Fix scrollback snapping back to bottom after scrolling up
Wheel-up now clears stickToBottom immediately on the raw scroll event, wheel-down re-checks the resting position, and the auto-scroll effect re-checks stickToBottom right before scrollToItem(0). The scroll-state effect could miss a wheel scroll that began and ended within one frame, so the next incoming line pulled the buffer back to the bottom.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW57ZvxRmxcDF7AFACLSoV
2026-10-29 18:09:13 +00:00
weishauptandClaude Sonnet 5.5 ed5d9a9c78 Fix line spacing compile error (TextUnit addition), bump to 0.1.16
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 22:43:19 -04:00
weishauptandClaude Sonnet 5.5 03964ca313 Loosen buffer line spacing for all fonts
Add extra vertical space to every buffer line (LINE_SPACING_EXTRA, 30% of font size) on top of the font natural line height. The same style measures and draws rows so whole-line snapping stays in step.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW57ZwRSygeowNBguQEUUB
2026-10-29 02:30:39 +00:00
2 changed files with 60 additions and 15 deletions
+3 -3
View File
@@ -1,4 +1,4 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins { plugins {
kotlin("jvm") version "1.9.24" kotlin("jvm") version "1.9.24"
@@ -6,7 +6,7 @@ plugins {
} }
group = "com.tpuahsiew.wirc" group = "com.tpuahsiew.wirc"
version = "0.1.15" version = "0.1.17"
// No project-level repositories{} block here: settings.gradle.kts sets // No project-level repositories{} block here: settings.gradle.kts sets
// dependencyResolutionManagement.repositoriesMode = FAIL_ON_PROJECT_REPOS and already // dependencyResolutionManagement.repositoriesMode = FAIL_ON_PROJECT_REPOS and already
@@ -40,7 +40,7 @@ compose.desktop {
appResourcesRootDir.set(project.file("installerResources")) appResourcesRootDir.set(project.file("installerResources"))
targetFormats(TargetFormat.Msi, TargetFormat.Exe) targetFormats(TargetFormat.Msi, TargetFormat.Exe)
packageName = "wIRC" packageName = "wIRC"
packageVersion = "0.1.15" packageVersion = "0.1.17"
windows { windows {
menuGroup = "wIRC" menuGroup = "wIRC"
upgradeUuid = "8f14e45f-ceea-4a5e-9c3b-5b1f0e8c2b7a" upgradeUuid = "8f14e45f-ceea-4a5e-9c3b-5b1f0e8c2b7a"
+55 -10
View File
@@ -1,3 +1,5 @@
@file:OptIn(androidx.compose.ui.text.ExperimentalTextApi::class)
package ui package ui
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
@@ -816,7 +818,7 @@ fun MessageScrollback(
.first { item -> .first { item ->
item == null || (item.index < capturedList.size && item.key == capturedList[item.index].seq) item == null || (item.index < capturedList.size && item.key == capturedList[item.index].seq)
} }
listState.scrollToItem(0) if (stickToBottom) listState.scrollToItem(0)
} }
} }
@@ -847,14 +849,30 @@ fun MessageScrollback(
// then converts that now-stale pixel value using the NEW density, so the // then converts that now-stale pixel value using the NEW density, so the
// two silently disagreed until the window (and this remember scope) was // two silently disagreed until the window (and this remember scope) was
// recreated by closing and reopening it. // recreated by closing and reopening it.
val lineHeightPx = remember(displaySettings.fontSize, displaySettings.fontId, density) { // The buffer text style gets extra vertical air on top of the font's own natural
textMeasurer.measure( // line height (every bundled font has different metrics, so a fixed multiplier
text = "Mg", // would loosen some fonts and tighten others). The same style is used to measure
style = androidx.compose.ui.text.TextStyle( // the line height below AND to draw every row, so the whole-line snapping stays
// exactly in step with what is on screen. LineHeightStyle with Trim.None and a
// centered alignment makes each line exactly lineHeight tall, with the extra
// space split above and below the text.
val bufferTextStyle = remember(displaySettings.fontSize, displaySettings.fontId, density) {
val base = androidx.compose.ui.text.TextStyle(
fontFamily = AppFonts.familyFor(displaySettings.fontId), fontFamily = AppFonts.familyFor(displaySettings.fontId),
fontSize = displaySettings.fontSize.sp fontSize = displaySettings.fontSize.sp
) )
).size.height val naturalPx = textMeasurer.measure(text = "Mg", style = base).size.height
val spacedLine = with(density) { (naturalPx + (displaySettings.fontSize * LINE_SPACING_EXTRA).sp.toPx()).toSp() }
base.copy(
lineHeight = spacedLine,
lineHeightStyle = androidx.compose.ui.text.style.LineHeightStyle(
alignment = androidx.compose.ui.text.style.LineHeightStyle.Alignment.Center,
trim = androidx.compose.ui.text.style.LineHeightStyle.Trim.None
)
)
}
val lineHeightPx = remember(bufferTextStyle, density) {
textMeasurer.measure(text = "Mg", style = bufferTextStyle).size.height
} }
val lineHeightDp = with(density) { lineHeightPx.toDp() } val lineHeightDp = with(density) { lineHeightPx.toDp() }
// Ruled out (see task: newest-message-missing investigation) as the // Ruled out (see task: newest-message-missing investigation) as the
@@ -885,10 +903,7 @@ fun MessageScrollback(
// offset correction through stickToBottom's own effect instead of a // offset correction through stickToBottom's own effect instead of a
// second independent one. // second independent one.
val textStyle = androidx.compose.ui.text.TextStyle( val textStyle = bufferTextStyle
fontFamily = AppFonts.familyFor(displaySettings.fontId),
fontSize = displaySettings.fontSize.sp
)
val selection = selectionState.selection val selection = selectionState.selection
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
@@ -908,6 +923,31 @@ fun MessageScrollback(
.height(snappedHeight) .height(snappedHeight)
.onGloballyPositioned { containerCoords[0] = it } .onGloballyPositioned { containerCoords[0] = it }
.pointerHoverIcon(PointerIcon.Text) .pointerHoverIcon(PointerIcon.Text)
// Wheel-up means the user is reading backscroll: stop sticking to the bottom RIGHT NOW,
// on the raw wheel event. The scroll-state effect up top can miss a wheel scroll that starts
// and finishes inside one frame (isScrollInProgress is never seen as true), which left
// stickToBottom set and let the next incoming line yank the buffer back down.
// Wheel-down re-checks the resting position shortly after and re-enables sticking if the
// user is back at the bottom.
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Initial)
if (event.type == PointerEventType.Scroll) {
val dy = event.changes.firstOrNull()?.scrollDelta?.y ?: 0f
if (dy < 0f) {
stickToBottom = false
} else if (dy > 0f) {
coroutineScope.launch {
delay(200)
stickToBottom = listState.firstVisibleItemIndex == 0 &&
listState.firstVisibleItemScrollOffset <= stickToBottomThresholdPx
}
}
}
}
}
}
// Remembers the row under a right-click for the menu's "Copy line". Watches the // Remembers the row under a right-click for the menu's "Copy line". Watches the
// Initial pass, because ContextMenuArea takes the right-press there, before // Initial pass, because ContextMenuArea takes the right-press there, before
// the handler below would see it. // the handler below would see it.
@@ -1114,6 +1154,11 @@ fun MessageScrollback(
} }
} }
// Extra vertical space added to every buffer line, as a fraction of the font size, on top of
// the font's natural line height. 0.30 is a modest loosening; raise it for more air between
// lines (0.45 is roomy), lower it for less. Applies to every font in the font list.
private const val LINE_SPACING_EXTRA = 0.30f
// Reports this element's height as a whole number of text lines ([linePx] each): rounded up // Reports this element's height as a whole number of text lines ([linePx] each): rounded up
// for image slots (the picture is never cut), to the nearest line for text rows (a wrapped // for image slots (the picture is never cut), to the nearest line for text rows (a wrapped
// message can measure a pixel or two off an exact multiple). Content is drawn from the top. // message can measure a pixel or two off an exact multiple). Content is drawn from the top.