Stretch the whole file at the ends, not one line

`Modifier.horizontalScroll` builds its own overscroll effect per node, so
with a node per row only the line under the finger bent when the file was
dragged past either end and the rest sat still beside it -- the same
complaint as the offsets, one layer out.

There is an overload that takes the effect instead of making one, and it
leaves the rendering to the caller. So the viewer makes one effect, hands
it to every row, and renders it once on a box around the list: the file
bends as the block it scrolls as. This only works now that every row is
the same width -- rows that disagreed about where the end was would
disagree about when to stretch.

**Not seen working.** Measured on the emulator here: over-dragging well
past the end and capturing mid-gesture produces a frame with no stretch in
it at all, and the list's own vertical overscroll does not appear either,
so this VM cannot show the effect for any scrollable. Noted in
~/.claude/MACHINE.md so the next session does not spend the same half hour
on it. What was checked here is everything either side: the scroll still
reaches both ends, the position survives scrolling vertically, and nothing
else moved. The stretch itself wants a look on the phone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-04 01:38:21 -04:00
1 parent 457907087c
commit 7b08a71e64
2 files changed
+32 -4

No files matched your search

+9
View File
@@ -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
@@ -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,12 +106,16 @@ 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()) {
// 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)
@@ -108,13 +124,16 @@ fun FileViewer(lines: FileLines, modifier: Modifier = Modifier) {
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 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),
)
}
}
}
}
}
}
/**