Merge "Update detectDragGestures to be re-used in Draggables." into androidx-main
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index aaf6abe..44e9b90 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -374,7 +374,8 @@
 ) : DragGestureNode(
     canDrag = AlwaysDrag,
     enabled = enabled,
-    interactionSource = interactionSource
+    interactionSource = interactionSource,
+    orientationLock = orientation
 ) {
 
     open suspend fun AnchoredDragScope.anchoredDrag(
@@ -389,9 +390,6 @@
         state.anchoredDrag(MutatePriority.Default) { anchoredDrag(forEachDelta) }
     }
 
-    override val pointerDirectionConfig: PointerDirectionConfig
-        get() = orientation.toPointerDirectionConfig()
-
     override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
 
     override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
@@ -429,7 +427,8 @@
         update(
             enabled = enabled,
             interactionSource = interactionSource,
-            isResetPointerInputHandling = resetPointerInputHandling,
+            shouldResetPointerInputHandling = resetPointerInputHandling,
+            orientationLock = orientation
         )
     }
 
@@ -742,7 +741,8 @@
     @Deprecated(
         message = "Use the progress function to query the progress between two specified " +
             "anchors.",
-        replaceWith = ReplaceWith("progress(state.settledValue, state.targetValue)"))
+        replaceWith = ReplaceWith("progress(state.settledValue, state.targetValue)")
+    )
     @get:FloatRange(from = 0.0, to = 1.0)
     val progress: Float by derivedStateOf(structuralEqualityPolicy()) {
         val a = anchors.positionOf(settledValue)
@@ -1296,7 +1296,7 @@
     }
 }
 
