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 bb81e4a..5db59d0 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/TranscriptScroll.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/TranscriptScroll.kt @@ -145,6 +145,7 @@ class TranscriptScroll(internal val scroll: ScrollState) { } tops = out topsStale = false + topsVersion++ } /** Where the last touch went down, in the content's own coordinates. */ @@ -256,17 +257,59 @@ class TranscriptScroll(internal val scroll: ScrollState) { * made a page of history land as one hundred-millisecond frame -- seventeen screens of markdown * shaped at once, because "not measured yet" described the whole page. */ - fun retains(seq: Long): Boolean { - val index = rowIndex[seq] ?: return true - val height = assumed(seq) + fun retains(seq: Long): Boolean = rowIndex[seq]?.let { it in retainedRange() } ?: true + + /** + * Which rows stay built, as a range of indices rather than a test each row makes for itself. + * + * A range can be guaranteed non-empty and a per-row test cannot, which is the point. Every row + * answering independently means a fault in the arithmetic stands *all* of them down at once and + * leaves a blank transcript with nothing measured, so nothing to correct it -- which is exactly + * what happened. Here the nearest row to the viewport is in the range by construction, whatever + * the numbers say, so the worst a mistake can do is build too few rows or too many. + */ + private fun retainedRange(): IntRange { refreshTops() - val top = tops.getOrNull(index) ?: return true + if (order.isEmpty()) return IntRange.EMPTY val viewportTop = scroll.maxValue - scroll.value - val margin = scroll.viewportSize * RETAIN_SCREENS - return top + height >= viewportTop - margin && - top <= viewportTop + scroll.viewportSize + margin + if (viewportTop == rangeAt && topsVersion == rangeVersion) return rangeFrom..rangeTo + val margin = (scroll.viewportSize * RETAIN_SCREENS).coerceAtLeast(1) + val from = viewportTop - margin + val to = viewportTop + scroll.viewportSize + margin + var first = -1 + var last = -1 + var nearest = 0 + var nearestGap = Int.MAX_VALUE + order.forEachIndexed { index, seq -> + val top = tops[index] + val bottom = top + assumed(seq) + if (bottom >= from && top <= to) { + if (first < 0) first = index + last = index + } + val gap = if (bottom < viewportTop) viewportTop - bottom else top - viewportTop + if (gap in 0..