Measure the explorer, and cap edit mode at what it can carry
Three numbers, taken on the emulator through the app's own render report and written into EXPLORER.md; the fixture tree the sandbox now builds is what they were taken against. The viewer's scan was on the main thread. Decision 8 said off it, and the first version did it in a `remember` inside the composition, which is not that -- 460ms of frozen screen on a 1 MiB file, long enough that the accessibility tree cannot be read, which is exactly what "the app has stopped" looks like from outside. It runs on Dispatchers.Default now, with a spinner where the file will be. Reading a megabyte is otherwise fine: the viewer is a row per line, and it opens and scrolls 28,660 of them. Edit mode needed a cap, and not the one the plan expected. The cost that matters is not the highlighting -- 40ms a keystroke at 128 kB, which is survivable -- it is Compose laying out one enormous text in the field: 2,027ms per frame at 128 kB, with typed characters dropped, and no response at all at 1 MiB. Switching highlighting off would have saved nothing, since every arrangement of a single text field pays it. So EDIT_LIMIT is 32 kB, the largest size actually measured as usable, and above it the pencil is disabled with the reason in words beside it: a disabled control teaches what the thing can do but cannot say why it is off, and a reader who cannot edit a file they can plainly read would otherwise conclude the app is broken. `FileLines.of` is timed like everything else here, so the figure lands in the render report rather than needing a harness to ask for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9c4d33273b
commit
2c12274285
6 files changed
+147
-24
No files matched your search
@@ -4,15 +4,21 @@ import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -23,12 +29,39 @@ import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/** The face every verbatim thing in this app is drawn in, and the one the gutter has to match. */
|
||||
@Composable
|
||||
fun codeStyle(): TextStyle =
|
||||
MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace)
|
||||
|
||||
/**
|
||||
* [content] scanned off the main thread, then drawn.
|
||||
*
|
||||
* Measured on the emulator on 2026-09-04: [FileLines.of] takes **460ms** on a 1 MiB Rust file
|
||||
* (28,660 lines) and 11ms on 32 kB. Called from a `remember` inside the composition, as it was
|
||||
* first written, that is 460ms of frozen screen at the size the server is willing to send -- long
|
||||
* enough that the accessibility tree cannot be read, which is what "the app has stopped" looks like
|
||||
* from outside. So it runs on [Dispatchers.Default] and the spinner is what the reader sees
|
||||
* meanwhile, in the place the file will appear.
|
||||
*
|
||||
* Keyed on the text and the language, so re-reading the same file does not rescan it and a file
|
||||
* that changed does.
|
||||
*/
|
||||
@Composable
|
||||
fun ScannedFile(content: String, language: Language?, modifier: Modifier = Modifier) {
|
||||
var lines by remember(content, language) { mutableStateOf<FileLines?>(null) }
|
||||
LaunchedEffect(content, language) {
|
||||
lines = withContext(Dispatchers.Default) { FileLines.of(content, language) }
|
||||
}
|
||||
when (val ready = lines) {
|
||||
null -> CircularProgressIndicator(Modifier.padding(8.dp))
|
||||
else -> FileViewer(ready, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A file, one line per row, coloured by the same scanner that colours a reply's code fences.
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user