-private fun<K> ObjectFloatMap<K>.minValueOrNaN(): Float {
+private fun <K> ObjectFloatMap<K>.minValueOrNaN(): Float {
     if (size == 1) return Float.NaN
     var minValue = Float.POSITIVE_INFINITY
     forEachValue { value ->
@@ -1307,7 +1307,7 @@
     return minValue
 }
 
-private fun<K> ObjectFloatMap<K>.maxValueOrNaN(): Float {
+private fun <K> ObjectFloatMap<K>.maxValueOrNaN(): Float {
     if (size == 1) return Float.NaN
     var maxValue = Float.NEGATIVE_INFINITY
     forEachValue { value ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index 4d7c7fc..86835e4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -45,7 +45,7 @@
 import androidx.compose.ui.util.fastAny
 import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastForEach
-import kotlin.math.abs
+import kotlin.math.absoluteValue
 import kotlin.math.sign
 import kotlinx.coroutines.CancellationException
 
@@ -79,7 +79,7 @@
         pointerId,
         PointerType.Touch,
         onPointerSlopReached = onTouchSlopReached,
-        pointerDirectionConfig = BidirectionalPointerDirectionConfig,
+        orientation = null,
     )
 }
 
@@ -170,33 +170,113 @@
     onDragEnd: () -> Unit = { },
     onDragCancel: () -> Unit = { },
     onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
+) = detectDragGestures(
+    onDragStart = { _, offset -> onDragStart(offset) },
+    onDragEnd = { onDragEnd.invoke() },
+    onDragCancel = onDragCancel,
+    shouldAwaitTouchSlop = { true },
+    orientationLock = null,
+    onDrag = onDrag
+)
+
+/**
+ * A Gesture detector that waits for pointer down and touch slop in the direction specified by
+ * [orientationLock] and then calls [onDrag] for each drag event.
+ * It follows the touch slop detection of [awaitTouchSlopOrCancellation] but will consume the
+ * position change automatically once the touch slop has been crossed, the amount of drag over
+ * the touch slop is reported as the first drag event [onDrag] after the slop is crossed.
+ * If [shouldAwaitTouchSlop] returns true the touch slop recognition phase will be ignored
+ * and the drag gesture will be recognized immediately.The first [onDrag] in this case will report
+ * an [Offset.Zero].
+ *
+ * [onDragStart] is called when the touch slop has been passed and includes an [Offset] representing
+ * the last known pointer position relative to the containing element as well as  the initial
+ * down event that triggered this gesture detection cycle. The [Offset] can be outside
+ * the actual bounds of the element itself meaning the numbers can be negative or larger than the
+ * element bounds if the touch target is smaller than the
+ * [ViewConfiguration.minimumTouchTargetSize].
+ *
+ * [onDragEnd] is called after all pointers are up with the event change of the up event
+ * and [onDragCancel] is called if another gesture has consumed pointer input,
+ * canceling this gesture.
+ *
+ * @param onDragStart A lambda to be called when the drag gesture starts, it contains information
+ * about the triggering [PointerInputChange] and post slop delta.
+ * @param onDragEnd A lambda to be called when the gesture ends. It contains information about the
+ * up [PointerInputChange] that finished the gesture.
+ * @param onDragCancel A lambda to be called when the gesture is cancelled either by an error or
+ * when it was consumed.
+ * @param shouldAwaitTouchSlop Indicates if touch slop detection should be skipped.
+ * @param orientationLock Optionally locks detection to this orientation, this means, when this is
+ * provided, touch slop detection and drag event detection will be conditioned to the given
+ * orientation axis. [onDrag] will still dispatch events on with information in both axis, but
+ * if orientation lock is provided, only events that happen on the given orientation will be
+ * considered. If no value is provided (i.e. null) touch slop and drag detection will happen on
+ * an "any" orientation basis, that is, touch slop will be detected if crossed in either direction
+ * and drag events will be dispatched if present in either direction.
+ * @param onDrag A lambda to be called for each delta event in the gesture. It contains information
+ * about the [PointerInputChange] and the movement offset.
+ *
+ * Example Usage:
+ * @sample androidx.compose.foundation.samples.DetectDragGesturesSample
+ *
+ * @see detectVerticalDragGestures
+ * @see detectHorizontalDragGestures
+ * @see detectDragGesturesAfterLongPress to detect gestures after long press
+ */
+internal suspend fun PointerInputScope.detectDragGestures(
+    onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit,
+    onDragEnd: (change: PointerInputChange) -> Unit,
+    onDragCancel: () -> Unit,
+    shouldAwaitTouchSlop: () -> Boolean,
+    orientationLock: Orientation?,
+    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
 ) {
     awaitEachGesture {
+        val initialDown =
+            awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
+        val awaitTouchSlop = shouldAwaitTouchSlop()
+
+        if (!awaitTouchSlop) {
+            initialDown.consume()
+        }
         val down = awaitFirstDown(requireUnconsumed = false)
         var drag: PointerInputChange?
         var overSlop = Offset.Zero
-        do {
-            drag = awaitPointerSlopOrCancellation(
-                down.id,
-                down.type,
-                pointerDirectionConfig = BidirectionalPointerDirectionConfig
-            ) { change, over ->
-                change.consume()
-                overSlop = over
-            }
-        } while (drag != null && !drag.isConsumed)
-        if (drag != null) {
-            onDragStart.invoke(drag.position)
-            onDrag(drag, overSlop)
-            if (
-                !drag(drag.id) {
-                    onDrag(it, it.positionChange())
-                    it.consume()
+        var initialDelta = Offset.Zero
+
+        if (awaitTouchSlop) {
+            do {
+                drag = awaitPointerSlopOrCancellation(
+                    down.id,
+                    down.type,
+                    orientation = orientationLock
+                ) { change, over ->
+                    change.consume()
+                    overSlop = over
                 }
-            ) {
+            } while (drag != null && !drag.isConsumed)
+            initialDelta = drag?.position ?: Offset.Zero
+        } else {
+            drag = initialDown
+        }
+
+        if (drag != null) {
+            onDragStart.invoke(initialDown, initialDelta)
+            onDrag(drag, overSlop)
+            val upEvent = drag(
+                pointerId = drag.id,
+                onDrag = {
+                    onDrag(it, it.positionChange())
+                },
+                orientation = orientationLock,
+                motionConsumed = {
+                    it.isConsumed
+                })
+            if (upEvent == null) {
                 onDragCancel()
             } else {
-                onDragEnd()
+                onDragEnd(upEvent)
             }
         }
     }
@@ -288,7 +368,7 @@
     pointerId = pointerId,
     pointerType = PointerType.Touch,
     onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.y) },
-    pointerDirectionConfig = VerticalPointerDirectionConfig
+    orientation = Orientation.Vertical
 )
 
 internal suspend fun AwaitPointerEventScope.awaitVerticalPointerSlopOrCancellation(
@@ -299,7 +379,7 @@
     pointerId = pointerId,
     pointerType = pointerType,
     onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.y) },
-    pointerDirectionConfig = VerticalPointerDirectionConfig
+    orientation = Orientation.Vertical
 )
 
 /**
@@ -324,7 +404,7 @@
 ): Boolean = drag(
     pointerId = pointerId,
     onDrag = onDrag,
-    hasDragged = { it.positionChangeIgnoreConsumed().y != 0f },
+    orientation = Orientation.Vertical,
     motionConsumed = { it.isConsumed }
 ) != null
 
@@ -439,7 +519,7 @@
     pointerId = pointerId,
     pointerType = PointerType.Touch,
     onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.x) },
-    pointerDirectionConfig = HorizontalPointerDirectionConfig
+    orientation = Orientation.Horizontal
 )
 
 internal suspend fun AwaitPointerEventScope.awaitHorizontalPointerSlopOrCancellation(
@@ -450,7 +530,7 @@
     pointerId = pointerId,
     pointerType = pointerType,
     onPointerSlopReached = { change, overSlop -> onPointerSlopReached(change, overSlop.x) },
-    pointerDirectionConfig = HorizontalPointerDirectionConfig
+    orientation = Orientation.Horizontal
 )
 
 /**
@@ -472,7 +552,7 @@
 ): Boolean = drag(
     pointerId = pointerId,
     onDrag = onDrag,
-    hasDragged = { it.positionChangeIgnoreConsumed().x != 0f },
+    orientation = Orientation.Horizontal,
     motionConsumed = { it.isConsumed }
 ) != null
 
@@ -563,9 +643,12 @@
 
 /**
  * Continues to read drag events until all pointers are up or the drag event is canceled.
- * The initial pointer to use for driving the drag is [pointerId]. [hasDragged]
- * passes the result whether a change was detected from the drag function or not. [onDrag] is called
- * whenever the pointer moves and [hasDragged] returns non-zero.
+ * The initial pointer to use for driving the drag is [pointerId]. [onDrag] is called
+ * whenever the pointer moves. The up event is returned at the end of the drag gesture.
+ *
+ * @param pointerId The pointer where that is driving the gesture.
+ * @param onDrag Callback for every new drag event.
+ * @param motionConsumed If the PointerInputChange should be considered as consumed.
  *
  * @return The last pointer input event change when gesture ended with all pointers up
  * and null when the gesture was canceled.
@@ -573,7 +656,7 @@
 internal suspend inline fun AwaitPointerEventScope.drag(
     pointerId: PointerId,
     onDrag: (PointerInputChange) -> Unit,
-    hasDragged: (PointerInputChange) -> Boolean,
+    orientation: Orientation?,
     motionConsumed: (PointerInputChange) -> Boolean
 ): PointerInputChange? {
     if (currentEvent.isPointerUp(pointerId)) {
@@ -581,7 +664,15 @@
     }
     var pointer = pointerId
     while (true) {
-        val change = awaitDragOrUp(pointer, hasDragged) ?: return null
+        val change = awaitDragOrUp(pointer) {
+            val positionChange = it.positionChangeIgnoreConsumed()
+            val motionChange = if (orientation == null) {
+                positionChange.getDistance()
+            } else {
+                if (orientation == Orientation.Vertical) positionChange.y else positionChange.x
+            }
+            motionChange != 0.0f
+        } ?: return null
 
         if (motionConsumed(change)) {
             return null
@@ -629,16 +720,14 @@
 }
 
 /**
- * Waits for drag motion along one axis when [pointerDirectionConfig] is
- * [HorizontalPointerDirectionConfig] or [VerticalPointerDirectionConfig], and drag motion along
- * any axis when using [BidirectionalPointerDirectionConfig]. It passes [pointerId] as the pointer
- * to examine. If [pointerId] is raised, another pointer from those that are down will be chosen to
+ * Waits for drag motion and uses [orientation] to detect the direction of  touch slop detection.
+ * It passes [pointerId] as the pointer to examine. If [pointerId] is raised, another pointer from
+ * those that are down will be chosen to
  * lead the gesture, and if none are down, `null` is returned. If [pointerId] is not down when
  * [awaitPointerSlopOrCancellation] is called, then `null` is returned.
  *
  * When pointer slop is detected, [onPointerSlopReached] is called with the change and the distance
- * beyond the pointer slop. [PointerDirectionConfig.calculateDeltaChange] should return the position
- * change in the direction of the drag axis. If [onPointerSlopReached] does not consume the
+ * beyond the pointer slop. If [onPointerSlopReached] does not consume the
  * position change, pointer slop will not have been considered detected and the detection will
  * continue or, if it is consumed, the [PointerInputChange] that was consumed will be returned.
  *
@@ -650,10 +739,10 @@
  * `null` if all pointers are raised or the position change was consumed by another gesture
  * detector.
  */
-internal suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
+private suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
     pointerId: PointerId,
     pointerType: PointerType,
-    pointerDirectionConfig: PointerDirectionConfig,
+    orientation: Orientation?,
     onPointerSlopReached: (PointerInputChange, Offset) -> Unit,
 ): PointerInputChange? {
     if (currentEvent.isPointerUp(pointerId)) {
@@ -661,8 +750,7 @@
     }
     val touchSlop = viewConfiguration.pointerSlop(pointerType)
     var pointer: PointerId = pointerId
-    var totalPositionChange = Offset.Zero
-
+    val touchSlopDetector = TouchSlopDetector(orientation)
     while (true) {
         val event = awaitPointerEvent()
         val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null
@@ -677,29 +765,8 @@
                 pointer = otherDown.id
             }
         } else {
-            val currentPosition = dragEvent.position
-            val previousPosition = dragEvent.previousPosition
-
-            val positionChange = currentPosition - previousPosition
-
-            totalPositionChange += positionChange
-
-            val inDirection = pointerDirectionConfig.calculateDeltaChange(
-                totalPositionChange
-            )
-
-            if (inDirection < touchSlop) {
-                // verify that nothing else consumed the drag event
-                awaitPointerEvent(PointerEventPass.Final)
-                if (dragEvent.isConsumed) {
-                    return null
-                }
-            } else {
-                val postSlopOffset = pointerDirectionConfig.calculatePostSlopOffset(
-                    totalPositionChange,
-                    touchSlop
-                )
-
+            val postSlopOffset = touchSlopDetector.addPointerInputChange(dragEvent, touchSlop)
+            if (postSlopOffset != null) {
                 onPointerSlopReached(
                     dragEvent,
                     postSlopOffset
@@ -707,7 +774,13 @@
                 if (dragEvent.isConsumed) {
                     return dragEvent
                 } else {
-                    totalPositionChange = Offset.Zero
+                    touchSlopDetector.reset()
+                }
+            } else {
+                // verify that nothing else consumed the drag event
+                awaitPointerEvent(PointerEventPass.Final)
+                if (dragEvent.isConsumed) {
+                    return null
                 }
             }
         }
@@ -715,70 +788,77 @@
 }
 
 /**
- * Configures the calculations to get the change amount depending on the dragging type.
- * [calculatePostSlopOffset] will return the post offset slop when the touchSlop is reached.
+ * Detects if touch slop has been crossed after adding a series of [PointerInputChange].
+ * For every new [PointerInputChange] one should add it to this detector using
+ * [addPointerInputChange]. If the position change causes the touch slop to be crossed,
+ * [addPointerInputChange] will return true.
  */
-internal interface PointerDirectionConfig {
-    fun calculateDeltaChange(offset: Offset): Float
-    fun calculatePostSlopOffset(
-        totalPositionChange: Offset,
-        touchSlop: Float
-    ): Offset
-}
+private class TouchSlopDetector(val orientation: Orientation? = null) {
 
-/**
- * Used for monitoring changes on X axis.
- */
-internal val HorizontalPointerDirectionConfig = object : PointerDirectionConfig {
-    override fun calculateDeltaChange(offset: Offset): Float = abs(offset.x)
+    fun Offset.mainAxis() = if (orientation == Orientation.Horizontal) x else y
+    fun Offset.crossAxis() = if (orientation == Orientation.Horizontal) y else x
 
-    override fun calculatePostSlopOffset(
-        totalPositionChange: Offset,
+    /**
+     * The accumulation of drag deltas in this detector.
+     */
+    private var totalPositionChange: Offset = Offset.Zero
+
+    /**
+     * Adds [dragEvent] to this detector. If the accumulated position changes crosses the touch
+     * slop provided by [touchSlop], this method will return the post slop offset, that is the
+     * total accumulated delta change minus the touch slop value, otherwise this should return null.
+     */
+    fun addPointerInputChange(
+        dragEvent: PointerInputChange,
         touchSlop: Float
-    ): Offset {
-        val finalMainPositionChange = totalPositionChange.x -
-            (sign(totalPositionChange.x) * touchSlop)
-        return Offset(finalMainPositionChange, totalPositionChange.y)
+    ): Offset? {
+        val currentPosition = dragEvent.position
+        val previousPosition = dragEvent.previousPosition
+        val positionChange = currentPosition - previousPosition
+        totalPositionChange += positionChange
+
+        val inDirection = if (orientation == null) {
+            totalPositionChange.getDistance()
+        } else {
+            totalPositionChange.mainAxis().absoluteValue
+        }
+
+        val hasCrossedSlop = inDirection >= touchSlop
+
+        return if (hasCrossedSlop) {
+            calculatePostSlopOffset(touchSlop)
+        } else {
+            null
+        }
+    }
+
+    /**
+     * Resets the accumulator associated with this detector.
+     */
+    fun reset() {
+        totalPositionChange = Offset.Zero
+    }
+
+    private fun calculatePostSlopOffset(touchSlop: Float): Offset {
+        return if (orientation == null) {
+            val touchSlopOffset =
+                totalPositionChange / totalPositionChange.getDistance() * touchSlop
+            // update postSlopOffset
+            totalPositionChange - touchSlopOffset
+        } else {
+            val finalMainAxisChange = totalPositionChange.mainAxis() -
+                (sign(totalPositionChange.mainAxis()) * touchSlop)
+            val finalCrossAxisChange = totalPositionChange.crossAxis()
+            if (orientation == Orientation.Horizontal) {
+                Offset(finalMainAxisChange, finalCrossAxisChange)
+            } else {
+                Offset(finalCrossAxisChange, finalMainAxisChange)
+            }
+        }
     }
 }
 
 /**
- * Used for monitoring changes on Y axis.
- */
-internal val VerticalPointerDirectionConfig = object : PointerDirectionConfig {
-    override fun calculateDeltaChange(offset: Offset): Float = abs(offset.y)
-
-    override fun calculatePostSlopOffset(
-        totalPositionChange: Offset,
-        touchSlop: Float
-    ): Offset {
-        val finalMainPositionChange = totalPositionChange.y -
-            (sign(totalPositionChange.y) * touchSlop)
-        return Offset(totalPositionChange.x, finalMainPositionChange)
-    }
-}
-
-/**
- * Used for monitoring changes on both X and Y axes.
- */
-internal val BidirectionalPointerDirectionConfig = object : PointerDirectionConfig {
-    override fun calculateDeltaChange(offset: Offset): Float = offset.getDistance()
-
-    override fun calculatePostSlopOffset(
-        totalPositionChange: Offset,
-        touchSlop: Float
-    ): Offset {
-        val touchSlopOffset =
-            totalPositionChange / calculateDeltaChange(totalPositionChange) * touchSlop
-        return totalPositionChange - touchSlopOffset
-    }
-}
-
-internal fun Orientation.toPointerDirectionConfig(): PointerDirectionConfig =
-    if (this == Orientation.Vertical) VerticalPointerDirectionConfig
-    else HorizontalPointerDirectionConfig
-
-/**
  * Waits for a long press by examining [pointerId].
  *
  * If that [pointerId] is raised (that is, the user lifts their finger), but another
@@ -839,7 +919,7 @@
                         // should technically never happen as we checked it above
                         finished = true
                     }
-                // Pointer (id) stayed down.
+                    // Pointer (id) stayed down.
                 } else {
                     longPress = event.changes.fastFirstOrNull { it.id == currentDown.id }
                 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index 5db29b1..92f3e6c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -31,16 +31,11 @@
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.AwaitPointerEventScope
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerId
 import androidx.compose.ui.input.pointer.PointerInputChange
 import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.positionChange
-import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
 import androidx.compose.ui.input.pointer.util.VelocityTracker
 import androidx.compose.ui.input.pointer.util.addPointerInputChange
 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
@@ -56,7 +51,6 @@
 import kotlin.math.sign
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.SendChannel
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
@@ -301,9 +295,10 @@
     private var onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
     private var reverseDirection: Boolean
 ) : DragGestureNode(
-    canDrag,
-    enabled,
-    interactionSource
+    canDrag = canDrag,
+    enabled = enabled,
+    interactionSource = interactionSource,
+    orientationLock = orientation
 ) {
 
     override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
@@ -314,8 +309,6 @@
         }
     }
 
-    override val pointerDirectionConfig = orientation.toPointerDirectionConfig()
-
     override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) =
         this@DraggableNode.onDragStarted(this, startedPosition)
 
@@ -357,6 +350,7 @@
             canDrag,
             enabled,
             interactionSource,
+            orientation,
             resetPointerInputHandling
         )
     }
@@ -372,6 +366,7 @@
     canDrag: (PointerInputChange) -> Boolean,
     enabled: Boolean,
     interactionSource: MutableInteractionSource?,
+    private var orientationLock: Orientation?
 ) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode {
 
     protected var canDrag = canDrag
@@ -397,13 +392,6 @@
     abstract suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit)
 
     /**
-     * Returns the pointerDirectionConfig which specifies the main and cross axis deltas. This is
-     * important when observing the delta change for Draggable, as we want to observe the change
-     * in the main axis only.
-     */
-    abstract val pointerDirectionConfig: PointerDirectionConfig
-
-    /**
      * Passes the action needed when a drag starts. This gives the ability to pass the desired
      * behavior from other nodes implementing AbstractDraggableNode
      */
@@ -478,62 +466,63 @@
             // re-create tracker when pointer input block restarts. This lazily creates the tracker
             // only when it is need.
             val velocityTracker = VelocityTracker()
+            val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
+                { startEvent, initialDelta ->
+                    if (canDrag.invoke(startEvent)) {
+                        if (!isListeningForEvents) {
+                            if (channel == null) {
+                                channel = Channel(capacity = Channel.UNLIMITED)
+                            }
+                            startListeningForEvents()
+                        }
+                        val overSlopOffset = initialDelta
+                        val xSign = sign(startEvent.position.x)
+                        val ySign = sign(startEvent.position.y)
+                        val adjustedStart = startEvent.position -
+                            Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
+
+                        channel?.trySend(DragStarted(adjustedStart))
+                    }
+                }
+
+            val onDragEnd: (change: PointerInputChange) -> Unit = { upEvent ->
+                velocityTracker.addPointerInputChange(upEvent)
+                val maximumVelocity = currentValueOf(LocalViewConfiguration)
+                    .maximumFlingVelocity
+                val velocity = velocityTracker.calculateVelocity(
+                    Velocity(maximumVelocity, maximumVelocity)
+                )
+                velocityTracker.resetTracking()
+                channel?.trySend(DragStopped(velocity))
+            }
+
+            val onDragCancel: () -> Unit = {
+                channel?.trySend(DragCancelled)
+            }
+
+            val shouldAwaitTouchSlop: () -> Boolean = {
+                !startDragImmediately()
+            }
+
+            val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit =
+                { change, delta ->
+                    velocityTracker.addPointerInputChange(change)
+                    channel?.trySend(DragDelta(delta))
+                }
+
             coroutineScope {
                 try {
-                    awaitPointerEventScope {
-                        while (isActive) {
-                            awaitDownAndSlop(
-                                _canDrag,
-                                ::startDragImmediately,
-                                velocityTracker,
-                                pointerDirectionConfig
-                            )?.let {
-                                /**
-                                 * The gesture crossed the touch slop, events are now relevant
-                                 * and should be propagated
-                                 */
-                                if (!isListeningForEvents) {
-                                    if (channel == null) {
-                                        channel = Channel(capacity = Channel.UNLIMITED)
-                                    }
-                                    startListeningForEvents()
-                                }
-                                var isDragSuccessful = false
-                                try {
-                                    isDragSuccessful = awaitDrag(
-                                        it.first,
-                                        it.second,
-                                        velocityTracker,
-                                        channel
-                                    ) { event ->
-                                        pointerDirectionConfig.calculateDeltaChange(
-                                            event.positionChangeIgnoreConsumed()
-                                        ) != 0f
-                                    }
-                                } catch (cancellation: CancellationException) {
-                                    isDragSuccessful = false
-                                    if (!isActive) throw cancellation
-                                } finally {
-                                    val maximumVelocity = currentValueOf(LocalViewConfiguration)
-                                        .maximumFlingVelocity
-                                    val event = if (isDragSuccessful) {
-                                        val velocity = velocityTracker.calculateVelocity(
-                                            Velocity(maximumVelocity, maximumVelocity)
-                                        )
-                                        velocityTracker.resetTracking()
-                                        DragStopped(velocity)
-                                    } else {
-                                        DragCancelled
-                                    }
-                                    channel?.trySend(event)
-                                }
-                            }
-                        }
-                    }
-                } catch (exception: CancellationException) {
-                    if (!isActive) {
-                        throw exception
-                    }
+                    detectDragGestures(
+                        orientationLock = orientationLock,
+                        onDragStart = onDragStart,
+                        onDragEnd = onDragEnd,
+                        onDragCancel = onDragCancel,
+                        shouldAwaitTouchSlop = shouldAwaitTouchSlop,
+                        onDrag = onDrag
+                    )
+                } catch (cancellation: CancellationException) {
+                    channel?.trySend(DragCancelled)
+                    if (!isActive) throw cancellation
                 }
             }
         }
@@ -580,9 +569,10 @@
         canDrag: (PointerInputChange) -> Boolean = this.canDrag,
         enabled: Boolean = this.enabled,
         interactionSource: MutableInteractionSource? = this.interactionSource,
-        isResetPointerInputHandling: Boolean = false
+        orientationLock: Orientation? = this.orientationLock,
+        shouldResetPointerInputHandling: Boolean = false
     ) {
-        var resetPointerInputHandling = isResetPointerInputHandling
+        var resetPointerInputHandling = shouldResetPointerInputHandling
 
         this.canDrag = canDrag
         if (this.enabled != enabled) {
@@ -599,91 +589,17 @@
             this.interactionSource = interactionSource
         }
 
+        if (this.orientationLock != orientationLock) {
+            this.orientationLock = orientationLock
+            resetPointerInputHandling = true
+        }
+
         if (resetPointerInputHandling) {
             pointerInputNode?.resetPointerInputHandler()
         }
     }
 }
 
