[go: nahoru, domu]

blob: 5bf0b16ed70658e9b0b104f068805b499c720bff [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
Grant Toepferd681b5c2023-06-14 11:24:02 -070019import androidx.annotation.VisibleForTesting
haoyuc40d02752021-01-25 17:32:47 -080020import androidx.compose.foundation.focusable
George Mount32de9dd2022-10-05 14:51:06 -070021import androidx.compose.foundation.gestures.awaitEachGesture
Ralston Da Silvade62bc62021-06-02 17:46:44 -070022import androidx.compose.foundation.gestures.waitForUpOrCancellation
Zach Klippensteinadabe342021-11-11 16:38:13 -080023import androidx.compose.foundation.text.Handle
24import androidx.compose.foundation.text.TextDragObserver
25import androidx.compose.foundation.text.selection.Selection.AnchorInfo
Grant Toepfer427dbdf2023-09-05 18:38:42 -070026import androidx.compose.foundation.text2.input.internal.coerceIn
Zach Klippensteinadabe342021-11-11 16:38:13 -080027import androidx.compose.runtime.MutableState
Louis Pullen-Freilich1f10a592020-07-24 16:35:14 +010028import androidx.compose.runtime.State
29import androidx.compose.runtime.getValue
30import androidx.compose.runtime.mutableStateOf
31import androidx.compose.runtime.setValue
haoyuc40d02752021-01-25 17:32:47 -080032import androidx.compose.ui.Modifier
33import androidx.compose.ui.focus.FocusRequester
34import androidx.compose.ui.focus.focusRequester
haoyuc40d02752021-01-25 17:32:47 -080035import androidx.compose.ui.focus.onFocusChanged
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010036import androidx.compose.ui.geometry.Offset
37import androidx.compose.ui.geometry.Rect
Grant Toepferd681b5c2023-06-14 11:24:02 -070038import androidx.compose.ui.geometry.isSpecified
39import androidx.compose.ui.geometry.isUnspecified
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010040import androidx.compose.ui.hapticfeedback.HapticFeedback
41import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010042import androidx.compose.ui.input.key.KeyEvent
43import androidx.compose.ui.input.key.onKeyEvent
Ralston Da Silvade62bc62021-06-02 17:46:44 -070044import androidx.compose.ui.input.pointer.PointerInputScope
45import androidx.compose.ui.input.pointer.pointerInput
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010046import androidx.compose.ui.layout.LayoutCoordinates
George Mount77ca2a22020-12-11 17:46:19 +000047import androidx.compose.ui.layout.boundsInWindow
haoyuc40d02752021-01-25 17:32:47 -080048import androidx.compose.ui.layout.onGloballyPositioned
haoyu7ad5ea32021-03-22 10:36:35 -070049import androidx.compose.ui.layout.positionInWindow
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010050import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010051import androidx.compose.ui.platform.TextToolbar
52import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010053import androidx.compose.ui.text.AnnotatedString
Grant Toepfer427dbdf2023-09-05 18:38:42 -070054import androidx.compose.ui.text.buildAnnotatedString
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -080055import androidx.compose.ui.unit.IntSize
Grant Toepferd681b5c2023-06-14 11:24:02 -070056import androidx.compose.ui.util.fastAny
Grant Toepfer427dbdf2023-09-05 18:38:42 -070057import androidx.compose.ui.util.fastFilter
Louis Pullen-Freilich1ffdbfc2023-08-24 18:35:34 +010058import androidx.compose.ui.util.fastFold
Grant Toepferfefbd7a2023-03-03 15:02:55 -080059import androidx.compose.ui.util.fastForEach
Grant Toepferd681b5c2023-06-14 11:24:02 -070060import androidx.compose.ui.util.fastForEachIndexed
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -080061import kotlin.math.absoluteValue
Qingqing Deng6f56a912019-05-13 10:10:37 -070062
Qingqing Deng35f97ea2019-09-18 19:24:37 -070063/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010064 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070065 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070066internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Zach Klippensteinadabe342021-11-11 16:38:13 -080067
68 private val _selection: MutableState<Selection?> = mutableStateOf(null)
69
Qingqing Deng6f56a912019-05-13 10:10:37 -070070 /**
71 * The current selection.
72 */
Zach Klippensteinadabe342021-11-11 16:38:13 -080073 var selection: Selection?
74 get() = _selection.value
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010075 set(value) {
Zach Klippensteinadabe342021-11-11 16:38:13 -080076 _selection.value = value
haoyu9085c882020-12-08 12:01:06 -080077 if (value != null) {
78 updateHandleOffsets()
79 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010080 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070081
82 /**
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010083 * Is touch mode active
84 */
Grant Toepferfefbd7a2023-03-03 15:02:55 -080085 private val _isInTouchMode = mutableStateOf(true)
86 var isInTouchMode: Boolean
87 get() = _isInTouchMode.value
88 set(value) {
89 if (_isInTouchMode.value != value) {
90 _isInTouchMode.value = value
Grant Toepfer427dbdf2023-09-05 18:38:42 -070091 updateSelectionToolbar()
Grant Toepferfefbd7a2023-03-03 15:02:55 -080092 }
93 }
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010094
95 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070096 * The manager will invoke this every time it comes to the conclusion that the selection should
97 * change. The expectation is that this callback will end up causing `setSelection` to get
98 * called. This is what makes this a "controlled component".
99 */
100 var onSelectionChange: (Selection?) -> Unit = {}
101
102 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800103 * [HapticFeedback] handle to perform haptic feedback.
104 */
105 var hapticFeedBack: HapticFeedback? = null
106
107 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700108 * [ClipboardManager] to perform clipboard features.
109 */
110 var clipboardManager: ClipboardManager? = null
111
112 /**
Qingqing Denga89450d2020-04-03 19:11:49 -0700113 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
114 */
115 var textToolbar: TextToolbar? = null
116
117 /**
haoyuc40d02752021-01-25 17:32:47 -0800118 * Focus requester used to request focus when selection becomes active.
119 */
120 var focusRequester: FocusRequester = FocusRequester()
121
122 /**
haoyu3c3fb452021-02-18 01:01:14 -0800123 * Return true if the corresponding SelectionContainer is focused.
haoyuc40d02752021-01-25 17:32:47 -0800124 */
haoyu3c3fb452021-02-18 01:01:14 -0800125 var hasFocus: Boolean by mutableStateOf(false)
haoyuc40d02752021-01-25 17:32:47 -0800126
127 /**
128 * Modifier for selection container.
129 */
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800130 val modifier
131 get() = Modifier
132 .onClearSelectionRequested { onRelease() }
133 .onGloballyPositioned { containerLayoutCoordinates = it }
134 .focusRequester(focusRequester)
135 .onFocusChanged { focusState ->
136 if (!focusState.isFocused && hasFocus) {
137 onRelease()
138 }
139 hasFocus = focusState.isFocused
haoyuc40d02752021-01-25 17:32:47 -0800140 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800141 .focusable()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800142 .updateSelectionTouchMode { isInTouchMode = it }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800143 .onKeyEvent {
144 if (isCopyKeyEvent(it)) {
145 copy()
146 true
147 } else {
148 false
149 }
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100150 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800151 .then(if (shouldShowMagnifier) Modifier.selectionMagnifier(this) else Modifier)
haoyuc40d02752021-01-25 17:32:47 -0800152
haoyu7ad5ea32021-03-22 10:36:35 -0700153 private var previousPosition: Offset? = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800154
haoyuc40d02752021-01-25 17:32:47 -0800155 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700156 * Layout Coordinates of the selection container.
157 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100158 var containerLayoutCoordinates: LayoutCoordinates? = null
159 set(value) {
160 field = value
haoyu2c6e9842021-03-30 17:39:04 -0700161 if (hasFocus && selection != null) {
162 val positionInWindow = value?.positionInWindow()
163 if (previousPosition != positionInWindow) {
164 previousPosition = positionInWindow
165 updateHandleOffsets()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700166 updateSelectionToolbar()
haoyu2c6e9842021-03-30 17:39:04 -0700167 }
haoyu3c3fb452021-02-18 01:01:14 -0800168 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100169 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700170
171 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700172 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
173 * recalculated.
174 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800175 internal var dragBeginPosition by mutableStateOf(Offset.Zero)
176 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700177
178 /**
179 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
180 * it will be zeroed out.
181 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800182 internal var dragTotalDistance by mutableStateOf(Offset.Zero)
183 private set
Qingqing Deng6f56a912019-05-13 10:10:37 -0700184
185 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100186 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
187 * is null when handle shouldn't be displayed.
188 * It is a [State] so reading it during the composition will cause recomposition every time
189 * the position has been changed.
190 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800191 var startHandlePosition: Offset? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100192 private set
193
194 /**
195 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
196 * is null when handle shouldn't be displayed.
197 * It is a [State] so reading it during the composition will cause recomposition every time
198 * the position has been changed.
199 */
Zach Klippensteinadabe342021-11-11 16:38:13 -0800200 var endHandlePosition: Offset? by mutableStateOf(null)
201 private set
202
203 /**
Zach Klippenstein63870892022-01-14 12:45:18 -0800204 * The handle that is currently being dragged, or null when no handle is being dragged. To get
205 * the position of the last drag event, use [currentDragPosition].
Zach Klippensteinadabe342021-11-11 16:38:13 -0800206 */
207 var draggingHandle: Handle? by mutableStateOf(null)
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100208 private set
209
Zach Klippenstein63870892022-01-14 12:45:18 -0800210 /**
211 * When a handle is being dragged (i.e. [draggingHandle] is non-null), this is the last position
212 * of the actual drag event. It is not clamped to handle positions. Null when not being dragged.
213 */
214 var currentDragPosition: Offset? by mutableStateOf(null)
215 private set
216
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800217 private val shouldShowMagnifier
Grant Toepferd681b5c2023-06-14 11:24:02 -0700218 get() = draggingHandle != null && isInTouchMode && !isTriviallyCollapsedSelection()
219
220 @VisibleForTesting
221 internal var previousSelectionLayout: SelectionLayout? = null
Zach Klippenstein4688a462021-12-08 08:28:07 -0800222
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100223 init {
haoyu7ad5ea32021-03-22 10:36:35 -0700224 selectionRegistrar.onPositionChangeCallback = { selectableId ->
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700225 if (selectableId in selectionRegistrar.subselections) {
haoyu7ad5ea32021-03-22 10:36:35 -0700226 updateHandleOffsets()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700227 updateSelectionToolbar()
haoyu7ad5ea32021-03-22 10:36:35 -0700228 }
haoyue6d80a12020-12-02 16:04:52 -0800229 }
230
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100231 selectionRegistrar.onSelectionUpdateStartCallback =
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800232 { isInTouchMode, layoutCoordinates, position, selectionMode ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700233 val positionInContainer = convertToContainerCoordinates(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100234 layoutCoordinates,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700235 position
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100236 )
237
Grant Toepferd681b5c2023-06-14 11:24:02 -0700238 if (positionInContainer.isSpecified) {
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800239 this.isInTouchMode = isInTouchMode
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700240 startSelection(
241 position = positionInContainer,
242 isStartHandle = false,
243 adjustment = selectionMode
244 )
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100245
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700246 focusRequester.requestFocus()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800247 showToolbar = false
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700248 }
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100249 }
haoyue6d80a12020-12-02 16:04:52 -0800250
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700251 selectionRegistrar.onSelectionUpdateSelectAll =
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800252 { isInTouchMode, selectableId ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700253 val (newSelection, newSubselection) = selectAll(
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700254 selectableId = selectableId,
255 previousSelection = selection,
256 )
257 if (newSelection != selection) {
258 selectionRegistrar.subselections = newSubselection
259 onSelectionChange(newSelection)
260 }
261
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800262 this.isInTouchMode = isInTouchMode
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700263 focusRequester.requestFocus()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800264 showToolbar = false
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700265 }
266
haoyue6d80a12020-12-02 16:04:52 -0800267 selectionRegistrar.onSelectionUpdateCallback =
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800268 { isInTouchMode,
269 layoutCoordinates,
270 newPosition,
271 previousPosition,
272 isStartHandle,
273 selectionMode ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700274 val newPositionInContainer =
275 convertToContainerCoordinates(layoutCoordinates, newPosition)
276 val previousPositionInContainer =
277 convertToContainerCoordinates(layoutCoordinates, previousPosition)
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100278
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800279 this.isInTouchMode = isInTouchMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700280 updateSelection(
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700281 newPosition = newPositionInContainer,
282 previousPosition = previousPositionInContainer,
283 isStartHandle = isStartHandle,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700284 adjustment = selectionMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700285 )
286 }
haoyue6d80a12020-12-02 16:04:52 -0800287
288 selectionRegistrar.onSelectionUpdateEndCallback = {
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800289 showToolbar = true
Zach Klippensteinadabe342021-11-11 16:38:13 -0800290 // This property is set by updateSelection while dragging, so we need to clear it after
291 // the original selection drag.
292 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800293 currentDragPosition = null
haoyue6d80a12020-12-02 16:04:52 -0800294 }
haoyu9085c882020-12-08 12:01:06 -0800295
haoyue04245e2021-03-08 14:52:56 -0800296 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
297 if (selectableKey in selectionRegistrar.subselections) {
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700298 // Clear the selection range of each Selectable.
haoyu9085c882020-12-08 12:01:06 -0800299 onRelease()
300 selection = null
301 }
302 }
haoyue04245e2021-03-08 14:52:56 -0800303
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700304 selectionRegistrar.afterSelectableUnsubscribe = { selectableId ->
305 if (selectableId == selection?.start?.selectableId) {
haoyue04245e2021-03-08 14:52:56 -0800306 // The selectable that contains a selection handle just unsubscribed.
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700307 // Hide the associated selection handle
haoyue04245e2021-03-08 14:52:56 -0800308 startHandlePosition = null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700309 }
310 if (selectableId == selection?.end?.selectableId) {
haoyue04245e2021-03-08 14:52:56 -0800311 endHandlePosition = null
312 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700313
314 if (selectableId in selectionRegistrar.subselections) {
315 // Unsubscribing the selectable may make the selection empty, which would hide it.
316 updateSelectionToolbar()
317 }
haoyue04245e2021-03-08 14:52:56 -0800318 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100319 }
320
Zach Klippensteinadabe342021-11-11 16:38:13 -0800321 /**
322 * Returns the [Selectable] responsible for managing the given [AnchorInfo], or null if the
323 * anchor is not from a currently-registered [Selectable].
324 */
325 internal fun getAnchorSelectable(anchor: AnchorInfo): Selectable? {
326 return selectionRegistrar.selectableMap[anchor.selectableId]
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100327 }
328
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100329 private fun updateHandleOffsets() {
330 val selection = selection
331 val containerCoordinates = containerLayoutCoordinates
Zach Klippensteinadabe342021-11-11 16:38:13 -0800332 val startSelectable = selection?.start?.let(::getAnchorSelectable)
333 val endSelectable = selection?.end?.let(::getAnchorSelectable)
haoyue04245e2021-03-08 14:52:56 -0800334 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
335 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700336
haoyue2678c62020-12-09 08:39:12 -0800337 if (
338 selection == null ||
339 containerCoordinates == null ||
340 !containerCoordinates.isAttached ||
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700341 (startLayoutCoordinates == null && endLayoutCoordinates == null)
haoyue2678c62020-12-09 08:39:12 -0800342 ) {
343 this.startHandlePosition = null
344 this.endHandlePosition = null
345 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100346 }
haoyue2678c62020-12-09 08:39:12 -0800347
haoyue2678c62020-12-09 08:39:12 -0800348 val visibleBounds = containerCoordinates.visibleBounds()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700349 this.startHandlePosition = startLayoutCoordinates?.let { handleCoordinates ->
350 // Set the new handle position only if the handle is in visible bounds or
351 // the handle is still dragging. If handle goes out of visible bounds during drag,
352 // handle popup is also removed from composition, halting the drag gesture. This
353 // affects multiple text selection when selected text is configured with maxLines=1
354 // and overflow=clip.
355 val handlePosition = startSelectable.getHandlePosition(selection, isStartHandle = true)
356 val position = containerCoordinates.localPositionOf(handleCoordinates, handlePosition)
357 position.takeIf {
358 draggingHandle == Handle.SelectionStart || visibleBounds.containsInclusive(it)
359 }
Halil Ozercanb2486f12023-01-09 07:40:22 +0000360 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700361
362 this.endHandlePosition = endLayoutCoordinates?.let { handleCoordinates ->
363 val handlePosition = endSelectable.getHandlePosition(selection, isStartHandle = false)
364 val position = containerCoordinates.localPositionOf(handleCoordinates, handlePosition)
365 position.takeIf {
366 draggingHandle == Handle.SelectionEnd || visibleBounds.containsInclusive(it)
367 }
Halil Ozercanb2486f12023-01-09 07:40:22 +0000368 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100369 }
370
371 /**
372 * Returns non-nullable [containerLayoutCoordinates].
373 */
374 internal fun requireContainerCoordinates(): LayoutCoordinates {
375 val coordinates = containerLayoutCoordinates
Ralston Da Silvaf24ae422023-05-30 12:59:24 -0700376 requireNotNull(coordinates) { "null coordinates" }
377 require(coordinates.isAttached) { "unattached coordinates" }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100378 return coordinates
379 }
380
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700381 internal fun selectAll(
382 selectableId: Long,
383 previousSelection: Selection?
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700384 ): Pair<Selection?, Map<Long, Selection>> {
385 val subselections = mutableMapOf<Long, Selection>()
386 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
387 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
388 val selection = if (selectable.selectableId == selectableId)
389 selectable.getSelectAllSelection() else null
390 selection?.let { subselections[selectable.selectableId] = it }
391 merge(mergedSelection, selection)
392 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800393 if (isInTouchMode && newSelection != previousSelection) {
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700394 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
395 }
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700396 return Pair(newSelection, subselections)
397 }
398
Grant Toepferd681b5c2023-06-14 11:24:02 -0700399 /**
400 * Returns whether the start and end anchors are equal.
401 *
402 * It is possible that this returns true, but the selection is still empty because it has
403 * multiple collapsed selections across multiple selectables. To test for that case, use
404 * [isNonEmptySelection].
405 */
406 internal fun isTriviallyCollapsedSelection(): Boolean {
407 val selection = selection ?: return true
408 return selection.start == selection.end
409 }
410
411 /**
412 * Returns whether the selection selects zero characters.
413 *
414 * It is possible that the selection anchors are different but still result in a zero-width
415 * selection. In this case, you may want to still show the selection anchors, but not allow for
416 * a user to try and copy zero characters. To test for whether the anchors are equal, use
417 * [isTriviallyCollapsedSelection].
418 */
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800419 internal fun isNonEmptySelection(): Boolean {
420 val selection = selection ?: return false
Grant Toepferd681b5c2023-06-14 11:24:02 -0700421 if (selection.start == selection.end) {
422 return false
423 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800424
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700425 if (selection.start.selectableId == selection.end.selectableId) {
426 // Selection is in the same selectable, but not the same anchors,
427 // so there must be some selected text.
428 return true
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800429 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700430
431 // All subselections associated with a selectable must be an empty selection.
432 return selectionRegistrar.sort(requireContainerCoordinates()).fastAny { selectable ->
433 selectionRegistrar.subselections[selectable.selectableId]
434 ?.run { start.offset != end.offset }
435 ?: false
436 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800437 }
438
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800439 internal fun getSelectedText(): AnnotatedString? {
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700440 if (selection == null || selectionRegistrar.subselections.isEmpty()) {
441 return null
442 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800443
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700444 return buildAnnotatedString {
445 selectionRegistrar.sort(requireContainerCoordinates()).fastForEach { selectable ->
446 selectionRegistrar.subselections[selectable.selectableId]?.let { subSelection ->
447 val currentText = selectable.getText()
448 val currentSelectedText = if (subSelection.handlesCrossed) {
449 currentText.subSequence(
450 subSelection.end.offset,
451 subSelection.start.offset
452 )
453 } else {
454 currentText.subSequence(
455 subSelection.start.offset,
456 subSelection.end.offset
457 )
458 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800459
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700460 append(currentSelectedText)
461 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800462 }
463 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800464 }
465
Qingqing Dengde023cc2020-04-24 14:23:41 -0700466 internal fun copy() {
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700467 getSelectedText()?.takeIf { it.isNotEmpty() }?.let { clipboardManager?.setText(it) }
Qingqing Dengde023cc2020-04-24 14:23:41 -0700468 }
469
470 /**
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800471 * Whether toolbar should be shown right now.
472 * Examples: Show toolbar after user finishes selection.
473 * Hide it during selection.
474 * Hide it when no selection exists.
475 */
476 internal var showToolbar = false
477 internal set(value) {
478 field = value
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700479 updateSelectionToolbar()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800480 }
481
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700482 private fun toolbarCopy() {
483 copy()
484 onRelease()
485 }
486
487 private fun updateSelectionToolbar() {
488 if (!hasFocus) {
489 return
490 }
491
492 val textToolbar = textToolbar ?: return
493 if (showToolbar && isInTouchMode && isNonEmptySelection()) {
494 val rect = getContentRect() ?: return
495 textToolbar.showMenu(rect = rect, onCopyRequested = ::toolbarCopy)
496 } else if (textToolbar.status == TextToolbarStatus.Shown) {
497 textToolbar.hide()
498 }
499 }
500
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800501 /**
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700502 * Calculate selected region as [Rect].
503 * The result is the smallest [Rect] that encapsulates the entire selection,
504 * coerced into visible bounds.
Qingqing Dengde023cc2020-04-24 14:23:41 -0700505 */
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700506 private fun getContentRect(): Rect? {
507 selection ?: return null
508 val containerCoordinates = containerLayoutCoordinates ?: return null
509 if (!containerCoordinates.isAttached) return null
510 val visibleBounds = containerCoordinates.visibleBounds()
511
512 var anyExists = false
513 var rootLeft = Float.POSITIVE_INFINITY
514 var rootTop = Float.POSITIVE_INFINITY
515 var rootRight = Float.NEGATIVE_INFINITY
516 var rootBottom = Float.NEGATIVE_INFINITY
517
518 val sortedSelectables = selectionRegistrar.sort(requireContainerCoordinates())
519 .fastFilter {
520 it.selectableId in selectionRegistrar.subselections
521 }
522
523 if (sortedSelectables.isEmpty()) {
524 return null
525 }
526
527 val selectedSelectables = if (sortedSelectables.size == 1) {
528 sortedSelectables
529 } else {
530 listOf(sortedSelectables.first(), sortedSelectables.last())
531 }
532
533 selectedSelectables.fastForEach { selectable ->
534 val subSelection = selectionRegistrar.subselections[selectable.selectableId]
535 ?: return@fastForEach
536
537 val coordinates = selectable.getLayoutCoordinates()
538 ?: return@fastForEach
539
540 with(subSelection) {
541 if (start.offset == end.offset) {
542 return@fastForEach
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800543 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700544
545 val minOffset = minOf(start.offset, end.offset)
546 val maxOffset = maxOf(start.offset, end.offset)
547
548 var left = Float.POSITIVE_INFINITY
549 var top = Float.POSITIVE_INFINITY
550 var right = Float.NEGATIVE_INFINITY
551 var bottom = Float.NEGATIVE_INFINITY
552 for (i in intArrayOf(minOffset, maxOffset)) {
553 val rect = selectable.getBoundingBox(i)
554 left = minOf(left, rect.left)
555 top = minOf(top, rect.top)
556 right = maxOf(right, rect.right)
557 bottom = maxOf(bottom, rect.bottom)
558 }
559
560 val localTopLeft = Offset(left, top)
561 val localBottomRight = Offset(right, bottom)
562
563 val containerTopLeft =
564 containerCoordinates.localPositionOf(coordinates, localTopLeft)
565 val containerBottomRight =
566 containerCoordinates.localPositionOf(coordinates, localBottomRight)
567
568 val rootVisibleTopLeft =
569 containerCoordinates.localToRoot(containerTopLeft.coerceIn(visibleBounds))
570 val rootVisibleBottomRight =
571 containerCoordinates.localToRoot(containerBottomRight.coerceIn(visibleBounds))
572
573 rootLeft = minOf(rootLeft, rootVisibleTopLeft.x)
574 rootTop = minOf(rootTop, rootVisibleTopLeft.y)
575 rootRight = maxOf(rootRight, rootVisibleBottomRight.x)
576 rootBottom = maxOf(rootBottom, rootVisibleBottomRight.y)
577 anyExists = true
578 }
Qingqing Denga89450d2020-04-03 19:11:49 -0700579 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700580
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700581 if (!anyExists) {
582 return null
Qingqing Dengde023cc2020-04-24 14:23:41 -0700583 }
Qingqing Dengde023cc2020-04-24 14:23:41 -0700584
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700585 rootBottom += HandleHeight.value * 4
586 return Rect(rootLeft, rootTop, rootRight, rootBottom)
Qingqing Dengde023cc2020-04-24 14:23:41 -0700587 }
588
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800589 // This is for PressGestureDetector to cancel the selection.
590 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800591 selectionRegistrar.subselections = emptyMap()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800592 showToolbar = false
haoyue04245e2021-03-08 14:52:56 -0800593 if (selection != null) {
594 onSelectionChange(null)
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800595 if (isInTouchMode) {
596 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
597 }
haoyue04245e2021-03-08 14:52:56 -0800598 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800599 }
600
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800601 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver = object : TextDragObserver {
602 override fun onDown(point: Offset) {
Zach Klippenstein63870892022-01-14 12:45:18 -0800603 val selection = selection ?: return
604 val anchor = if (isStartHandle) selection.start else selection.end
605 val selectable = getAnchorSelectable(anchor) ?: return
606 // The LayoutCoordinates of the composable where the drag gesture should begin. This
607 // is used to convert the position of the beginning of the drag gesture from the
608 // composable coordinates to selection container coordinates.
609 val beginLayoutCoordinates = selectable.getLayoutCoordinates() ?: return
610
611 // The position of the character where the drag gesture should begin. This is in
612 // the composable coordinates.
613 val beginCoordinates = getAdjustedCoordinates(
614 selectable.getHandlePosition(
615 selection = selection, isStartHandle = isStartHandle
616 )
617 )
618
619 // Convert the position where drag gesture begins from composable coordinates to
620 // selection container coordinates.
621 currentDragPosition = requireContainerCoordinates().localPositionOf(
622 beginLayoutCoordinates,
623 beginCoordinates
624 )
625 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800626 showToolbar = false
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800627 }
628
629 override fun onStart(startPoint: Offset) {
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800630 val selection = selection!!
631 val startSelectable =
632 selectionRegistrar.selectableMap[selection.start.selectableId]
633 val endSelectable =
634 selectionRegistrar.selectableMap[selection.end.selectableId]
635 // The LayoutCoordinates of the composable where the drag gesture should begin. This
636 // is used to convert the position of the beginning of the drag gesture from the
637 // composable coordinates to selection container coordinates.
638 val beginLayoutCoordinates = if (isStartHandle) {
639 startSelectable?.getLayoutCoordinates()!!
640 } else {
641 endSelectable?.getLayoutCoordinates()!!
642 }
643
644 // The position of the character where the drag gesture should begin. This is in
645 // the composable coordinates.
646 val beginCoordinates = getAdjustedCoordinates(
647 if (isStartHandle) {
648 startSelectable!!.getHandlePosition(
649 selection = selection, isStartHandle = true
650 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700651 } else {
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800652 endSelectable!!.getHandlePosition(
653 selection = selection, isStartHandle = false
654 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700655 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800656 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700657
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800658 // Convert the position where drag gesture begins from composable coordinates to
659 // selection container coordinates.
660 dragBeginPosition = requireContainerCoordinates().localPositionOf(
661 beginLayoutCoordinates,
662 beginCoordinates
663 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700664
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800665 // Zero out the total distance that being dragged.
666 dragTotalDistance = Offset.Zero
667 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700668
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800669 override fun onDrag(delta: Offset) {
670 dragTotalDistance += delta
671 val endPosition = dragBeginPosition + dragTotalDistance
672 val consumed = updateSelection(
673 newPosition = endPosition,
674 previousPosition = dragBeginPosition,
675 isStartHandle = isStartHandle,
676 adjustment = SelectionAdjustment.CharacterWithWordAccelerate
677 )
678 if (consumed) {
679 dragBeginPosition = endPosition
Nader Jawad6df06122020-06-03 15:27:08 -0700680 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700681 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800682 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700683
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800684 private fun done() {
685 showToolbar = true
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800686 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800687 currentDragPosition = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800688 }
haoyue6d80a12020-12-02 16:04:52 -0800689
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800690 override fun onUp() = done()
691 override fun onStop() = done()
692 override fun onCancel() = done()
Qingqing Deng6f56a912019-05-13 10:10:37 -0700693 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700694
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700695 /**
696 * Detect tap without consuming the up event.
697 */
698 private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
George Mount32de9dd2022-10-05 14:51:06 -0700699 awaitEachGesture {
700 waitForUpOrCancellation()?.let {
701 onTap(it.position)
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700702 }
703 }
704 }
705
706 private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
707 return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
708 }
709
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700710 private fun convertToContainerCoordinates(
711 layoutCoordinates: LayoutCoordinates,
712 offset: Offset
Grant Toepferd681b5c2023-06-14 11:24:02 -0700713 ): Offset {
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700714 val coordinates = containerLayoutCoordinates
Grant Toepferd681b5c2023-06-14 11:24:02 -0700715 if (coordinates == null || !coordinates.isAttached) return Offset.Unspecified
George Mount77ca2a22020-12-11 17:46:19 +0000716 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700717 }
718
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700719 /**
720 * Cancel the previous selection and start a new selection at the given [position].
721 * It's used for long-press, double-click, triple-click and so on to start selection.
722 *
723 * @param position initial position of the selection. Both start and end handle is considered
724 * at this position.
725 * @param isStartHandle whether it's considered as the start handle moving. This parameter
726 * will influence the [SelectionAdjustment]'s behavior. For example,
727 * [SelectionAdjustment.Character] only adjust the moving handle.
728 * @param adjustment the selection adjustment.
729 */
730 private fun startSelection(
731 position: Offset,
732 isStartHandle: Boolean,
733 adjustment: SelectionAdjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700734 ) {
Grant Toepferd681b5c2023-06-14 11:24:02 -0700735 previousSelectionLayout = null
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700736 updateSelection(
737 startHandlePosition = position,
738 endHandlePosition = position,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700739 previousHandlePosition = Offset.Unspecified,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700740 isStartHandle = isStartHandle,
741 adjustment = adjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700742 )
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700743 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700744
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700745 /**
746 * Updates the selection after one of the selection handle moved.
747 *
748 * @param newPosition the new position of the moving selection handle.
749 * @param previousPosition the previous position of the moving selection handle.
750 * @param isStartHandle whether the moving selection handle is the start handle.
751 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
752 * produce the final selection range.
753 *
754 * @return a boolean representing whether the movement is consumed.
755 *
756 * @see SelectionAdjustment
757 */
758 internal fun updateSelection(
759 newPosition: Offset?,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700760 previousPosition: Offset,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700761 isStartHandle: Boolean,
762 adjustment: SelectionAdjustment,
763 ): Boolean {
764 if (newPosition == null) return false
765 val otherHandlePosition = selection?.let { selection ->
766 val otherSelectableId = if (isStartHandle) {
767 selection.end.selectableId
768 } else {
769 selection.start.selectableId
770 }
771 val otherSelectable =
772 selectionRegistrar.selectableMap[otherSelectableId] ?: return@let null
773 convertToContainerCoordinates(
774 otherSelectable.getLayoutCoordinates()!!,
775 getAdjustedCoordinates(
776 otherSelectable.getHandlePosition(selection, !isStartHandle)
777 )
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700778 )
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700779 } ?: return false
780
781 val startHandlePosition = if (isStartHandle) newPosition else otherHandlePosition
782 val endHandlePosition = if (isStartHandle) otherHandlePosition else newPosition
783
784 return updateSelection(
785 startHandlePosition = startHandlePosition,
786 endHandlePosition = endHandlePosition,
787 previousHandlePosition = previousPosition,
788 isStartHandle = isStartHandle,
789 adjustment = adjustment
790 )
791 }
792
793 /**
794 * Updates the selection after one of the selection handle moved.
795 *
796 * To make sure that [SelectionAdjustment] works correctly, it's expected that only one
797 * selection handle is updated each time. The only exception is that when a new selection is
798 * started. In this case, [previousHandlePosition] is always null.
799 *
800 * @param startHandlePosition the position of the start selection handle.
801 * @param endHandlePosition the position of the end selection handle.
802 * @param previousHandlePosition the position of the moving handle before the update.
803 * @param isStartHandle whether the moving selection handle is the start handle.
804 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
805 * produce the final selection range.
806 *
807 * @return a boolean representing whether the movement is consumed. It's useful for the case
808 * where a selection handle is updating consecutively. When the return value is true, it's
809 * expected that the caller will update the [startHandlePosition] to be the given
810 * [endHandlePosition] in following calls.
811 *
812 * @see SelectionAdjustment
813 */
814 internal fun updateSelection(
815 startHandlePosition: Offset,
816 endHandlePosition: Offset,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700817 previousHandlePosition: Offset,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700818 isStartHandle: Boolean,
819 adjustment: SelectionAdjustment,
820 ): Boolean {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800821 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Zach Klippenstein63870892022-01-14 12:45:18 -0800822 currentDragPosition = if (isStartHandle) startHandlePosition else endHandlePosition
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700823
Grant Toepferd681b5c2023-06-14 11:24:02 -0700824 val selectionLayout = getSelectionLayout(
825 startHandlePosition = startHandlePosition,
826 endHandlePosition = endHandlePosition,
827 previousHandlePosition = previousHandlePosition,
828 isStartHandle = isStartHandle,
829 )
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800830
Grant Toepferd681b5c2023-06-14 11:24:02 -0700831 if (!selectionLayout.shouldRecomputeSelection(previousSelectionLayout)) {
832 return false
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700833 }
Grant Toepferd681b5c2023-06-14 11:24:02 -0700834
835 val newSelection = adjustment.adjust(selectionLayout)
836 if (newSelection != selection) {
837 selectionChanged(selectionLayout, newSelection)
838 }
839 previousSelectionLayout = selectionLayout
840 return true
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700841 }
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200842
Grant Toepferd681b5c2023-06-14 11:24:02 -0700843 private fun getSelectionLayout(
844 startHandlePosition: Offset,
845 endHandlePosition: Offset,
846 previousHandlePosition: Offset,
847 isStartHandle: Boolean,
848 ): SelectionLayout {
849 val containerCoordinates = requireContainerCoordinates()
850 val sortedSelectables = selectionRegistrar.sort(containerCoordinates)
851
852 val idToIndexMap = mutableMapOf<Long, Int>()
853 sortedSelectables.fastForEachIndexed { index, selectable ->
854 idToIndexMap[selectable.selectableId] = index
855 }
856
857 val selectableIdOrderingComparator = compareBy<Long> { idToIndexMap[it] }
858
859 // if previous handle is null, then treat this as a new selection.
860 val previousSelection = if (previousHandlePosition.isUnspecified) null else selection
861 val builder = SelectionLayoutBuilder(
862 startHandlePosition = startHandlePosition,
863 endHandlePosition = endHandlePosition,
864 previousHandlePosition = previousHandlePosition,
865 containerCoordinates = containerCoordinates,
866 isStartHandle = isStartHandle,
867 previousSelection = previousSelection,
868 selectableIdOrderingComparator = selectableIdOrderingComparator,
869 )
870
871 sortedSelectables.fastForEach {
872 it.appendSelectableInfoToBuilder(builder)
873 }
874
875 return builder.build()
876 }
877
878 private fun selectionChanged(selectionLayout: SelectionLayout, newSelection: Selection) {
879 if (shouldPerformHaptics()) {
880 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
881 }
882 selectionRegistrar.subselections = selectionLayout.createSubSelections(newSelection)
883 onSelectionChange(newSelection)
884 }
885
886 @VisibleForTesting
887 internal fun shouldPerformHaptics(): Boolean =
888 isInTouchMode && selectionRegistrar.selectables.fastAny { it.getText().isNotEmpty() }
889
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200890 fun contextMenuOpenAdjustment(position: Offset) {
891 val isEmptySelection = selection?.toTextRange()?.collapsed ?: true
892 // TODO(b/209483184) the logic should be more complex here, it should check that current
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800893 // selection doesn't include click position
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200894 if (isEmptySelection) {
895 startSelection(
896 position = position,
897 isStartHandle = true,
898 adjustment = SelectionAdjustment.Word
899 )
900 }
901 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700902}
903
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200904internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700905 return lhs?.merge(rhs) ?: rhs
906}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800907
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100908internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
909
Zach Klippensteinadabe342021-11-11 16:38:13 -0800910internal expect fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier
911
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -0800912internal fun calculateSelectionMagnifierCenterAndroid(
913 manager: SelectionManager,
914 magnifierSize: IntSize
915): Offset {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800916 val selection = manager.selection ?: return Offset.Unspecified
917 return when (manager.draggingHandle) {
918 null -> return Offset.Unspecified
Grant Toepferd681b5c2023-06-14 11:24:02 -0700919 Handle.SelectionStart -> getMagnifierCenter(manager, magnifierSize, selection.start)
920 Handle.SelectionEnd -> getMagnifierCenter(manager, magnifierSize, selection.end)
Zach Klippensteinadabe342021-11-11 16:38:13 -0800921 Handle.Cursor -> error("SelectionContainer does not support cursor")
922 }
923}
924
Grant Toepferd681b5c2023-06-14 11:24:02 -0700925private fun getMagnifierCenter(
926 manager: SelectionManager,
927 magnifierSize: IntSize,
928 anchor: AnchorInfo
929): Offset {
930 val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
931 val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
932 val selectableCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified
933 val offset = anchor.offset
934
935 if (offset > selectable.getLastVisibleOffset()) return Offset.Unspecified
936
937 // The horizontal position doesn't snap to cursor positions but should directly track the
938 // actual drag.
939 val localDragPosition = selectableCoordinates.localPositionOf(
940 containerCoordinates,
941 manager.currentDragPosition!!
942 )
943 val dragX = localDragPosition.x
944
945 // But it is constrained by the horizontal bounds of the current line.
946 val lineRange = selectable.getRangeOfLineContaining(offset)
947 val textConstrainedX = if (lineRange.collapsed) {
948 // A collapsed range implies the text is empty.
949 // line left and right are equal for this offset, so use either
950 selectable.getLineLeft(offset)
951 } else {
952 val lineStartX = selectable.getLineLeft(lineRange.start)
953 val lineEndX = selectable.getLineRight(lineRange.end - 1)
954 // in RTL/BiDi, lineStartX may be larger than lineEndX
955 val minX = minOf(lineStartX, lineEndX)
956 val maxX = maxOf(lineStartX, lineEndX)
957 dragX.coerceIn(minX, maxX)
958 }
959
960 // selectable couldn't determine horizontals
961 if (textConstrainedX == -1f) return Offset.Unspecified
962
963 // Hide the magnifier when dragged too far (outside the horizontal bounds of how big the
964 // magnifier actually is). See
965 // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Editor.java;l=5228-5231;drc=2fdb6bd709be078b72f011334362456bb758922c
966 if ((dragX - textConstrainedX).absoluteValue > magnifierSize.width / 2) {
967 return Offset.Unspecified
968 }
969
970 val lineCenterY = selectable.getCenterYForOffset(offset)
971
972 // selectable couldn't determine the line center
973 if (lineCenterY == -1f) return Offset.Unspecified
974
975 return containerCoordinates.localPositionOf(
976 sourceCoordinates = selectableCoordinates,
977 relativeToSource = Offset(textConstrainedX, lineCenterY)
978 )
979}
980
haoyue2678c62020-12-09 08:39:12 -0800981/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800982internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800983 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
984 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
985 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000986 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800987 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000988 windowToLocal(boundsInWindow.topLeft),
989 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800990 )
991}
992
haoyuac341f02021-01-22 22:01:56 -0800993internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800994 offset.x in left..right && offset.y in top..bottom