diff --git a/EXPLORER.md b/EXPLORER.md index 7deb312..75b0ca6 100644 --- a/EXPLORER.md +++ b/EXPLORER.md @@ -214,7 +214,7 @@ which highlighting is on -- see "Numbers to measure". `FilesScreen` is composed **on top of** the session in the same `Box`, and the session stays composed under it: its event stream keeps flowing, its scroll position and draft stay where they were, and returning from a file -costs nothing. Back -- the button, the platform gesture and `swipeBack` -- +costs nothing. Back -- the button and the platform gesture -- clears `files` when it is set and goes to the list otherwise. Inside the explorer the same back steps one level: editor → viewer (with the unsaved question), viewer → listing, listing → parent directory it came from, and diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/AppRoot.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/AppRoot.kt index ad1b81c..28a6d77 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/AppRoot.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/AppRoot.kt @@ -198,21 +198,17 @@ fun AppRoot( // row key. Only reachable since a notification can move straight from one session to // another; every other way here passes through [Screen.Main], which disposes it anyway. key(here.summary.id) { - // The gesture goes on a box around the screen rather than inside it, so it is the - // outermost thing in the tree and everything within has already had its chance at - // the drag. See [swipeBack]. No imePadding here, for the reason above. - Box(Modifier.swipeBack(goToMain)) { - SessionScreen( - settings = current, - summary = here.summary, - onBack = goToMain, - share = share, - onShareTaken = { share = null }, - ) - } + // No imePadding here, for the reason above. + SessionScreen( + settings = current, + summary = here.summary, + onBack = goToMain, + share = share, + onShareTaken = { share = null }, + ) } is Screen.Spawn -> - Box(Modifier.imePadding().swipeBack(goToMain)) { + Box(Modifier.imePadding()) { SpawnScreen( settings = current, onSpawned = { spawned -> @@ -223,7 +219,7 @@ fun AppRoot( ) } is Screen.Settings -> - Box(Modifier.imePadding().swipeBack(goToMain)) { + Box(Modifier.imePadding()) { SettingsScreen( existing = current, onSaved = { saved -> diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/SwipeBack.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/SwipeBack.kt deleted file mode 100644 index 7697715..0000000 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SwipeBack.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.example.aiapp - -import androidx.compose.animation.core.Animatable -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.launch - -/** - * Dragging the screen to the right to step back to the one behind it. - * - * The platform's own back gesture is a swipe from the very edge, and only from there; on a phone - * held in one hand the way back from a session is either that narrow strip or the arrow at the top - * left, which is the far corner from the thumb. This is the same movement from anywhere on the - * screen. - * - * **It loses every argument.** The gesture is a plain horizontal [draggable] on the outside of the - * screen, so anything inside that wants horizontal drags has already taken them by the time this - * would see them: pointer events reach the innermost node first, and a drag a child has consumed - * never crosses this modifier's touch slop. That is what keeps a wide code fence, a table scrolled - * sideways or a text selection working -- they are the components the reader meant, and this is - * only what is left over. Vertical drags are not its orientation, so the transcript scrolls - * untouched. - * - * The screen follows the finger rather than jumping at the end, because a gesture with no feedback - * cannot be aborted: the reader has to be able to see it starting and change their mind. Released - * short of [SWIPE_BACK_TRAVEL] it slides back and nothing happens. Right rather than left, and only - * right, since there is nothing forward of these screens to go to. - */ -@Composable -fun Modifier.swipeBack(onBack: () -> Unit): Modifier { - val offset = remember { Animatable(0f) } - val scope = rememberCoroutineScope() - val travel = with(LocalDensity.current) { SWIPE_BACK_TRAVEL.toPx() } - return draggable( - state = - rememberDraggableState { delta -> - // Rightward only: a leftward drag stays at zero rather than lifting the - // screen off its left edge, which would look like a gesture that does - // something and does not. - scope.launch { offset.snapTo((offset.value + delta).coerceAtLeast(0f)) } - }, - orientation = Orientation.Horizontal, - onDragStopped = { - if (offset.value >= travel) { - onBack() - // Straight back rather than animated: the screen this was moving is being - // replaced, and animating it home first would show the old one sliding back - // into place after the new one had arrived. - offset.snapTo(0f) - } else { - offset.animateTo(0f) - } - }, - ) - // Read inside the block, so following the finger is a draw-phase change and costs no - // recomposition of the screen being dragged. - .graphicsLayer { translationX = offset.value } -} - -/** How far the screen has to be pulled for letting go to mean "back" rather than "never mind". */ -private val SWIPE_BACK_TRAVEL: Dp = 96.dp