-private suspend fun AwaitPointerEventScope.awaitDownAndSlop(
-    canDrag: (PointerInputChange) -> Boolean,
-    startDragImmediately: () -> Boolean,
-    velocityTracker: VelocityTracker,
-    pointerDirectionConfig: PointerDirectionConfig
-): Pair<PointerInputChange, Offset>? {
-    val initialDown =
-        awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
-    return if (!canDrag(initialDown)) {
-        null
-    } else if (startDragImmediately()) {
-        initialDown.consume()
-        velocityTracker.addPointerInputChange(initialDown)
-        // since we start immediately we don't wait for slop and the initial delta is 0
-        initialDown to Offset.Zero
-    } else {
-        val down = awaitFirstDown(requireUnconsumed = false)
-        velocityTracker.addPointerInputChange(down)
-        var initialDelta = Offset.Zero
-        val postPointerSlop = { event: PointerInputChange, offset: Offset ->
-            velocityTracker.addPointerInputChange(event)
-            event.consume()
-            initialDelta = offset
-        }
-
-        val afterSlopResult = awaitPointerSlopOrCancellation(
-            down.id,
-            down.type,
-            pointerDirectionConfig = pointerDirectionConfig,
-            onPointerSlopReached = postPointerSlop
-        )
-
-        if (afterSlopResult != null) afterSlopResult to initialDelta else null
-    }
-}
-
-private suspend fun AwaitPointerEventScope.awaitDrag(
-    startEvent: PointerInputChange,
-    initialDelta: Offset,
-    velocityTracker: VelocityTracker,
-    channel: SendChannel<DragEvent>?,
-    hasDragged: (PointerInputChange) -> Boolean,
-): Boolean {
-
-    val overSlopOffset = initialDelta
-    val xSign = sign(startEvent.position.x)
-    val ySign = sign(startEvent.position.y)
-    val adjustedStart = startEvent.position -
-        Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
-    channel?.trySend(DragStarted(adjustedStart))
-
-    channel?.trySend(DragDelta(initialDelta))
-
-    return onDragOrUp(hasDragged, startEvent.id) { event ->
-        // Velocity tracker takes all events, even UP
-        velocityTracker.addPointerInputChange(event)
-
-        // Dispatch only MOVE events
-        if (!event.changedToUpIgnoreConsumed()) {
-            val delta = event.positionChange()
-            event.consume()
-            channel?.trySend(DragDelta(delta))
-        }
-    }
-}
-
-private suspend fun AwaitPointerEventScope.onDragOrUp(
-    hasDragged: (PointerInputChange) -> Boolean,
-    pointerId: PointerId,
-    onDrag: (PointerInputChange) -> Unit
-): Boolean {
-    return drag(
-        pointerId = pointerId,
-        onDrag = onDrag,
-        hasDragged = hasDragged,
-        motionConsumed = { it.isConsumed }
-    )?.let(onDrag) != null
-}
-
 private class DefaultDraggableState(val onDelta: (Float) -> Unit) : DraggableState {
 
     private val dragScope: DragScope = object : DragScope {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
index f9c9571..0c4b4c4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
@@ -267,9 +267,10 @@
     private var onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
     private var reverseDirection: Boolean
 ) : DragGestureNode(
-    canDrag,
-    enabled,
-    interactionSource
+    canDrag = canDrag,
+    enabled = enabled,
+    interactionSource = interactionSource,
+    orientationLock = null
 ) {
 
     override suspend fun drag(
@@ -282,8 +283,6 @@
         }
     }
 
-    override val pointerDirectionConfig = BidirectionalPointerDirectionConfig
-
     override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) =
         this@Draggable2DNode.onDragStarted(this, startedPosition)
 
