diff --git a/EXPLORER.md b/EXPLORER.md index b5b18fe..0775389 100644 --- a/EXPLORER.md +++ b/EXPLORER.md @@ -181,6 +181,15 @@ can carry, so a minified file is a scroll that stops early rather than a crash. Reported by Iris on 2026-09-04 as "it seems to affect different rows differently", which is precisely what a per-row range looks like. +**The stretch at the ends is one effect too**, shared by every row and +rendered once on the box around the list -- `horizontalScroll` makes its +own per node otherwise, so only the line under the finger bent and the +rest of the file sat still beside it. That is the same complaint one layer +further out, and it is only fixable now that every row agrees where the +end is. It cannot be seen from this VM: the emulator's screenshots come +back with no stretch in them at all, for any scrollable, so this one is +checked on the phone. + Line numbers are a gutter in each row, right-aligned, with the gutter width taken from the digit count of the line count in the same monospace style -- so a 9-line file and a 12,000-line file each get exactly the diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/FileViewer.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/FileViewer.kt index 32201de..745db7f 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/FileViewer.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/FileViewer.kt @@ -1,13 +1,16 @@ package com.example.aiapp import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.overscroll +import androidx.compose.foundation.rememberOverscrollEffect import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.CircularProgressIndicator @@ -21,6 +24,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle @@ -85,6 +89,14 @@ fun ScannedFile(content: String, language: Language?, modifier: Modifier = Modif * as the block this comment always claimed it was. Reported by Iris on 2026-09-04 as "it seems to * affect different rows differently", which is exactly what a per-row range looks like. * + * The stretch at the ends of the travel is **one** effect for the whole file, rendered on the box + * around the list rather than by each row. `horizontalScroll` makes its own per node otherwise, so + * only the line under the finger stretched and the rest of the file sat still beside it -- the same + * complaint as the offsets above, one layer further out. Handing every row the same effect and + * rendering it once is what makes the file bend as the block it scrolls as. Only possible because + * every row now has the same range: rows that disagreed about where the end was would disagree + * about when to stretch. + * * The gutter stays put while the text scrolls, so a line number is still there to read at the right * hand end of a long line. Its width is measured from the digit count of the line count in the very * style it is drawn in, so a nine-line file and a twelve-thousand-line file each get exactly what @@ -94,23 +106,30 @@ fun ScannedFile(content: String, language: Language?, modifier: Modifier = Modif fun FileViewer(lines: FileLines, modifier: Modifier = Modifier) { val style = codeStyle() val scroll = rememberScrollState() + val overscroll = rememberOverscrollEffect() val gutter = gutterWidth(lines.size, style) val content = contentWidth(lines.columns, style) // One container around the whole file rather than one per line, so a selection can run across // lines -- the same arrangement the transcript uses. SelectionContainer(modifier) { - LazyColumn(Modifier.fillMaxWidth()) { - items(lines.size) { index -> - Row(verticalAlignment = Alignment.Top) { - LineNumber(index + 1, gutter, style) - Text( - lines.line(index), - style = style, - softWrap = false, - // The scroll outside the width: the scrolling node's viewport is what - // the row has room for, and its content is the whole file's widest line. - modifier = Modifier.horizontalScroll(scroll).width(content), - ) + // The stretch is drawn here, once, over everything the viewport holds; the rows below only + // feed it. `clipToBounds` because a stretch draws outside the box it came from. + Box(Modifier.fillMaxSize().clipToBounds().overscroll(overscroll)) { + LazyColumn(Modifier.fillMaxSize()) { + items(lines.size) { index -> + Row(verticalAlignment = Alignment.Top) { + LineNumber(index + 1, gutter, style) + Text( + lines.line(index), + style = style, + softWrap = false, + // The scroll outside the width: the scrolling node's viewport is what + // the row has room for, and its content is the whole file's widest + // line. The shared effect is given to every row and rendered by none + // of them -- see the box above. + modifier = Modifier.horizontalScroll(scroll, overscroll).width(content), + ) + } } } }