diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/TranscriptScroll.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/TranscriptScroll.kt index 069221e..fcc4da2 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/TranscriptScroll.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/TranscriptScroll.kt @@ -418,6 +418,37 @@ class TranscriptScroll(internal val scroll: ScrollState) { * Getting that wrong does not look like a spacing bug; it shortens the content, which moves * everything the reader is looking at. */ + /** + * The spacers that stand in for rows [run], because one of them will not always do. + * + * `Modifier.height` turns into a fixed `Constraints`, and Compose packs a Constraints into a + * single Long -- with a width of zero it has eighteen bits for the height, so anything over + * 262,143px throws `Can't represent a width of 0 and height of N in Constraints` and takes the + * app down during measure. Collapsing every stood-down row into one spacer is what made that + * reachable: a long conversation scrolled to the newest end has its whole history above the + * window, and one seen here was 273,238px of it. + * + * Split rather than clamped, because the height is load-bearing -- it is what holds the + * transcript's total the same as the rows it replaces, and shortening it would move everything + * under the reader. The gaps the arrangement inserts between the pieces come out of the total + * for the same reason. + */ + fun spacerHeights(run: IntRange): List { + val total = runHeight(run) + if (total <= 0) return emptyList() + if (total <= SPACER_MAX) return listOf(total) + var pieces = 2 + while (true) { + val body = total - (pieces - 1) * spacing + if (body <= 0) return listOf(total.coerceAtMost(SPACER_MAX)) + if ((body + pieces - 1) / pieces <= SPACER_MAX) { + val each = body / pieces + return List(pieces) { each + if (it == 0) body - each * pieces else 0 } + } + pieces++ + } + } + fun runHeight(run: IntRange): Int { if (run.isEmpty() || order.isEmpty()) return 0 var total = 0 @@ -854,10 +885,13 @@ val TRANSCRIPT_PADDING: PaddingValues = PaddingValues(16.dp) @Composable private fun StoodDownRun(state: TranscriptScroll, run: IntRange) { if (run.isEmpty()) return - DebugStats.count("rows stood down as one spacer") - Spacer( - Modifier.fillMaxWidth().height(with(LocalDensity.current) { state.runHeight(run).toDp() }) - ) + val heights = state.spacerHeights(run) + DebugStats.count("rows stood down as spacers") + DebugStats.atLeast("spacers standing in for a run", heights.size.toLong()) + val density = LocalDensity.current + heights.forEachIndexed { index, height -> + key(index) { Spacer(Modifier.fillMaxWidth().height(with(density) { height.toDp() })) } + } } @Composable @@ -919,5 +953,14 @@ private const val STAND_UP_PER_FRAME = 2 */ private const val SEED_SCREENS = 2 +/** + * The tallest a single spacer may be; see [TranscriptScroll.spacerHeights]. + * + * Comfortably under the 262,143px that a fixed-width `Constraints` can hold, rather than at it: the + * limit depends on how many bits the width took, and a spacer that is nearly the maximum is a crash + * waiting for a wider screen. + */ +private const val SPACER_MAX = 100_000 + /** What a row is assumed to be worth before any of them have been measured. */ private const val ROW_GUESS = 800