@@ -317,10 +316,11 @@
         this.startDragImmediately = startDragImmediately
 
         update(
-            canDrag,
-            enabled,
-            interactionSource,
-            resetPointerInputHandling
+            canDrag = canDrag,
+            enabled = enabled,
+            interactionSource = interactionSource,
+            orientationLock = null,
+            shouldResetPointerInputHandling = resetPointerInputHandling
         )
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index cf62744a..bcfaf97 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -273,7 +273,8 @@
 ) : DragGestureNode(
     canDrag = CanDragCalculation,
     enabled = enabled,
-    interactionSource = interactionSource
+    interactionSource = interactionSource,
+    orientationLock = orientation
 ), ObserverModifierNode, CompositionLocalConsumerModifierNode,
     FocusPropertiesModifierNode, KeyInputModifierNode {
 
@@ -333,9 +334,6 @@
         scrollingLogic.dispatchDragEvents(forEachDelta)
     }
 
-    override val pointerDirectionConfig: PointerDirectionConfig
-        get() = scrollingLogic.pointerDirectionConfig()
-
     override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
 
     override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
@@ -386,7 +384,13 @@
         this.flingBehavior = flingBehavior
 
         // update DragGestureNode
-        update(CanDragCalculation, enabled, interactionSource, resetPointerInputHandling)
+        update(
+            canDrag = CanDragCalculation,
+            enabled = enabled,
+            interactionSource = interactionSource,
+            orientationLock = if (scrollingLogic.isVertical()) Vertical else Horizontal,
+            shouldResetPointerInputHandling = resetPointerInputHandling
+        )
     }
 
     override fun onAttach() {
@@ -777,8 +781,6 @@
         return resetPointerInputHandling
     }
 
-    fun pointerDirectionConfig(): PointerDirectionConfig = orientation.toPointerDirectionConfig()
-
     fun isVertical(): Boolean = orientation == Vertical
 }