[go: nahoru, domu]

blob: 2e1e17d112d16cb5ee49789d8dfc0c50ab46a291 [file] [log] [blame]
Qingqing Deng6f56a912019-05-13 10:10:37 -07001/*
haoyuac341f02021-01-22 22:01:56 -08002 * Copyright 2021 The Android Open Source Project
Qingqing Deng6f56a912019-05-13 10:10:37 -07003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
haoyuac341f02021-01-22 22:01:56 -080017package androidx.compose.foundation.text.selection
Qingqing Deng6f56a912019-05-13 10:10:37 -070018
haoyuc40d02752021-01-25 17:32:47 -080019import androidx.compose.foundation.focusable
George Mount32de9dd2022-10-05 14:51:06 -070020import androidx.compose.foundation.gestures.awaitEachGesture
Ralston Da Silvade62bc62021-06-02 17:46:44 -070021import androidx.compose.foundation.gestures.waitForUpOrCancellation
Zach Klippensteinadabe342021-11-11 16:38:13 -080022import androidx.compose.foundation.text.Handle
23import androidx.compose.foundation.text.TextDragObserver
24import androidx.compose.foundation.text.selection.Selection.AnchorInfo
25import androidx.compose.runtime.MutableState
Louis Pullen-Freilich1f10a592020-07-24 16:35:14 +010026import androidx.compose.runtime.State
27import androidx.compose.runtime.getValue
28import androidx.compose.runtime.mutableStateOf
29import androidx.compose.runtime.setValue
haoyuc40d02752021-01-25 17:32:47 -080030import androidx.compose.ui.Modifier
31import androidx.compose.ui.focus.FocusRequester
32import androidx.compose.ui.focus.focusRequester
haoyuc40d02752021-01-25 17:32:47 -080033import androidx.compose.ui.focus.onFocusChanged
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010034import androidx.compose.ui.geometry.Offset
35import androidx.compose.ui.geometry.Rect
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010036import androidx.compose.ui.hapticfeedback.HapticFeedback
37import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010038import androidx.compose.ui.input.key.KeyEvent
39import androidx.compose.ui.input.key.onKeyEvent
Ralston Da Silvade62bc62021-06-02 17:46:44 -070040import androidx.compose.ui.input.pointer.PointerInputScope
41import androidx.compose.ui.input.pointer.pointerInput
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010042import androidx.compose.ui.layout.LayoutCoordinates
George Mount77ca2a22020-12-11 17:46:19 +000043import androidx.compose.ui.layout.boundsInWindow
haoyuc40d02752021-01-25 17:32:47 -080044import androidx.compose.ui.layout.onGloballyPositioned
haoyu7ad5ea32021-03-22 10:36:35 -070045import androidx.compose.ui.layout.positionInWindow
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010046import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010047import androidx.compose.ui.platform.TextToolbar
48import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010049import androidx.compose.ui.text.AnnotatedString
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -080050import androidx.compose.ui.unit.IntSize
Louis Pullen-Freilich1ffdbfc2023-08-24 18:35:34 +010051import androidx.compose.ui.util.fastFold
Grant Toepferfefbd7a2023-03-03 15:02:55 -080052import androidx.compose.ui.util.fastForEach
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -080053import kotlin.math.absoluteValue
Nader Jawade6a9b332020-05-21 13:49:20 -070054import kotlin.math.max
55import kotlin.math.min
Qingqing Deng6f56a912019-05-13 10:10:37 -070056
Qingqing Deng35f97ea2019-09-18 19:24:37 -070057/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010058 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070059 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070060internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Zach Klippensteinadabe342021-11-11 16:38:13 -080061
62 private val _selection: MutableState<Selection?> = mutableStateOf(null)
63
Qingqing Deng6f56a912019-05-13 10:10:37 -070064 /**
65 * The current selection.
66 */
Zach Klippensteinadabe342021-11-11 16:38:13 -080067 var selection: Selection?
68 get() = _selection.value
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010069 set(value) {
Zach Klippensteinadabe342021-11-11 16:38:13 -080070 _selection.value = value
haoyu9085c882020-12-08 12:01:06 -080071 if (value != null) {
72 updateHandleOffsets()
73 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010074 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070075
76 /**
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010077 * Is touch mode active
78 */
Grant Toepferfefbd7a2023-03-03 15:02:55 -080079 private val _isInTouchMode = mutableStateOf(true)
80 var isInTouchMode: Boolean
81 get() = _isInTouchMode.value
82 set(value) {
83 if (_isInTouchMode.value != value) {
84 _isInTouchMode.value = value
85 if (value && showToolbar) showSelectionToolbar() else hideSelectionToolbar()
86 }
87 }
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010088
89 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070090 * The manager will invoke this every time it comes to the conclusion that the selection should
91 * change. The expectation is that this callback will end up causing `setSelection` to get
92 * called. This is what makes this a "controlled component".
93 */
94 var onSelectionChange: (Selection?) -> Unit = {}
95
96 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080097 * [HapticFeedback] handle to perform haptic feedback.
98 */
99 var hapticFeedBack: HapticFeedback? = null
100
101 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700102 * [ClipboardManager] to perform clipboard features.
103 */
104 var clipboardManager: ClipboardManager? = null
105
106 /**
Qingqing Denga89450d2020-04-03 19:11:49 -0700107 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
108 */
109 var textToolbar: TextToolbar? = null
110
111 /**
haoyuc40d02752021-01-25 17:32:47 -0800112 * Focus requester used to request focus when selection becomes active.
113 */
114 var focusRequester: FocusRequester = FocusRequester()
115
116 /**
haoyu3c3fb452021-02-18 01:01:14 -0800117 * Return true if the corresponding SelectionContainer is focused.
haoyuc40d02752021-01-25 17:32:47 -0800118 */
haoyu3c3fb452021-02-18 01:01:14 -0800119 var hasFocus: Boolean by mutableStateOf(false)
haoyuc40d02752021-01-25 17:32:47 -0800120
121 /**
122 * Modifier for selection container.
123 */
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800124 val modifier
125 get() = Modifier
126 .onClearSelectionRequested { onRelease() }
127 .onGloballyPositioned { containerLayoutCoordinates = it }
128 .focusRequester(focusRequester)
129 .onFocusChanged { focusState ->
130 if (!focusState.isFocused && hasFocus) {
131 onRelease()
132 }
133 hasFocus = focusState.isFocused
haoyuc40d02752021-01-25 17:32:47 -0800134 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800135 .focusable()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800136 .updateSelectionTouchMode { isInTouchMode = it }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800137 .onKeyEvent {
138 if (isCopyKeyEvent(it)) {
139 copy()
140 true
141 } else {
142 false
143 }
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100144 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800145 .then(if (shouldShowMagnifier) Modifier.selectionMagnifier(this) else Modifier)
haoyuc40d02752021-01-25 17:32:47 -0800146
haoyu7ad5ea32021-03-22 10:36:35 -0700147 private var previousPosition: Offset? = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800148
haoyuc40d02752021-01-25 17:32:47 -0800149 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700150 * Layout Coordinates of the selection container.
151 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100152 var containerLayoutCoordinates: LayoutCoordinates? = null
153 set(value) {
154 field = value
haoyu2c6e9842021-03-30 17:39:04 -0700155 if (hasFocus && selection != null) {
156 val positionInWindow = value?.positionInWindow()
157 if (previousPosition != positionInWindow) {
158 previousPosition = positionInWindow
159 updateHandleOffsets()
160 updateSelectionToolbarPosition()
161 }
haoyu3c3fb452021-02-18 01:01:14 -0800162 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100163 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700164
165 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700166 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
167 * recalculated.
168 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800169 internal var dragBeginPosition by mutableStateOf(Offset.Zero)
170 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700171
172 /**
173 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
174 * it will be zeroed out.
175 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800176 internal var dragTotalDistance by mutableStateOf(Offset.Zero)
177 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700178
179 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100180 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
181 * is null when handle shouldn't be displayed.
182 * It is a [State] so reading it during the composition will cause recomposition every time
183 * the position has been changed.
184 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800185 var startHandlePosition: Offset? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100186 private set
187
188 /**
189 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
190 * is null when handle shouldn't be displayed.
191 * It is a [State] so reading it during the composition will cause recomposition every time
192 * the position has been changed.
193 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800194 var endHandlePosition: Offset? by mutableStateOf(null)
195 private set
196
197 /**
Zach Klippenstein63870892022-01-14 12:45:18 -0800198 * The handle that is currently being dragged, or null when no handle is being dragged. To get
199 * the position of the last drag event, use [currentDragPosition].
Zach Klippensteinadabe342021-11-11 16:38:13 -0800200 */
201 var draggingHandle: Handle? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100202 private set
203
Zach Klippenstein63870892022-01-14 12:45:18 -0800204 /**
205 * When a handle is being dragged (i.e. [draggingHandle] is non-null), this is the last position
206 * of the actual drag event. It is not clamped to handle positions. Null when not being dragged.
207 */
208 var currentDragPosition: Offset? by mutableStateOf(null)
209 private set
210
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800211 private val shouldShowMagnifier
212 get() = draggingHandle != null && isInTouchMode && isNonEmptySelection()
Zach Klippenstein4688a462021-12-08 08:28:07 -0800213
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100214 init {
haoyu7ad5ea32021-03-22 10:36:35 -0700215 selectionRegistrar.onPositionChangeCallback = { selectableId ->
216 if (
217 selectableId == selection?.start?.selectableId ||
218 selectableId == selection?.end?.selectableId
219 ) {
220 updateHandleOffsets()
221 updateSelectionToolbarPosition()
222 }
haoyue6d80a12020-12-02 16:04:52 -0800223 }
224
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100225 selectionRegistrar.onSelectionUpdateStartCallback =
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800226 { isInTouchMode, layoutCoordinates, position, selectionMode ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700227 val positionInContainer = convertToContainerCoordinates(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100228 layoutCoordinates,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700229 position
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100230 )
231
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700232 if (positionInContainer != null) {
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800233 this.isInTouchMode = isInTouchMode
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700234 startSelection(
235 position = positionInContainer,
236 isStartHandle = false,
237 adjustment = selectionMode
238 )
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100239
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700240 focusRequester.requestFocus()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800241 showToolbar = false
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700242 }
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100243 }
haoyue6d80a12020-12-02 16:04:52 -0800244
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700245 selectionRegistrar.onSelectionUpdateSelectAll =
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800246 { isInTouchMode, selectableId ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700247 val (newSelection, newSubselection) = selectAll(
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700248 selectableId = selectableId,
249 previousSelection = selection,
250 )
251 if (newSelection != selection) {
252 selectionRegistrar.subselections = newSubselection
253 onSelectionChange(newSelection)
254 }
255
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800256 this.isInTouchMode = isInTouchMode
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700257 focusRequester.requestFocus()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800258 showToolbar = false
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700259 }
260
haoyue6d80a12020-12-02 16:04:52 -0800261 selectionRegistrar.onSelectionUpdateCallback =
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800262 { isInTouchMode,
263 layoutCoordinates,
264 newPosition,
265 previousPosition,
266 isStartHandle,
267 selectionMode ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700268 val newPositionInContainer =
269 convertToContainerCoordinates(layoutCoordinates, newPosition)
270 val previousPositionInContainer =
271 convertToContainerCoordinates(layoutCoordinates, previousPosition)
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100272
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800273 this.isInTouchMode = isInTouchMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700274 updateSelection(
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700275 newPosition = newPositionInContainer,
276 previousPosition = previousPositionInContainer,
277 isStartHandle = isStartHandle,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700278 adjustment = selectionMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700279 )
280 }
haoyue6d80a12020-12-02 16:04:52 -0800281
282 selectionRegistrar.onSelectionUpdateEndCallback = {
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800283 showToolbar = true
Zach Klippensteinadabe342021-11-11 16:38:13 -0800284 // This property is set by updateSelection while dragging, so we need to clear it after
285 // the original selection drag.
286 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800287 currentDragPosition = null
haoyue6d80a12020-12-02 16:04:52 -0800288 }
haoyu9085c882020-12-08 12:01:06 -0800289
haoyue04245e2021-03-08 14:52:56 -0800290 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
291 if (selectableKey in selectionRegistrar.subselections) {
haoyu9085c882020-12-08 12:01:06 -0800292 // clear the selection range of each Selectable.
293 onRelease()
294 selection = null
295 }
296 }
haoyue04245e2021-03-08 14:52:56 -0800297
298 selectionRegistrar.afterSelectableUnsubscribe = { selectableKey ->
299 if (
300 selectableKey == selection?.start?.selectableId ||
301 selectableKey == selection?.end?.selectableId
302 ) {
303 // The selectable that contains a selection handle just unsubscribed.
304 // Hide selection handles for now
305 startHandlePosition = null
306 endHandlePosition = null
307 }
308 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100309 }
310
Zach Klippensteinadabe342021-11-11 16:38:13 -0800311 /**
312 * Returns the [Selectable] responsible for managing the given [AnchorInfo], or null if the
313 * anchor is not from a currently-registered [Selectable].
314 */
315 internal fun getAnchorSelectable(anchor: AnchorInfo): Selectable? {
316 return selectionRegistrar.selectableMap[anchor.selectableId]
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100317 }
318
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100319 private fun updateHandleOffsets() {
320 val selection = selection
321 val containerCoordinates = containerLayoutCoordinates
Zach Klippensteinadabe342021-11-11 16:38:13 -0800322 val startSelectable = selection?.start?.let(::getAnchorSelectable)
323 val endSelectable = selection?.end?.let(::getAnchorSelectable)
haoyue04245e2021-03-08 14:52:56 -0800324 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
325 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
haoyue2678c62020-12-09 08:39:12 -0800326 if (
327 selection == null ||
328 containerCoordinates == null ||
329 !containerCoordinates.isAttached ||
330 startLayoutCoordinates == null ||
331 endLayoutCoordinates == null
332 ) {
333 this.startHandlePosition = null
334 this.endHandlePosition = null
335 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100336 }
haoyue2678c62020-12-09 08:39:12 -0800337
George Mount77ca2a22020-12-11 17:46:19 +0000338 val startHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800339 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800340 startSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800341 selection = selection,
342 isStartHandle = true
343 )
344 )
George Mount77ca2a22020-12-11 17:46:19 +0000345 val endHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800346 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800347 endSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800348 selection = selection,
349 isStartHandle = false
350 )
351 )
352
353 val visibleBounds = containerCoordinates.visibleBounds()
Halil Ozercanb2486f12023-01-09 07:40:22 +0000354
355 // set the new handle position only if the handle is in visible bounds or
356 // the handle is still dragging. If handle goes out of visible bounds during drag, handle
357 // popup is also removed from composition, halting the drag gesture. This affects multiple
358 // text selection when selected text is configured with maxLines=1 and overflow=clip.
359 this.startHandlePosition = startHandlePosition.takeIf {
360 visibleBounds.containsInclusive(startHandlePosition) ||
361 draggingHandle == Handle.SelectionStart
362 }
363 this.endHandlePosition = endHandlePosition.takeIf {
364 visibleBounds.containsInclusive(endHandlePosition) ||
365 draggingHandle == Handle.SelectionEnd
366 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100367 }
368
369 /**
370 * Returns non-nullable [containerLayoutCoordinates].
371 */
372 internal fun requireContainerCoordinates(): LayoutCoordinates {
373 val coordinates = containerLayoutCoordinates
Ralston Da Silvaf24ae422023-05-30 12:59:24 -0700374 requireNotNull(coordinates) { "null coordinates" }
375 require(coordinates.isAttached) { "unattached coordinates" }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100376 return coordinates
377 }
378
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700379 internal fun selectAll(
380 selectableId: Long,
381 previousSelection: Selection?
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700382 ): Pair<Selection?, Map<Long, Selection>> {
383 val subselections = mutableMapOf<Long, Selection>()
384 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
385 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
386 val selection = if (selectable.selectableId == selectableId)
387 selectable.getSelectAllSelection() else null
388 selection?.let { subselections[selectable.selectableId] = it }
389 merge(mergedSelection, selection)
390 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800391 if (isInTouchMode && newSelection != previousSelection) {
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700392 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
393 }
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700394 return Pair(newSelection, subselections)
395 }
396
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800397 internal fun isNonEmptySelection(): Boolean {
398 val selection = selection ?: return false
399
400 var betweenSelectables = false
401 selectionRegistrar.sort(requireContainerCoordinates()).fastForEach {
402 if (
403 it.selectableId != selection.start.selectableId &&
404 it.selectableId != selection.end.selectableId &&
405 !betweenSelectables
406 ) {
407 // haven't found our selection yet, continue
408 return@fastForEach
409 }
410
411 betweenSelectables = true
412 if (!isCurrentSelectionEmpty(selection = selection, selectable = it)) {
413 return true
414 }
415
416 // short-circuit if this is the last selectable
417 if (it.selectableId == selection.end.selectableId && !selection.handlesCrossed ||
418 it.selectableId == selection.start.selectableId && selection.handlesCrossed
419 ) return false
420 }
421 return false
422 }
423
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800424 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100425 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800426 var selectedText: AnnotatedString? = null
427
428 selection?.let {
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +0000429 for (i in selectables.indices) {
haoyue04245e2021-03-08 14:52:56 -0800430 val selectable = selectables[i]
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800431 // Continue if the current selectable is before the selection starts.
haoyue04245e2021-03-08 14:52:56 -0800432 if (selectable.selectableId != it.start.selectableId &&
433 selectable.selectableId != it.end.selectableId &&
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800434 selectedText == null
435 ) continue
436
437 val currentSelectedText = getCurrentSelectedText(
haoyue04245e2021-03-08 14:52:56 -0800438 selectable = selectable,
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800439 selection = it
440 )
441 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
442
443 // Break if the current selectable is the last selected selectable.
haoyue04245e2021-03-08 14:52:56 -0800444 if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
445 selectable.selectableId == it.start.selectableId && it.handlesCrossed
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800446 ) break
447 }
448 }
449 return selectedText
450 }
451
Qingqing Dengde023cc2020-04-24 14:23:41 -0700452 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700453 val selectedText = getSelectedText()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800454 selectedText?.takeIf { it.isNotEmpty() }?.let { clipboardManager?.setText(it) }
Qingqing Dengde023cc2020-04-24 14:23:41 -0700455 }
456
457 /**
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800458 * Whether toolbar should be shown right now.
459 * Examples: Show toolbar after user finishes selection.
460 * Hide it during selection.
461 * Hide it when no selection exists.
462 */
463 internal var showToolbar = false
464 internal set(value) {
465 field = value
466 if (value && isInTouchMode) showSelectionToolbar() else hideSelectionToolbar()
467 }
468
469 /**
Qingqing Dengde023cc2020-04-24 14:23:41 -0700470 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
471 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
472 * the copy method as a callback when "copy" is clicked.
473 */
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800474 private fun showSelectionToolbar() {
475 if (hasFocus && isNonEmptySelection()) {
476 textToolbar?.showMenu(
477 getContentRect(),
478 onCopyRequested = {
479 copy()
480 onRelease()
481 }
482 )
Qingqing Denga89450d2020-04-03 19:11:49 -0700483 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700484 }
485
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800486 private fun hideSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800487 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800488 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700489 }
490 }
491
492 private fun updateSelectionToolbarPosition() {
haoyu3c3fb452021-02-18 01:01:14 -0800493 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700494 showSelectionToolbar()
495 }
496 }
497
498 /**
499 * Calculate selected region as [Rect]. The top is the top of the first selected
500 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
501 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
502 */
503 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700504 val selection = selection ?: return Rect.Zero
Zach Klippensteinadabe342021-11-11 16:38:13 -0800505 val startSelectable = getAnchorSelectable(selection.start)
506 val endSelectable = getAnchorSelectable(selection.end)
haoyue04245e2021-03-08 14:52:56 -0800507 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
508 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700509
510 val localLayoutCoordinates = containerLayoutCoordinates
511 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000512 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700513 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800514 startSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700515 selection = selection,
516 isStartHandle = true
517 )
518 )
George Mount77ca2a22020-12-11 17:46:19 +0000519 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700520 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800521 endSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700522 selection = selection,
523 isStartHandle = false
524 )
525 )
526
527 startOffset = localLayoutCoordinates.localToRoot(startOffset)
528 endOffset = localLayoutCoordinates.localToRoot(endOffset)
529
530 val left = min(startOffset.x, endOffset.x)
531 val right = max(startOffset.x, endOffset.x)
532
George Mount77ca2a22020-12-11 17:46:19 +0000533 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700534 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700535 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700536 0f,
haoyue04245e2021-03-08 14:52:56 -0800537 startSelectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700538 )
539 )
540
George Mount77ca2a22020-12-11 17:46:19 +0000541 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700542 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700543 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700544 0.0f,
haoyue04245e2021-03-08 14:52:56 -0800545 endSelectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700546 )
547 )
548
549 startTop = localLayoutCoordinates.localToRoot(startTop)
550 endTop = localLayoutCoordinates.localToRoot(endTop)
551
552 val top = min(startTop.y, endTop.y)
Haoyu Zhangd47f1fe2021-07-13 03:10:55 -0700553 val bottom = max(startOffset.y, endOffset.y) + (HandleHeight.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700554
555 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700556 left,
557 top,
558 right,
559 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700560 )
561 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700562 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700563 }
564
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800565 // This is for PressGestureDetector to cancel the selection.
566 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800567 selectionRegistrar.subselections = emptyMap()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800568 showToolbar = false
haoyue04245e2021-03-08 14:52:56 -0800569 if (selection != null) {
570 onSelectionChange(null)
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800571 if (isInTouchMode) {
572 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
573 }
haoyue04245e2021-03-08 14:52:56 -0800574 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800575 }
576
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800577 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver = object : TextDragObserver {
578 override fun onDown(point: Offset) {
Zach Klippenstein63870892022-01-14 12:45:18 -0800579 val selection = selection ?: return
580 val anchor = if (isStartHandle) selection.start else selection.end
581 val selectable = getAnchorSelectable(anchor) ?: return
582 // The LayoutCoordinates of the composable where the drag gesture should begin. This
583 // is used to convert the position of the beginning of the drag gesture from the
584 // composable coordinates to selection container coordinates.
585 val beginLayoutCoordinates = selectable.getLayoutCoordinates() ?: return
586
587 // The position of the character where the drag gesture should begin. This is in
588 // the composable coordinates.
589 val beginCoordinates = getAdjustedCoordinates(
590 selectable.getHandlePosition(
591 selection = selection, isStartHandle = isStartHandle
592 )
593 )
594
595 // Convert the position where drag gesture begins from composable coordinates to
596 // selection container coordinates.
597 currentDragPosition = requireContainerCoordinates().localPositionOf(
598 beginLayoutCoordinates,
599 beginCoordinates
600 )
601 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800602 showToolbar = false
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800603 }
604
605 override fun onStart(startPoint: Offset) {
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800606 val selection = selection!!
607 val startSelectable =
608 selectionRegistrar.selectableMap[selection.start.selectableId]
609 val endSelectable =
610 selectionRegistrar.selectableMap[selection.end.selectableId]
611 // The LayoutCoordinates of the composable where the drag gesture should begin. This
612 // is used to convert the position of the beginning of the drag gesture from the
613 // composable coordinates to selection container coordinates.
614 val beginLayoutCoordinates = if (isStartHandle) {
615 startSelectable?.getLayoutCoordinates()!!
616 } else {
617 endSelectable?.getLayoutCoordinates()!!
618 }
619
620 // The position of the character where the drag gesture should begin. This is in
621 // the composable coordinates.
622 val beginCoordinates = getAdjustedCoordinates(
623 if (isStartHandle) {
624 startSelectable!!.getHandlePosition(
625 selection = selection, isStartHandle = true
626 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700627 } else {
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800628 endSelectable!!.getHandlePosition(
629 selection = selection, isStartHandle = false
630 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700631 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800632 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700633
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800634 // Convert the position where drag gesture begins from composable coordinates to
635 // selection container coordinates.
636 dragBeginPosition = requireContainerCoordinates().localPositionOf(
637 beginLayoutCoordinates,
638 beginCoordinates
639 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700640
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800641 // Zero out the total distance that being dragged.
642 dragTotalDistance = Offset.Zero
643 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700644
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800645 override fun onDrag(delta: Offset) {
646 dragTotalDistance += delta
647 val endPosition = dragBeginPosition + dragTotalDistance
648 val consumed = updateSelection(
649 newPosition = endPosition,
650 previousPosition = dragBeginPosition,
651 isStartHandle = isStartHandle,
652 adjustment = SelectionAdjustment.CharacterWithWordAccelerate
653 )
654 if (consumed) {
655 dragBeginPosition = endPosition
Nader Jawad6df06122020-06-03 15:27:08 -0700656 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700657 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800658 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700659
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800660 private fun done() {
661 showToolbar = true
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800662 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800663 currentDragPosition = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800664 }
haoyue6d80a12020-12-02 16:04:52 -0800665
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800666 override fun onUp() = done()
667 override fun onStop() = done()
668 override fun onCancel() = done()
Qingqing Deng6f56a912019-05-13 10:10:37 -0700669 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700670
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700671 /**
672 * Detect tap without consuming the up event.
673 */
674 private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
George Mount32de9dd2022-10-05 14:51:06 -0700675 awaitEachGesture {
676 waitForUpOrCancellation()?.let {
677 onTap(it.position)
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700678 }
679 }
680 }
681
682 private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
683 return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
684 }
685
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700686 private fun convertToContainerCoordinates(
687 layoutCoordinates: LayoutCoordinates,
688 offset: Offset
689 ): Offset? {
690 val coordinates = containerLayoutCoordinates
691 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000692 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700693 }
694
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700695 /**
696 * Cancel the previous selection and start a new selection at the given [position].
697 * It's used for long-press, double-click, triple-click and so on to start selection.
698 *
699 * @param position initial position of the selection. Both start and end handle is considered
700 * at this position.
701 * @param isStartHandle whether it's considered as the start handle moving. This parameter
702 * will influence the [SelectionAdjustment]'s behavior. For example,
703 * [SelectionAdjustment.Character] only adjust the moving handle.
704 * @param adjustment the selection adjustment.
705 */
706 private fun startSelection(
707 position: Offset,
708 isStartHandle: Boolean,
709 adjustment: SelectionAdjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700710 ) {
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700711 updateSelection(
712 startHandlePosition = position,
713 endHandlePosition = position,
714 previousHandlePosition = null,
715 isStartHandle = isStartHandle,
716 adjustment = adjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700717 )
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700718 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700719
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700720 /**
721 * Updates the selection after one of the selection handle moved.
722 *
723 * @param newPosition the new position of the moving selection handle.
724 * @param previousPosition the previous position of the moving selection handle.
725 * @param isStartHandle whether the moving selection handle is the start handle.
726 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
727 * produce the final selection range.
728 *
729 * @return a boolean representing whether the movement is consumed.
730 *
731 * @see SelectionAdjustment
732 */
733 internal fun updateSelection(
734 newPosition: Offset?,
735 previousPosition: Offset?,
736 isStartHandle: Boolean,
737 adjustment: SelectionAdjustment,
738 ): Boolean {
739 if (newPosition == null) return false
740 val otherHandlePosition = selection?.let { selection ->
741 val otherSelectableId = if (isStartHandle) {
742 selection.end.selectableId
743 } else {
744 selection.start.selectableId
745 }
746 val otherSelectable =
747 selectionRegistrar.selectableMap[otherSelectableId] ?: return@let null
748 convertToContainerCoordinates(
749 otherSelectable.getLayoutCoordinates()!!,
750 getAdjustedCoordinates(
751 otherSelectable.getHandlePosition(selection, !isStartHandle)
752 )
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700753 )
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700754 } ?: return false
755
756 val startHandlePosition = if (isStartHandle) newPosition else otherHandlePosition
757 val endHandlePosition = if (isStartHandle) otherHandlePosition else newPosition
758
759 return updateSelection(
760 startHandlePosition = startHandlePosition,
761 endHandlePosition = endHandlePosition,
762 previousHandlePosition = previousPosition,
763 isStartHandle = isStartHandle,
764 adjustment = adjustment
765 )
766 }
767
768 /**
769 * Updates the selection after one of the selection handle moved.
770 *
771 * To make sure that [SelectionAdjustment] works correctly, it's expected that only one
772 * selection handle is updated each time. The only exception is that when a new selection is
773 * started. In this case, [previousHandlePosition] is always null.
774 *
775 * @param startHandlePosition the position of the start selection handle.
776 * @param endHandlePosition the position of the end selection handle.
777 * @param previousHandlePosition the position of the moving handle before the update.
778 * @param isStartHandle whether the moving selection handle is the start handle.
779 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
780 * produce the final selection range.
781 *
782 * @return a boolean representing whether the movement is consumed. It's useful for the case
783 * where a selection handle is updating consecutively. When the return value is true, it's
784 * expected that the caller will update the [startHandlePosition] to be the given
785 * [endHandlePosition] in following calls.
786 *
787 * @see SelectionAdjustment
788 */
789 internal fun updateSelection(
790 startHandlePosition: Offset,
791 endHandlePosition: Offset,
792 previousHandlePosition: Offset?,
793 isStartHandle: Boolean,
794 adjustment: SelectionAdjustment,
795 ): Boolean {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800796 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Zach Klippenstein63870892022-01-14 12:45:18 -0800797 currentDragPosition = if (isStartHandle) startHandlePosition else endHandlePosition
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700798 val newSubselections = mutableMapOf<Long, Selection>()
799 var moveConsumed = false
800 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
801 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800802 val previousSubSelection =
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700803 selectionRegistrar.subselections[selectable.selectableId]
804 val (selection, consumed) = selectable.updateSelection(
805 startHandlePosition = startHandlePosition,
806 endHandlePosition = endHandlePosition,
807 previousHandlePosition = previousHandlePosition,
808 isStartHandle = isStartHandle,
809 containerLayoutCoordinates = requireContainerCoordinates(),
810 adjustment = adjustment,
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800811 previousSelection = previousSubSelection,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700812 )
813
814 moveConsumed = moveConsumed || consumed
815 selection?.let { newSubselections[selectable.selectableId] = it }
816 merge(mergedSelection, selection)
817 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800818
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700819 if (newSelection != selection) {
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800820 if (isInTouchMode) {
821 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
822 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700823 selectionRegistrar.subselections = newSubselections
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700824 onSelectionChange(newSelection)
Grant Toepfer4bd91cd2023-06-05 11:27:51 -0700825 // always consume if selection changed, it is possible that it is false at this
826 // point if selectables were only removed from the selection
827 moveConsumed = true
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700828 }
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700829 return moveConsumed
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700830 }
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200831
832 fun contextMenuOpenAdjustment(position: Offset) {
833 val isEmptySelection = selection?.toTextRange()?.collapsed ?: true
834 // TODO(b/209483184) the logic should be more complex here, it should check that current
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800835 // selection doesn't include click position
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200836 if (isEmptySelection) {
837 startSelection(
838 position = position,
839 isStartHandle = true,
840 adjustment = SelectionAdjustment.Word
841 )
842 }
843 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700844}
845
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200846internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700847 return lhs?.merge(rhs) ?: rhs
848}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800849
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100850internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
851
Zach Klippensteinadabe342021-11-11 16:38:13 -0800852internal expect fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier
853
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -0800854internal fun calculateSelectionMagnifierCenterAndroid(
855 manager: SelectionManager,
856 magnifierSize: IntSize
857): Offset {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800858 fun getMagnifierCenter(anchor: AnchorInfo, isStartHandle: Boolean): Offset {
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -0800859 val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
860 val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
861 val selectableCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified
862 // The end offset is exclusive.
863 val offset = if (isStartHandle) anchor.offset else anchor.offset - 1
864
Halil Ozercan1b849a62023-01-09 07:41:38 +0000865 if (offset > selectable.getLastVisibleOffset()) return Offset.Unspecified
866
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -0800867 // The horizontal position doesn't snap to cursor positions but should directly track the
868 // actual drag.
869 val localDragPosition = selectableCoordinates.localPositionOf(
870 containerCoordinates,
871 manager.currentDragPosition!!
872 )
873 val dragX = localDragPosition.x
874 // But it is constrained by the horizontal bounds of the current line.
875 val centerX = selectable.getRangeOfLineContaining(offset).let { line ->
876 val lineMin = selectable.getBoundingBox(line.min)
877 // line.end is exclusive, but we want the bounding box of the actual last character in
878 // the line.
879 val lineMax = selectable.getBoundingBox((line.max - 1).coerceAtLeast(line.min))
880 val minX = minOf(lineMin.left, lineMax.left)
881 val maxX = maxOf(lineMin.right, lineMax.right)
882 dragX.coerceIn(minX, maxX)
883 }
884
885 // Hide the magnifier when dragged too far (outside the horizontal bounds of how big the
886 // magnifier actually is). See
887 // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Editor.java;l=5228-5231;drc=2fdb6bd709be078b72f011334362456bb758922c
888 if ((dragX - centerX).absoluteValue > magnifierSize.width / 2) {
889 return Offset.Unspecified
890 }
Zach Klippensteinadabe342021-11-11 16:38:13 -0800891
892 // Let the selectable determine the vertical position of the magnifier, since it should be
893 // clamped to the center of text lines.
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -0800894 val anchorBounds = selectable.getBoundingBox(offset)
895 val centerY = anchorBounds.center.y
896
Zach Klippensteinadabe342021-11-11 16:38:13 -0800897 return containerCoordinates.localPositionOf(
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -0800898 sourceCoordinates = selectableCoordinates,
899 relativeToSource = Offset(centerX, centerY)
Zach Klippensteinadabe342021-11-11 16:38:13 -0800900 )
901 }
902
903 val selection = manager.selection ?: return Offset.Unspecified
904 return when (manager.draggingHandle) {
905 null -> return Offset.Unspecified
906 Handle.SelectionStart -> getMagnifierCenter(selection.start, isStartHandle = true)
907 Handle.SelectionEnd -> getMagnifierCenter(selection.end, isStartHandle = false)
908 Handle.Cursor -> error("SelectionContainer does not support cursor")
909 }
910}
911
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800912private fun isCurrentSelectionEmpty(
913 selectable: Selectable,
914 selection: Selection
915): Boolean {
916 val selectableId = selectable.selectableId
917 val startSelectableId = selection.start.selectableId
918 val endSelectableId = selection.end.selectableId
919
920 if (selectableId == startSelectableId && selectableId == endSelectableId) {
921 return selection.start.offset == selection.end.offset
922 }
923
924 val text = selectable.getText()
925 val handlesCrossed = selection.handlesCrossed
926 return when (selectableId) {
927 startSelectableId -> selection.start.offset == if (handlesCrossed) 0 else text.length
928 endSelectableId -> selection.end.offset == if (handlesCrossed) text.length else 0
929 else -> text.isEmpty()
930 }
931}
932
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200933internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800934 selectable: Selectable,
935 selection: Selection
936): AnnotatedString {
937 val currentText = selectable.getText()
938
939 return if (
haoyue04245e2021-03-08 14:52:56 -0800940 selectable.selectableId != selection.start.selectableId &&
941 selectable.selectableId != selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800942 ) {
943 // Select the full text content if the current selectable is between the
944 // start and the end selectables.
945 currentText
946 } else if (
haoyue04245e2021-03-08 14:52:56 -0800947 selectable.selectableId == selection.start.selectableId &&
948 selectable.selectableId == selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800949 ) {
950 // Select partial text content if the current selectable is the start and
951 // the end selectable.
952 if (selection.handlesCrossed) {
953 currentText.subSequence(selection.end.offset, selection.start.offset)
954 } else {
955 currentText.subSequence(selection.start.offset, selection.end.offset)
956 }
haoyue04245e2021-03-08 14:52:56 -0800957 } else if (selectable.selectableId == selection.start.selectableId) {
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800958 // Select partial text content if the current selectable is the start
959 // selectable.
960 if (selection.handlesCrossed) {
961 currentText.subSequence(0, selection.start.offset)
962 } else {
963 currentText.subSequence(selection.start.offset, currentText.length)
964 }
965 } else {
966 // Selectable partial text content if the current selectable is the end
967 // selectable.
968 if (selection.handlesCrossed) {
969 currentText.subSequence(selection.end.offset, currentText.length)
970 } else {
971 currentText.subSequence(0, selection.end.offset)
972 }
973 }
974}
haoyue2678c62020-12-09 08:39:12 -0800975
976/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800977internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800978 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
979 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
980 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000981 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800982 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000983 windowToLocal(boundsInWindow.topLeft),
984 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800985 )
986}
987
haoyuac341f02021-01-22 22:01:56 -0800988internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800989 offset.x in left..right && offset.y in top..bottom