package com.example.aiapp import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp /** * An item something is happening to: dimmed, drained of colour, inert, with a spinner and the name * of the operation over it. * * One composable rather than a pattern each list repeats, because "this row is busy" has to look * the same in the import list and the session list or the appearance becomes a per-screen dialect. * * [label] names the operation and `null` means none is running. One parameter rather than a boolean * beside a string, which can disagree. It is a *word* because a spinner alone cannot say which * operation this is -- deleting and importing are different in kind. * * It does **not** make the row inert; the caller disables its own click handling while it passes a * label. That was the other way round at first -- an overlay consuming pointer events -- and it * swallowed the drag along with the tap, so a list could not be scrolled while anything in it was * busy. */ @Composable fun BusyItem(label: String?, content: @Composable () -> Unit) { Box { Box(Modifier.busy(label != null)) { content() } if (label != null) { Box(Modifier.matchParentSize(), contentAlignment = Alignment.Center) { Row(verticalAlignment = Alignment.CenterVertically) { CircularProgressIndicator( modifier = Modifier.width(16.dp).height(16.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onSurface, ) Spacer(Modifier.width(8.dp)) // Full strength, over content that is not: the operation is the one thing on // this row that is still current, and it has to read against a card whose own // text is still visible behind it. Text( label, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurface, ) } } } } } /** * How an item looks while it is being acted on: darker, and nearly grey. * * Both, rather than either alone. Dimming by itself is the same cue as a disabled control, so a * busy row read as one more thing that could not be tapped. Draining the colour is what says the * row is *suspended* -- the status word and everything else that means something by its colour stop * meaning it for as long as the operation runs, which is exactly true. * * Not all the way to grey: a row with no colour left is hard to find again in a list. */ private fun Modifier.busy(busy: Boolean): Modifier = if (!busy) this else this.graphicsLayer { alpha = 0.5f } .drawWithContent { drawIntoCanvas { canvas -> canvas.saveLayer( Rect(Offset.Zero, size), Paint().apply { colorFilter = ColorFilter.colorMatrix( ColorMatrix().apply { setToSaturation(0.2f) } ) }, ) drawContent() canvas.restore() } }