From 39b426e0997ec0c43d5637dd3bed09f6daeb1695 Mon Sep 17 00:00:00 2001 From: Saad Khan Date: Wed, 17 Jun 2026 14:00:00 +0000 Subject: [PATCH 1/5] refactor: add eachGestureWhileActive as forEachGesture replacement --- .../burnoutcrew/reorderable/GestureLoop.kt | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/GestureLoop.kt diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/GestureLoop.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/GestureLoop.kt new file mode 100644 index 0000000..634506c --- /dev/null +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/GestureLoop.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2022 André Claßen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.burnoutcrew.reorderable + +import androidx.compose.ui.input.pointer.AwaitPointerEventScope +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.util.fastAll +import androidx.compose.ui.util.fastAny +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.isActive + +/** + * Replacement for the deprecated [androidx.compose.foundation.gestures.forEachGesture]. + * Use when the gesture loop needs arbitrary suspending calls (e.g. channel receive) + * that [androidx.compose.foundation.gestures.awaitEachGesture] does not support. + */ +internal suspend fun PointerInputScope.eachGestureWhileActive( + block: suspend PointerInputScope.() -> Unit +) { + val currentContext = currentCoroutineContext() + while (currentContext.isActive) { + try { + block() + } catch (e: CancellationException) { + if (!currentContext.isActive) throw e + awaitPointerEventScope { + awaitAllPointersUp() + } + } + } +} + +private suspend fun AwaitPointerEventScope.awaitAllPointersUp() { + if (currentEvent.changes.fastAll { !it.pressed }) return + do { + awaitPointerEvent(PointerEventPass.Initial) + } while (currentEvent.changes.fastAny { it.pressed }) +} From 23a8ce7f35ebc4169359b041057861dacd7d08b9 Mon Sep 17 00:00:00 2001 From: Saad Khan Date: Wed, 17 Jun 2026 15:00:00 +0000 Subject: [PATCH 2/5] refactor: add AwaitPointerEventScope long press overload --- .../burnoutcrew/reorderable/DragGesture.kt | 64 +++++++++---------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DragGesture.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DragGesture.kt index 24e90d9..0c5f537 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DragGesture.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DragGesture.kt @@ -88,50 +88,42 @@ internal suspend fun AwaitPointerEventScope.awaitPointerSlopOrCancellation( } } -internal suspend fun PointerInputScope.awaitLongPressOrCancellation( +internal suspend fun AwaitPointerEventScope.awaitLongPressOrCancellation( initialDown: PointerInputChange ): PointerInputChange? { var longPress: PointerInputChange? = null var currentDown = initialDown val longPressTimeout = viewConfiguration.longPressTimeoutMillis return try { - // wait for first tap up or long press withTimeout(longPressTimeout) { - awaitPointerEventScope { - var finished = false - while (!finished) { - val event = awaitPointerEvent(PointerEventPass.Main) - if (event.changes.fastAll { it.changedToUpIgnoreConsumed() }) { - // All pointers are up - finished = true - } + var finished = false + while (!finished) { + val event = awaitPointerEvent(PointerEventPass.Main) + if (event.changes.fastAll { it.changedToUpIgnoreConsumed() }) { + finished = true + } - if ( - event.changes.fastAny { - it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding) - } - ) { - finished = true // Canceled + if ( + event.changes.fastAny { + it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding) } + ) { + finished = true + } - // Check for cancel by position consumption. We can look on the Final pass of - // the existing pointer event because it comes after the Main pass we checked - // above. - val consumeCheck = awaitPointerEvent(PointerEventPass.Final) - if (consumeCheck.changes.fastAny { it.isConsumed }) { - finished = true - } - if (!event.isPointerUp(currentDown.id)) { - longPress = event.changes.fastFirstOrNull { it.id == currentDown.id } + val consumeCheck = awaitPointerEvent(PointerEventPass.Final) + if (consumeCheck.changes.fastAny { it.isConsumed }) { + finished = true + } + if (!event.isPointerUp(currentDown.id)) { + longPress = event.changes.fastFirstOrNull { it.id == currentDown.id } + } else { + val newPressed = event.changes.fastFirstOrNull { it.pressed } + if (newPressed != null) { + currentDown = newPressed + longPress = currentDown } else { - val newPressed = event.changes.fastFirstOrNull { it.pressed } - if (newPressed != null) { - currentDown = newPressed - longPress = currentDown - } else { - // should technically never happen as we checked it above - finished = true - } + finished = true } } } @@ -142,6 +134,12 @@ internal suspend fun PointerInputScope.awaitLongPressOrCancellation( } } +internal suspend fun PointerInputScope.awaitLongPressOrCancellation( + initialDown: PointerInputChange +): PointerInputChange? = awaitPointerEventScope { + awaitLongPressOrCancellation(initialDown) +} + private fun PointerEvent.isPointerUp(pointerId: PointerId): Boolean = changes.fastFirstOrNull { it.id == pointerId }?.pressed != true From 513627de78fc6e9f1a0a0a410f4ec2f5d9592ec4 Mon Sep 17 00:00:00 2001 From: Saad Khan Date: Wed, 17 Jun 2026 16:00:00 +0000 Subject: [PATCH 3/5] refactor: migrate detectReorder to awaitEachGesture --- .../burnoutcrew/reorderable/DetectReorder.kt | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt index d73f341..72a3839 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt @@ -15,6 +15,7 @@ */ package org.burnoutcrew.reorderable +import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.ui.Modifier @@ -25,20 +26,18 @@ import androidx.compose.ui.input.pointer.pointerInput fun Modifier.detectReorder(state: ReorderableState<*>) = this.then( Modifier.pointerInput(Unit) { - forEachGesture { - awaitPointerEventScope { - val down = awaitFirstDown(requireUnconsumed = false) - var drag: PointerInputChange? - var overSlop = Offset.Zero - do { - drag = awaitPointerSlopOrCancellation(down.id, down.type) { change, over -> - change.consume() - overSlop = over - } - } while (drag != null && !drag.isConsumed) - if (drag != null) { - state.interactions.trySend(StartDrag(down.id, overSlop)) + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + var drag: PointerInputChange? + var overSlop = Offset.Zero + do { + drag = awaitPointerSlopOrCancellation(down.id, down.type) { change, over -> + change.consume() + overSlop = over } + } while (drag != null && !drag.isConsumed) + if (drag != null) { + state.interactions.trySend(StartDrag(down.id, overSlop)) } } } @@ -57,4 +56,4 @@ fun Modifier.detectReorderAfterLongPress(state: ReorderableState<*>) = } } } - ) \ No newline at end of file + ) From 55f14ba95f89066a1afd5eca751246c15032841d Mon Sep 17 00:00:00 2001 From: Saad Khan Date: Wed, 17 Jun 2026 17:00:00 +0000 Subject: [PATCH 4/5] refactor: migrate detectReorderAfterLongPress to awaitEachGesture --- .../kotlin/org/burnoutcrew/reorderable/DetectReorder.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt index 72a3839..2fb524d 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt @@ -17,7 +17,6 @@ package org.burnoutcrew.reorderable import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown -import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.PointerInputChange @@ -47,10 +46,8 @@ fun Modifier.detectReorder(state: ReorderableState<*>) = fun Modifier.detectReorderAfterLongPress(state: ReorderableState<*>) = this.then( Modifier.pointerInput(Unit) { - forEachGesture { - val down = awaitPointerEventScope { - awaitFirstDown(requireUnconsumed = false) - } + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) awaitLongPressOrCancellation(down)?.also { state.interactions.trySend(StartDrag(down.id)) } From 14af6b45ded552bb4b50c54adaf5a9639ae1d08b Mon Sep 17 00:00:00 2001 From: Saad Khan Date: Wed, 17 Jun 2026 18:00:00 +0000 Subject: [PATCH 5/5] refactor: replace forEachGesture in Reorderable with eachGestureWhileActive --- .../kotlin/org/burnoutcrew/reorderable/Reorderable.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt index e9d6d07..a8e4941 100644 --- a/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt +++ b/reorderable/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt @@ -16,7 +16,6 @@ package org.burnoutcrew.reorderable import androidx.compose.foundation.gestures.drag -import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.PointerId @@ -31,7 +30,7 @@ fun Modifier.reorderable( state: ReorderableState<*> ) = then( Modifier.pointerInput(Unit) { - forEachGesture { + eachGestureWhileActive { val dragStart = state.interactions.receive() val down = awaitPointerEventScope { currentEvent.changes.fastFirstOrNull { it.id == dragStart.id }