Merge "Reorganize selection toolbar" into androidx-master-dev
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
index 036f2c4..c338a45 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegateTest.kt
@@ -25,6 +25,7 @@
 import androidx.compose.ui.selection.Selection
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextDelegate
@@ -60,7 +61,10 @@
     style = FontStyle.Normal
 )
 
-@OptIn(InternalTextApi::class)
+@OptIn(
+    InternalTextApi::class,
+    ExperimentalTextApi::class
+)
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class MultiWidgetSelectionDelegateTest {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
index a39fc33..8638678 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
@@ -52,6 +52,7 @@
 import androidx.compose.ui.semantics.getTextLayoutResult
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.Placeholder
 import androidx.compose.ui.text.TextDelegate
@@ -96,6 +97,7 @@
  */
 @Composable
 @InternalTextApi
+@OptIn(ExperimentalTextApi::class)
 fun CoreText(
     text: AnnotatedString,
     modifier: Modifier = Modifier,
@@ -194,7 +196,10 @@
     }
 }
 
-@OptIn(InternalTextApi::class)
+@OptIn(
+    InternalTextApi::class,
+    ExperimentalTextApi::class
+)
 private class TextController(val state: TextState) {
     var selectionRegistrar: SelectionRegistrar? = null
 
@@ -225,7 +230,7 @@
             if (state.selectionRange != null) {
                 val newGlobalPosition = it.globalPosition
                 if (newGlobalPosition != state.previousGlobalPosition) {
-                    selectionRegistrar.onPositionChange()
+                    selectionRegistrar.notifyPositionChange()
                 }
                 state.previousGlobalPosition = newGlobalPosition
             }
@@ -353,7 +358,7 @@
     var layoutCoordinates: LayoutCoordinates? = null
     /** The latest TextLayoutResult calculated in the measure block */
     var layoutResult: TextLayoutResult? = null
-    /** The global position calculated during the last onPositioned callback */
+    /** The global position calculated during the last notifyPosition callback */
     var previousGlobalPosition: Offset = Offset.Zero
     /** The paint used to draw highlight background for selected text. */
     val selectionPaint: Paint = Paint()
@@ -433,7 +438,10 @@
     return Pair(placeholders, inlineComposables)
 }
 
