Allow reverse selection in the new EditingBuffer

This work adds handle crossing support to BTF2.

Test: EditingBufferTest
Test: :compose:foundation:foundation:cAT
Change-Id: Ib7af77f17cef9865d41ad49ada018f75694663f6
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionHandlesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionHandlesTest.kt
index 0444338..a839bad 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionHandlesTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionHandlesTest.kt
@@ -656,7 +656,7 @@
     }
 
     @Test
-    fun dragStartSelectionHandle_cannotExtendSelectionPastEndHandle() {
+    fun dragStartSelectionHandlePastEndHandle_reversesTheSelection() {
         state = TextFieldState("abc def ghj", initialSelectionInChars = TextRange(4, 7))
         rule.setContent {
             BasicTextField2(
@@ -672,12 +672,12 @@
 
         swipeToRight(Handle.SelectionStart, fontSizePx * 7)
         rule.runOnIdle {
-            assertThat(state.text.selectionInChars).isEqualTo(TextRange(7))
+            assertThat(state.text.selectionInChars).isEqualTo(TextRange(11, 7))
         }
     }
 
     @Test
-    fun dragEndSelectionHandle_cannotExtendSelectionPastStartHandle() {
+    fun dragEndSelectionHandlePastStartHandle_canReverseSelection() {
         state = TextFieldState("abc def ghj", initialSelectionInChars = TextRange(4, 7))
         rule.setContent {
             BasicTextField2(
@@ -693,7 +693,7 @@
 
         swipeToLeft(Handle.SelectionEnd, fontSizePx * 7)
         rule.runOnIdle {
-            assertThat(state.text.selectionInChars).isEqualTo(TextRange(4))
+            assertThat(state.text.selectionInChars).isEqualTo(TextRange(4, 0))
         }
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/EditProcessor.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/EditProcessor.kt
index 2862d17..27ffb8e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/EditProcessor.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/EditProcessor.kt
@@ -102,7 +102,7 @@
             )
             textChanged = true
         } else if (bufferState.selectionInChars != newValue.selectionInChars) {
-            mBuffer.setSelection(newValue.selectionInChars.min, newValue.selectionInChars.max)
+            mBuffer.setSelection(newValue.selectionInChars.start, newValue.selectionInChars.end)
             selectionChanged = true
         }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/EditingBuffer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/EditingBuffer.kt
index feeacb4..34a72d3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/EditingBuffer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/input/internal/EditingBuffer.kt
@@ -49,7 +49,7 @@
     /**
      * The inclusive selection start offset
      */
-    var selectionStart = selection.min
+    var selectionStart = selection.start
         private set(value) {
             require(value >= 0) { "Cannot set selectionStart to a negative value: $value" }
             field = value
@@ -58,7 +58,7 @@
     /**
      * The exclusive selection end offset
      */
-    var selectionEnd = selection.max
+    var selectionEnd = selection.end
         private set(value) {
             require(value >= 0) { "Cannot set selectionEnd to a negative value: $value" }
             field = value
@@ -135,23 +135,7 @@
     ) : this(AnnotatedString(text), selection)
 
     init {
-        val start = selection.min
-        val end = selection.max
-        if (start < 0 || start > text.length) {
-            throw IndexOutOfBoundsException(
-                "start ($start) offset is outside of text region ${text.length}"
-            )
-        }
-
-        if (end < 0 || end > text.length) {
-            throw IndexOutOfBoundsException(
-                "end ($end) offset is outside of text region ${text.length}"
-            )
-        }
-
-        if (start > end) {
-            throw IllegalArgumentException("Do not set reversed range: $start > $end")
-        }
+        checkRange(selection.start, selection.end)
     }
 
     fun replace(start: Int, end: Int, text: AnnotatedString) {
@@ -161,42 +145,30 @@
     /**
      * Replace the text and move the cursor to the end of inserted text.
      *
-     * This function cancels selection if there.
+     * This function cancels selection if there is any.
      *
      * @throws IndexOutOfBoundsException if start or end offset is outside of current buffer
      * @throws IllegalArgumentException if start is larger than end. (reversed range)
      */
     fun replace(start: Int, end: Int, text: String) {
+        checkRange(start, end)
+        val min = minOf(start, end)
+        val max = maxOf(start, end)
+
         changeTracker.trackChange(TextRange(start, end), text.length)
 
-        if (start < 0 || start > gapBuffer.length) {
-            throw IndexOutOfBoundsException(
-                "start ($start) offset is outside of text region ${gapBuffer.length}"
-            )
-        }
-
-        if (end < 0 || end > gapBuffer.length) {
-            throw IndexOutOfBoundsException(
-                "end ($end) offset is outside of text region ${gapBuffer.length}"
-            )
-        }
-
-        if (start > end) {
-            throw IllegalArgumentException("Do not set reversed range: $start > $end")
-        }
-
-        gapBuffer.replace(start, end, text)
+        gapBuffer.replace(min, max, text)
 
         // On Android, all text modification APIs also provides explicit cursor location. On the
-        // hand, desktop application usually doesn't. So, here tentatively move the cursor to the
-        // end offset of the editing area for desktop like application. In case of Android,
+        // other hand, desktop applications usually don't. So, here tentatively move the cursor to
+        // the end offset of the editing area for desktop like application. In case of Android,
         // implementation will call setSelection immediately after replace function to update this
         // tentative cursor location.
         selectionStart = start + text.length
         selectionEnd = start + text.length
 
-        // Similarly, if text modification happens, cancel ongoing composition. If caller want to
-        // change the composition text, it is caller responsibility to call setComposition again
+        // Similarly, if text modification happens, cancel ongoing composition. If caller wants to
+        // change the composition text, it is caller's responsibility to call setComposition again
         // to set composition range after replace function.
         compositionStart = NOWHERE
         compositionEnd = NOWHERE
@@ -209,17 +181,19 @@
      * Instead, preserve the selection with adjusting the deleted text.
      */
     fun delete(start: Int, end: Int) {
-        changeTracker.trackChange(TextRange(start, end), 0)
+        checkRange(start, end)
         val deleteRange = TextRange(start, end)
 
-        gapBuffer.replace(start, end, "")
+        changeTracker.trackChange(deleteRange, 0)
+
+        gapBuffer.replace(deleteRange.min, deleteRange.max, "")
 
         val newSelection = updateRangeAfterDelete(
             TextRange(selectionStart, selectionEnd),
             deleteRange
         )
-        selectionStart = newSelection.min
-        selectionEnd = newSelection.max
+        selectionStart = newSelection.start
+        selectionEnd = newSelection.end
 
         if (hasComposition()) {
             val compositionRange = TextRange(compositionStart, compositionEnd)
@@ -245,19 +219,7 @@
      * @throws IllegalArgumentException if start is larger than end. (reversed range)
      */
     fun setSelection(start: Int, end: Int) {
-        if (start < 0 || start > gapBuffer.length) {
-            throw IndexOutOfBoundsException(
-                "start ($start) offset is outside of text region ${gapBuffer.length}"
-            )
-        }
-        if (end < 0 || end > gapBuffer.length) {
-            throw IndexOutOfBoundsException(
-                "end ($end) offset is outside of text region ${gapBuffer.length}"
-            )
-        }
-        if (start > end) {
-            throw IllegalArgumentException("Do not set reversed range: $start > $end")
-        }
+        checkRange(start, end)
 
         selectionStart = start
         selectionEnd = end
@@ -315,6 +277,24 @@
     override fun toString(): String = gapBuffer.toString()
 
     fun toAnnotatedString(): AnnotatedString = AnnotatedString(toString())
+
+    private fun checkRange(start: Int, end: Int) {
+        if (start < 0 || start > gapBuffer.length) {
+            throw IndexOutOfBoundsException(
+                "start ($start) offset is outside of text region ${gapBuffer.length}"
+            )
+        }
+
+        if (end < 0 || end > gapBuffer.length) {
+            throw IndexOutOfBoundsException(
+                "end ($end) offset is outside of text region ${gapBuffer.length}"
+            )
+        }
+
+        if (start > end) {
+            println("Setting reversed range: $start > $end")
+        }
+    }
 }
 
 /**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionState.kt
index f6111aa..f94e21f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text2/selection/TextFieldSelectionState.kt
@@ -319,15 +319,10 @@
                         previousHandleOffset = previousDragOffset,
                         adjustment = SelectionAdjustment.CharacterWithWordAccelerate,
                     )
-                    // selection drag should never reverse the selection. It can only collapse the
-                    // selection as the handles are about to pass each other.
-                    if (newSelection.reversed != prevSelection.reversed) {
-                        // the common offset should be the one that is not being dragged
-                        val offset = if (isStartHandle) prevSelection.end else prevSelection.start
-                        newSelection = TextRange(offset)
-                    }
-                    editWithFilter {
-                        selectCharsIn(newSelection)
+                    if (prevSelection.collapsed || !newSelection.collapsed) {
+                        editWithFilter {
+                            selectCharsIn(newSelection)
+                        }
                     }
                     previousDragOffset = if (isStartHandle) startOffset else endOffset
                 }
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/EditProcessorTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/EditProcessorTest.kt
index a8d8db8..7a5eba6 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/EditProcessorTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/EditProcessorTest.kt
@@ -165,8 +165,8 @@
 
         val initialBuffer = processor.mBuffer
 
-        assertThat(initialSelection.min).isEqualTo(initialBuffer.selectionStart)
-        assertThat(initialSelection.max).isEqualTo(initialBuffer.selectionEnd)
+        assertThat(initialSelection.start).isEqualTo(initialBuffer.selectionStart)
+        assertThat(initialSelection.end).isEqualTo(initialBuffer.selectionEnd)
 
         val updatedSelection = TextRange(3, 0)
         val newTextFieldValue = TextFieldCharSequence(textFieldValue, selection = updatedSelection)
@@ -174,8 +174,8 @@
         processor.reset(newTextFieldValue)
 
         assertThat(processor.mBuffer).isSameInstanceAs(initialBuffer)
-        assertThat(updatedSelection.min).isEqualTo(initialBuffer.selectionStart)
-        assertThat(updatedSelection.max).isEqualTo(initialBuffer.selectionEnd)
+        assertThat(updatedSelection.start).isEqualTo(initialBuffer.selectionStart)
+        assertThat(updatedSelection.end).isEqualTo(initialBuffer.selectionEnd)
         assertThat(resetCalled).isEqualTo(1)
     }
 
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferTest.kt
index 905d35f3..2a17df51 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text2/input/internal/EditingBufferTest.kt
@@ -154,7 +154,15 @@
     }
 
     @Test
-    fun setCompostion_and_cancelComposition() {
+    fun setSelection_allowReversedSelection() {
+        val eb = EditingBuffer("ABCDE", TextRange.Zero)
+        eb.setSelection(4, 2)
+
+        assertThat(eb.selection).isEqualTo(TextRange(4, 2))
+    }
+
+    @Test
+    fun setComposition_and_cancelComposition() {
         val eb = EditingBuffer("ABCDE", TextRange.Zero)
 
         eb.setComposition(0, 5) // Make all text as composition
@@ -195,7 +203,7 @@
     }
 
     @Test
-    fun setCompostion_and_commitComposition() {
+    fun setComposition_and_commitComposition() {
         val eb = EditingBuffer("ABCDE", TextRange.Zero)
 
         eb.setComposition(0, 5) // Make all text as composition
@@ -334,6 +342,18 @@
     }
 
     @Test
+    fun delete_covered_reversedSelection() {
+        // A[BC]DE
+        val eb = EditingBuffer("ABCDE", TextRange(3, 1))
+
+        eb.delete(0, 4)
+        // []E
+        assertThat(eb).hasChars("E")
+        assertThat(eb.selectionStart).isEqualTo(0)
+        assertThat(eb.selectionEnd).isEqualTo(0)
+    }
+
+    @Test
     fun delete_intersects_first_half_of_selection() {
         // AB[CD]E
         val eb = EditingBuffer("ABCDE", TextRange(2, 4))
@@ -346,6 +366,18 @@
     }
 
     @Test
+    fun delete_intersects_first_half_of_reversedSelection() {
+        // AB[CD]E
+        val eb = EditingBuffer("ABCDE", TextRange(4, 2))
+
+        eb.delete(3, 1)
+        // A[D]E
+        assertThat(eb).hasChars("ADE")
+        assertThat(eb.selectionStart).isEqualTo(1)
+        assertThat(eb.selectionEnd).isEqualTo(2)
+    }
+
+    @Test
     fun delete_intersects_second_half_of_selection() {
         // A[BCD]EFG
         val eb = EditingBuffer("ABCDEFG", TextRange(1, 4))
@@ -358,6 +390,18 @@
     }
 
     @Test
+    fun delete_intersects_second_half_of_reversedSelection() {
+        // A[BCD]EFG
+        val eb = EditingBuffer("ABCDEFG", TextRange(4, 1))
+
+        eb.delete(5, 3)
+        // A[BC]FG
+        assertThat(eb).hasChars("ABCFG")
+        assertThat(eb.selectionStart).isEqualTo(1)
+        assertThat(eb.selectionEnd).isEqualTo(3)
+    }
+
+    @Test
     fun delete_preceding_composition_no_intersection() {
         val eb = EditingBuffer("ABCDE", TextRange.Zero)