-@OptIn(InternalTextApi::class)
+@OptIn(
+    InternalTextApi::class,
+    ExperimentalTextApi::class
+)
 @VisibleForTesting
 internal fun longPressDragObserver(
     state: TextState,
@@ -455,10 +463,9 @@
             state.layoutCoordinates?.let {
                 if (!it.isAttached) return
 
-                selectionRegistrar?.onUpdateSelection(
+                selectionRegistrar?.notifySelectionUpdateStart(
                     layoutCoordinates = it,
-                    startPosition = pxPosition,
-                    endPosition = pxPosition
+                    startPosition = pxPosition
                 )
 
                 dragBeginPosition = pxPosition
@@ -466,7 +473,6 @@
         }
 
         override fun onDragStart() {
-            super.onDragStart()
             // selection never started
             if (state.selectionRange == null) return
             // Zero out the total distance that being dragged.
@@ -481,7 +487,7 @@
 
                 dragTotalDistance += dragDistance
 
-                selectionRegistrar?.onUpdateSelection(
+                selectionRegistrar?.notifySelectionUpdate(
                     layoutCoordinates = it,
                     startPosition = dragBeginPosition,
                     endPosition = dragBeginPosition + dragTotalDistance
@@ -489,5 +495,13 @@
             }
             return dragDistance
         }
+
+        override fun onStop(velocity: Offset) {
+            selectionRegistrar?.notifySelectionUpdateEnd()
+        }
+
+        override fun onCancel() {
+            selectionRegistrar?.notifySelectionUpdateEnd()
+        }
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
index eaa5cc2..f908eb0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
@@ -22,10 +22,12 @@
 import androidx.compose.ui.selection.Selectable
 import androidx.compose.ui.selection.Selection
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextRange
 import kotlin.math.max
 
+@OptIn(ExperimentalTextApi::class)
 internal class MultiWidgetSelectionDelegate(
     private val selectionRangeUpdate: (TextRange?) -> Unit,
     private val coordinatesCallback: () -> LayoutCoordinates?,
@@ -123,6 +125,7 @@
  *
  * @return [Selection] of the current composable, or null if the composable is not selected.
  */
+@OptIn(ExperimentalTextApi::class)
 internal fun getTextSelectionInfo(
     textLayoutResult: TextLayoutResult,
     selectionCoordinates: Pair<Offset, Offset>,
@@ -206,6 +209,7 @@
  *
  * @return [Selection] of the current composable, or null if the composable is not selected.
  */
+@OptIn(ExperimentalTextApi::class)
 private fun getRefinedSelectionInfo(
     rawStartOffset: Int,
     rawEndOffset: Int,
@@ -282,6 +286,7 @@
  *
  * @return an assembled object of [Selection] using the offered selection info.
  */
+@OptIn(ExperimentalTextApi::class)
 private fun getAssembledSelectionInfo(
     startOffset: Int,
     endOffset: Int,
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
index 9ea31ce..cd6e838 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
@@ -22,6 +22,7 @@
 import androidx.compose.ui.selection.Selectable
 import androidx.compose.ui.selection.Selection
 import androidx.compose.ui.selection.SelectionRegistrar
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.TextDelegate
 import androidx.compose.ui.text.style.ResolvedTextDirection
@@ -36,7 +37,10 @@
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-@OptIn(InternalTextApi::class)
+@OptIn(
+    InternalTextApi::class,
+    ExperimentalTextApi::class
+)
 class TextSelectionLongPressDragTest {
     private val selectionRegistrar = mock<SelectionRegistrar>()
     private val selectable = mock<Selectable>()
@@ -93,15 +97,14 @@
     }
 
     @Test
-    fun longPressDragObserver_onLongPress_calls_getSelection_change_selection() {
+    fun longPressDragObserver_onLongPress_calls_notifySelectionInitiated() {
         val position = Offset(100f, 100f)
 
         gesture.onLongPress(position)
 
-        verify(selectionRegistrar, times(1)).onUpdateSelection(
+        verify(selectionRegistrar, times(1)).notifySelectionUpdateStart(
             layoutCoordinates = layoutCoordinates,
-            startPosition = position,
-            endPosition = position
+            startPosition = position
         )
     }
 
@@ -127,7 +130,7 @@
 
         // Verify.
         verify(selectionRegistrar, times(1))
-            .onUpdateSelection(
+            .notifySelectionUpdate(
                 layoutCoordinates = layoutCoordinates,
                 startPosition = beginPosition2,
                 endPosition = beginPosition2 + dragDistance2
@@ -135,7 +138,7 @@
     }
 
     @Test
-    fun longPressDragObserver_onDrag_calls_getSelection_change_selection() {
+    fun longPressDragObserver_onDrag_calls_notifySelectionDrag() {
         val dragDistance = Offset(15f, 10f)
         val beginPosition = Offset(30f, 20f)
         gesture.onLongPress(beginPosition)
@@ -146,10 +149,35 @@
 
         assertThat(result).isEqualTo(dragDistance)
         verify(selectionRegistrar, times(1))
-            .onUpdateSelection(
+            .notifySelectionUpdate(
                 layoutCoordinates = layoutCoordinates,
                 startPosition = beginPosition,
                 endPosition = beginPosition + dragDistance
             )
     }
+
+    @Test
+    fun longPressDragObserver_onStop_calls_notifySelectionEnd() {
+        val dragDistance = Offset(15f, 10f)
+        val beginPosition = Offset(30f, 20f)
+        gesture.onLongPress(beginPosition)
+        state.selectionRange = fakeInitialSelection.toTextRange()
+        gesture.onDragStart()
+        gesture.onStop(dragDistance)
+
+        verify(selectionRegistrar, times(1))
+            .notifySelectionUpdateEnd()
+    }
+
+    @Test
+    fun longPressDragObserver_onCancel_calls_notifySelectionEnd() {
+        val beginPosition = Offset(30f, 20f)
+        gesture.onLongPress(beginPosition)
+        state.selectionRange = fakeInitialSelection.toTextRange()
+        gesture.onDragStart()
+        gesture.onCancel()
+
+        verify(selectionRegistrar, times(1))
+            .notifySelectionUpdateEnd()
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 2b05879..ccac4f1 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2426,7 +2426,7 @@
 
 package androidx.compose.ui.selection {
 
-  public interface Selectable {
+  @androidx.compose.ui.text.ExperimentalTextApi public interface Selectable {
     method public androidx.compose.ui.geometry.Rect getBoundingBox(int offset);
     method public long getHandlePosition-F1C5BW0(androidx.compose.ui.selection.Selection selection, boolean isStartHandle);
     method public androidx.compose.ui.layout.LayoutCoordinates? getLayoutCoordinates();
@@ -2476,9 +2476,11 @@
   public final class SelectionManagerKt {
   }
 
-  public interface SelectionRegistrar {
-    method public void onPositionChange();
-    method public void onUpdateSelection-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+  @androidx.compose.ui.text.ExperimentalTextApi public interface SelectionRegistrar {
+    method public void notifyPositionChange();
+    method public void notifySelectionUpdate-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+    method public void notifySelectionUpdateEnd();
+    method public void notifySelectionUpdateStart-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition);
     method public androidx.compose.ui.selection.Selectable subscribe(androidx.compose.ui.selection.Selectable selectable);
     method public void unsubscribe(androidx.compose.ui.selection.Selectable selectable);
   }
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 2b05879..ccac4f1 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2426,7 +2426,7 @@
 
 package androidx.compose.ui.selection {
 
-  public interface Selectable {
+  @androidx.compose.ui.text.ExperimentalTextApi public interface Selectable {
     method public androidx.compose.ui.geometry.Rect getBoundingBox(int offset);
     method public long getHandlePosition-F1C5BW0(androidx.compose.ui.selection.Selection selection, boolean isStartHandle);
     method public androidx.compose.ui.layout.LayoutCoordinates? getLayoutCoordinates();
@@ -2476,9 +2476,11 @@
   public final class SelectionManagerKt {
   }
 
-  public interface SelectionRegistrar {
-    method public void onPositionChange();
-    method public void onUpdateSelection-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+  @androidx.compose.ui.text.ExperimentalTextApi public interface SelectionRegistrar {
+    method public void notifyPositionChange();
+    method public void notifySelectionUpdate-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+    method public void notifySelectionUpdateEnd();
+    method public void notifySelectionUpdateStart-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition);
     method public androidx.compose.ui.selection.Selectable subscribe(androidx.compose.ui.selection.Selectable selectable);
     method public void unsubscribe(androidx.compose.ui.selection.Selectable selectable);
   }
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 9061804..74fba3b 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2488,7 +2488,7 @@
 
 package androidx.compose.ui.selection {
 
-  public interface Selectable {
+  @androidx.compose.ui.text.ExperimentalTextApi public interface Selectable {
     method public androidx.compose.ui.geometry.Rect getBoundingBox(int offset);
     method public long getHandlePosition-F1C5BW0(androidx.compose.ui.selection.Selection selection, boolean isStartHandle);
     method public androidx.compose.ui.layout.LayoutCoordinates? getLayoutCoordinates();
@@ -2538,9 +2538,11 @@
   public final class SelectionManagerKt {
   }
 
-  public interface SelectionRegistrar {
-    method public void onPositionChange();
-    method public void onUpdateSelection-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+  @androidx.compose.ui.text.ExperimentalTextApi public interface SelectionRegistrar {
+    method public void notifyPositionChange();
+    method public void notifySelectionUpdate-rULFVbc(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition, long endPosition);
+    method public void notifySelectionUpdateEnd();
+    method public void notifySelectionUpdateStart-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates, long startPosition);
     method public androidx.compose.ui.selection.Selectable subscribe(androidx.compose.ui.selection.Selectable selectable);
     method public void unsubscribe(androidx.compose.ui.selection.Selectable selectable);
   }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selectable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selectable.kt
index 26d168e..b98c168 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selectable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selectable.kt
@@ -20,11 +20,13 @@
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 
 /**
  * Provides [Selection] information for a composable to SelectionContainer. Composables who can
  * be selected should subscribe to [SelectionRegistrar] using this interface.
  */
+@ExperimentalTextApi
 interface Selectable {
     /**
      * Returns [Selection] information for a selectable composable. If no selection can be provided
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selection.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selection.kt
index 714ac17..bb80ac9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selection.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/Selection.kt
@@ -17,6 +17,7 @@
 package androidx.compose.ui.selection
 
 import androidx.compose.runtime.Immutable
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.style.ResolvedTextDirection
 
@@ -24,6 +25,7 @@
  * Information about the current Selection.
  */
 @Immutable
+@OptIn(ExperimentalTextApi::class)
 data class Selection(
     /**
      * Information about the start of the selection.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionContainer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionContainer.kt
index 3e4b606..be22667 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionContainer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionContainer.kt
@@ -135,17 +135,12 @@
                         handle = null
                     )
                 }
-                SelectionFloatingToolBar(manager = manager)
             }
         }
     }
 
     onDispose {
         manager.selection = null
+        manager.hideSelectionToolbar()
     }
 }
-
-@Composable
-private fun SelectionFloatingToolBar(manager: SelectionManager) {
-    manager.showSelectionToolbar()
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt
index b6cbf8e..ce33357 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.platform.TextToolbarStatus
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.length
 import androidx.compose.ui.text.subSequence
@@ -40,7 +41,10 @@
 /**
  * A bridge class between user interaction to the text composables for text selection.
  */
-@OptIn(InternalTextApi::class)
+@OptIn(
+    InternalTextApi::class,
+    ExperimentalTextApi::class
+)
 internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
     /**
      * The current selection.
@@ -49,7 +53,6 @@
         set(value) {
             field = value
             updateHandleOffsets()
-            hideSelectionToolbar()
         }
 
     /**
@@ -123,9 +126,20 @@
     init {
         selectionRegistrar.onPositionChangeCallback = {
             updateHandleOffsets()
+            updateSelectionToolbarPosition()
+        }
+
+        selectionRegistrar.onSelectionUpdateStartCallback = { layoutCoordinates, startPosition ->
+            updateSelection(
+                startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
+                endPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
+                isStartHandle = true,
+                longPress = true
+            )
             hideSelectionToolbar()
         }
-        selectionRegistrar.onUpdateSelectionCallback =
+
+        selectionRegistrar.onSelectionUpdateCallback =
             { layoutCoordinates, startPosition, endPosition ->
                 updateSelection(
                     startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
@@ -134,6 +148,10 @@
                     longPress = true
                 )
             }
+
+        selectionRegistrar.onSelectionUpdateEndCallback = {
+            showSelectionToolbar()
+        }
     }
 
     private fun updateHandleOffsets() {
@@ -265,12 +283,9 @@
         }
     }
 
-    private fun hideSelectionToolbar() {
+    internal fun hideSelectionToolbar() {
         if (textToolbar?.status == TextToolbarStatus.Shown) {
-            val selection = selection
-            if (selection == null) {
-                textToolbar?.hide()
-            }
+            textToolbar?.hide()
         }
     }
 
@@ -356,12 +371,14 @@
             endPosition = Offset(-1f, -1f),
             previousSelection = selection
         )
+        hideSelectionToolbar()
         if (selection != null) onSelectionChange(null)
     }
 
     fun handleDragObserver(isStartHandle: Boolean): DragObserver {
         return object : DragObserver {
             override fun onStart(downPosition: Offset) {
+                hideSelectionToolbar()
                 val selection = selection!!
                 // The LayoutCoordinates of the composable where the drag gesture should begin. This
                 // is used to convert the position of the beginning of the drag gesture from the
@@ -375,13 +392,15 @@
                 // The position of the character where the drag gesture should begin. This is in
                 // the composable coordinates.
                 val beginCoordinates = getAdjustedCoordinates(
-                    if (isStartHandle)
+                    if (isStartHandle) {
                         selection.start.selectable.getHandlePosition(
                             selection = selection, isStartHandle = true
-                        ) else
+                        )
+                    } else {
                         selection.end.selectable.getHandlePosition(
                             selection = selection, isStartHandle = false
                         )
+                    }
                 )
 
                 // Convert the position where drag gesture begins from composable coordinates to
@@ -434,6 +453,14 @@
                 )
                 return dragDistance
             }
+
+            override fun onStop(velocity: Offset) {
+                showSelectionToolbar()
+            }
+
+            override fun onCancel() {
+                showSelectionToolbar()
+            }
         }
     }
 
@@ -468,6 +495,7 @@
     return lhs?.merge(rhs) ?: rhs
 }
 
+@OptIn(ExperimentalTextApi::class)
 internal fun getCurrentSelectedText(
     selectable: Selectable,
     selection: Selection
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrar.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrar.kt
index 3dbff76..7e1cded 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrar.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrar.kt
@@ -19,10 +19,12 @@
 import androidx.compose.runtime.ambientOf
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.ExperimentalTextApi
 
 /**
  *  An interface allowing a composable to subscribe and unsubscribe to selection changes.
  */
+@ExperimentalTextApi
 interface SelectionRegistrar {
     /**
      * Subscribe to SelectionContainer selection changes.
@@ -38,20 +40,58 @@
      * When the Global Position of a subscribed [Selectable] changes, this method
      * is called.
      */
-    fun onPositionChange()
+    fun notifyPositionChange()
 
     /**
-     * When selection changes, this method is called.
+     * Call this method to notify the [SelectionContainer] that the selection has been initiated.
+     * Depends on the input, [notifySelectionUpdate] may be called repeatedly after
+     * [notifySelectionUpdateStart] is called. And [notifySelectionUpdateEnd] should always be
+     * called after selection finished.
+     * For example:
+     *  1. User long pressed the text and then release. [notifySelectionUpdateStart] should be
+     *  called followed by [notifySelectionUpdateEnd] being called once.
+     *  2. User long pressed the text and then drag a distance and then release.
+     *  [notifySelectionUpdateStart] should be called first after the user long press, and then
+     *  [notifySelectionUpdate] is called several times reporting the updates, in the end
+     *  [notifySelectionUpdateEnd] is called to finish the selection.
+     *
+     * @param layoutCoordinates [LayoutCoordinates] of the [Selectable].
+     * @param startPosition coordinates of where the selection is initiated.
+     *
+     * @see notifySelectionUpdate
+     * @see notifySelectionUpdateEnd
+     */
+    fun notifySelectionUpdateStart(
+        layoutCoordinates: LayoutCoordinates,
+        startPosition: Offset
+    )
+
+    /**
+     * Call this method to notify the [SelectionContainer] that  the selection has been updated.
+     * The caller of this method should make sure that [notifySelectionUpdateStart] is always
+     * called once before calling this function. And [notifySelectionUpdateEnd] is always called
+     * once after the all updates finished.
      *
      * @param layoutCoordinates [LayoutCoordinates] of the [Selectable].
      * @param startPosition coordinates of where the selection starts.
      * @param endPosition coordinates of where the selection ends.
+     *
+     * @see notifySelectionUpdateStart
+     * @see notifySelectionUpdateEnd
      */
-    fun onUpdateSelection(
+    fun notifySelectionUpdate(
         layoutCoordinates: LayoutCoordinates,
         startPosition: Offset,
-        endPosition: Offset
+        endPosition: Offset,
     )
+
+    /**
+     * Call this method to notify the [SelectionContainer] that the selection update has stopped.
+     *
+     * @see notifySelectionUpdateStart
+     * @see notifySelectionUpdate
+     */
+    fun notifySelectionUpdateEnd()
 }
 
 /**
@@ -72,4 +112,5 @@
  * Ambient of SelectionRegistrar. Composables that implement selection logic can use this ambient
  * to get a [SelectionRegistrar] in order to subscribe and unsubscribe to [SelectionRegistrar].
  */
+@OptIn(ExperimentalTextApi::class)
 val AmbientSelectionRegistrar = ambientOf<SelectionRegistrar?>()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt
index c42a8d1..dd9250d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt
@@ -18,7 +18,9 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.ExperimentalTextApi
 
+@OptIn(ExperimentalTextApi::class)
 internal class SelectionRegistrarImpl : SelectionRegistrar {
     /**
      * A flag to check if the [Selectable]s have already been sorted.
@@ -42,6 +44,21 @@
      */
     internal var onPositionChangeCallback: (() -> Unit)? = null
 
+    /**
+     * The callback to be invoked when the selection is initiated.
+     */
+    internal var onSelectionUpdateStartCallback: ((LayoutCoordinates, Offset) -> Unit)? = null
+
+    /**
+     * The callback to be invoked when the selection is updated.
+     */
+    internal var onSelectionUpdateCallback: ((LayoutCoordinates, Offset, Offset) -> Unit)? = null
+
+    /**
+     * The callback to be invoked when selection update finished.
+     */
+    internal var onSelectionUpdateEndCallback: (() -> Unit)? = null
+
     override fun subscribe(selectable: Selectable): Selectable {
         _selectables.add(selectable)
         sorted = false
@@ -87,27 +104,29 @@
         return selectables
     }
 
-    override fun onPositionChange() {
+    override fun notifyPositionChange() {
         // Set the variable sorted to be false, when the global position of a registered
         // selectable changes.
         sorted = false
         onPositionChangeCallback?.invoke()
     }
 
-    /**
-     * The callback to be invoked when the selection change was triggered.
-     */
-    internal var onUpdateSelectionCallback: ((LayoutCoordinates, Offset, Offset) -> Unit)? = null
+    override fun notifySelectionUpdateStart(
+        layoutCoordinates: LayoutCoordinates,
+        startPosition: Offset
+    ) {
+        onSelectionUpdateStartCallback?.invoke(layoutCoordinates, startPosition)
+    }
 
-    override fun onUpdateSelection(
+    override fun notifySelectionUpdate(
         layoutCoordinates: LayoutCoordinates,
         startPosition: Offset,
         endPosition: Offset
     ) {
-        onUpdateSelectionCallback?.invoke(
-            layoutCoordinates,
-            startPosition,
-            endPosition
-        )
+        onSelectionUpdateCallback?.invoke(layoutCoordinates, startPosition, endPosition)
+    }
+
+    override fun notifySelectionUpdateEnd() {
+        onSelectionUpdateEndCallback?.invoke()
     }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionManager.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionManager.kt
index 8cf64c5..ea143f0 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionManager.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionManager.kt
@@ -25,7 +25,9 @@
 import androidx.compose.ui.selection.getCurrentSelectedText
 import androidx.compose.ui.selection.merge
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 
+@OptIn(ExperimentalTextApi::class)
 internal class DesktopSelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
     private var dragBeginPosition = Offset.Zero
 
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt
index ce9ba4b4..71eae61 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt
@@ -20,8 +20,10 @@
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.selection.Selectable
 import androidx.compose.ui.selection.SelectionRegistrar
+import androidx.compose.ui.text.ExperimentalTextApi
 
 // based on androidx.compose.ui.selection.SelectionRegistrarImpl
+@OptIn(ExperimentalTextApi::class)
 internal class DesktopSelectionRegistrar : SelectionRegistrar {
     internal var sorted: Boolean = false
 
@@ -71,12 +73,23 @@
         return selectables
     }
 
-    override fun onPositionChange() {
+    override fun notifyPositionChange() {
         sorted = false
         onPositionChangeCallback?.invoke()
     }
 
-    override fun onUpdateSelection(
+    override fun notifySelectionUpdateStart(
+        layoutCoordinates: LayoutCoordinates,
+        startPosition: Offset
+    ) {
+        onUpdateSelectionCallback?.invoke(
+            layoutCoordinates,
+            startPosition,
+            startPosition
+        )
+    }
+
+    override fun notifySelectionUpdate(
         layoutCoordinates: LayoutCoordinates,
         startPosition: Offset,
         endPosition: Offset
@@ -87,4 +100,6 @@
             endPosition
         )
     }
+
+    override fun notifySelectionUpdateEnd() { /* do nothing */ }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockSelectable.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockSelectable.kt
index ede87ef..baa520a 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockSelectable.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockSelectable.kt
@@ -20,7 +20,9 @@
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 
+@OptIn(ExperimentalTextApi::class)
 class MockSelectable(
     var getSelectionValue: Selection? = null,
     var getHandlePositionValue: Offset = Offset.Zero,
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt
index 323adf5..b066747 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.style.ResolvedTextDirection
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.any
@@ -32,6 +33,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
+@OptIn(ExperimentalTextApi::class)
 @RunWith(JUnit4::class)
 class SelectionManagerDragTest {
     private val selectionRegistrar = SelectionRegistrarImpl()
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt
index ff70baa..18ce38e 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt
@@ -24,6 +24,7 @@
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.length
 import androidx.compose.ui.text.style.ResolvedTextDirection
 import androidx.compose.ui.text.subSequence
@@ -42,6 +43,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
+@OptIn(ExperimentalTextApi::class)
 @RunWith(JUnit4::class)
 class SelectionManagerTest {
     private val selectionRegistrar = spy(SelectionRegistrarImpl())
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt
index 130db56..4bf9891a 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.ExperimentalTextApi
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.whenever
@@ -25,6 +26,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
+@OptIn(ExperimentalTextApi::class)
 @RunWith(JUnit4::class)
 class SelectionRegistrarImplTest {
     @Test
@@ -188,7 +190,7 @@
         assertThat(selectionRegistrar.sorted).isTrue()
 
         // Act.
-        selectionRegistrar.onPositionChange()
+        selectionRegistrar.notifyPositionChange()
 
         // Assert.
         assertThat(selectionRegistrar.sorted).isFalse()
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionTest.kt
index 5febaab..6afcab9 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.selection
 
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.style.ResolvedTextDirection
 import com.google.common.truth.Truth.assertThat
@@ -24,6 +25,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
+@OptIn(ExperimentalTextApi::class)
 @RunWith(JUnit4::class)
 class SelectionTest {
     @Test