[go: nahoru, domu]

blob: 9ed470a9c038ec50b30b7d53e3784a49a60038ac [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 Toepferc7d1a4e2023-09-18 23:06:37 +0000232 { isInTouchMode, layoutCoordinates, rawPosition, selectionMode ->
233 val textRect = with(layoutCoordinates.size) {
234 Rect(0f, 0f, width.toFloat(), height.toFloat())
235 }
236
237 val position = if (textRect.containsInclusive(rawPosition)) {
238 rawPosition
239 } else {
240 rawPosition.coerceIn(textRect)
241 }
242
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700243 val positionInContainer = convertToContainerCoordinates(
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100244 layoutCoordinates,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700245 position
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100246 )
247
Grant Toepferd681b5c2023-06-14 11:24:02 -0700248 if (positionInContainer.isSpecified) {
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800249 this.isInTouchMode = isInTouchMode
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700250 startSelection(
251 position = positionInContainer,
252 isStartHandle = false,
253 adjustment = selectionMode
254 )
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100255
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700256 focusRequester.requestFocus()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800257 showToolbar = false
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700258 }
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100259 }
haoyue6d80a12020-12-02 16:04:52 -0800260
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700261 selectionRegistrar.onSelectionUpdateSelectAll =
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800262 { isInTouchMode, selectableId ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700263 val (newSelection, newSubselection) = selectAll(
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700264 selectableId = selectableId,
265 previousSelection = selection,
266 )
267 if (newSelection != selection) {
268 selectionRegistrar.subselections = newSubselection
269 onSelectionChange(newSelection)
270 }
271
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800272 this.isInTouchMode = isInTouchMode
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700273 focusRequester.requestFocus()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800274 showToolbar = false
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700275 }
276
haoyue6d80a12020-12-02 16:04:52 -0800277 selectionRegistrar.onSelectionUpdateCallback =
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800278 { isInTouchMode,
279 layoutCoordinates,
280 newPosition,
281 previousPosition,
282 isStartHandle,
283 selectionMode ->
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700284 val newPositionInContainer =
285 convertToContainerCoordinates(layoutCoordinates, newPosition)
286 val previousPositionInContainer =
287 convertToContainerCoordinates(layoutCoordinates, previousPosition)
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100288
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800289 this.isInTouchMode = isInTouchMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700290 updateSelection(
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700291 newPosition = newPositionInContainer,
292 previousPosition = previousPositionInContainer,
293 isStartHandle = isStartHandle,
Haoyu Zhang4c6c6372021-04-20 10:25:21 -0700294 adjustment = selectionMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700295 )
296 }
haoyue6d80a12020-12-02 16:04:52 -0800297
298 selectionRegistrar.onSelectionUpdateEndCallback = {
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800299 showToolbar = true
Zach Klippensteinadabe342021-11-11 16:38:13 -0800300 // This property is set by updateSelection while dragging, so we need to clear it after
301 // the original selection drag.
302 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800303 currentDragPosition = null
haoyue6d80a12020-12-02 16:04:52 -0800304 }
haoyu9085c882020-12-08 12:01:06 -0800305
haoyue04245e2021-03-08 14:52:56 -0800306 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
307 if (selectableKey in selectionRegistrar.subselections) {
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700308 // Clear the selection range of each Selectable.
haoyu9085c882020-12-08 12:01:06 -0800309 onRelease()
310 selection = null
311 }
312 }
haoyue04245e2021-03-08 14:52:56 -0800313
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700314 selectionRegistrar.afterSelectableUnsubscribe = { selectableId ->
315 if (selectableId == selection?.start?.selectableId) {
haoyue04245e2021-03-08 14:52:56 -0800316 // The selectable that contains a selection handle just unsubscribed.
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700317 // Hide the associated selection handle
haoyue04245e2021-03-08 14:52:56 -0800318 startHandlePosition = null
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700319 }
320 if (selectableId == selection?.end?.selectableId) {
haoyue04245e2021-03-08 14:52:56 -0800321 endHandlePosition = null
322 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700323
324 if (selectableId in selectionRegistrar.subselections) {
325 // Unsubscribing the selectable may make the selection empty, which would hide it.
326 updateSelectionToolbar()
327 }
haoyue04245e2021-03-08 14:52:56 -0800328 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100329 }
330
Zach Klippensteinadabe342021-11-11 16:38:13 -0800331 /**
332 * Returns the [Selectable] responsible for managing the given [AnchorInfo], or null if the
333 * anchor is not from a currently-registered [Selectable].
334 */
335 internal fun getAnchorSelectable(anchor: AnchorInfo): Selectable? {
336 return selectionRegistrar.selectableMap[anchor.selectableId]
Andrey Rudenkod6c91be2021-03-18 15:45:20 +0100337 }
338
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100339 private fun updateHandleOffsets() {
340 val selection = selection
341 val containerCoordinates = containerLayoutCoordinates
Zach Klippensteinadabe342021-11-11 16:38:13 -0800342 val startSelectable = selection?.start?.let(::getAnchorSelectable)
343 val endSelectable = selection?.end?.let(::getAnchorSelectable)
haoyue04245e2021-03-08 14:52:56 -0800344 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
345 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700346
haoyue2678c62020-12-09 08:39:12 -0800347 if (
348 selection == null ||
349 containerCoordinates == null ||
350 !containerCoordinates.isAttached ||
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700351 (startLayoutCoordinates == null && endLayoutCoordinates == null)
haoyue2678c62020-12-09 08:39:12 -0800352 ) {
353 this.startHandlePosition = null
354 this.endHandlePosition = null
355 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100356 }
haoyue2678c62020-12-09 08:39:12 -0800357
haoyue2678c62020-12-09 08:39:12 -0800358 val visibleBounds = containerCoordinates.visibleBounds()
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700359 this.startHandlePosition = startLayoutCoordinates?.let { handleCoordinates ->
360 // Set the new handle position only if the handle is in visible bounds or
361 // the handle is still dragging. If handle goes out of visible bounds during drag,
362 // handle popup is also removed from composition, halting the drag gesture. This
363 // affects multiple text selection when selected text is configured with maxLines=1
364 // and overflow=clip.
365 val handlePosition = startSelectable.getHandlePosition(selection, isStartHandle = true)
366 val position = containerCoordinates.localPositionOf(handleCoordinates, handlePosition)
367 position.takeIf {
368 draggingHandle == Handle.SelectionStart || visibleBounds.containsInclusive(it)
369 }
Halil Ozercanb2486f12023-01-09 07:40:22 +0000370 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700371
372 this.endHandlePosition = endLayoutCoordinates?.let { handleCoordinates ->
373 val handlePosition = endSelectable.getHandlePosition(selection, isStartHandle = false)
374 val position = containerCoordinates.localPositionOf(handleCoordinates, handlePosition)
375 position.takeIf {
376 draggingHandle == Handle.SelectionEnd || visibleBounds.containsInclusive(it)
377 }
Halil Ozercanb2486f12023-01-09 07:40:22 +0000378 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100379 }
380
381 /**
382 * Returns non-nullable [containerLayoutCoordinates].
383 */
384 internal fun requireContainerCoordinates(): LayoutCoordinates {
385 val coordinates = containerLayoutCoordinates
Ralston Da Silvaf24ae422023-05-30 12:59:24 -0700386 requireNotNull(coordinates) { "null coordinates" }
387 require(coordinates.isAttached) { "unattached coordinates" }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100388 return coordinates
389 }
390
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700391 internal fun selectAll(
392 selectableId: Long,
393 previousSelection: Selection?
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700394 ): Pair<Selection?, Map<Long, Selection>> {
395 val subselections = mutableMapOf<Long, Selection>()
396 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
397 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
398 val selection = if (selectable.selectableId == selectableId)
399 selectable.getSelectAllSelection() else null
400 selection?.let { subselections[selectable.selectableId] = it }
401 merge(mergedSelection, selection)
402 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800403 if (isInTouchMode && newSelection != previousSelection) {
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700404 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
405 }
Qingqing Deng6bc981c2021-05-11 22:43:51 -0700406 return Pair(newSelection, subselections)
407 }
408
Grant Toepferd681b5c2023-06-14 11:24:02 -0700409 /**
410 * Returns whether the start and end anchors are equal.
411 *
412 * It is possible that this returns true, but the selection is still empty because it has
413 * multiple collapsed selections across multiple selectables. To test for that case, use
414 * [isNonEmptySelection].
415 */
416 internal fun isTriviallyCollapsedSelection(): Boolean {
417 val selection = selection ?: return true
418 return selection.start == selection.end
419 }
420
421 /**
422 * Returns whether the selection selects zero characters.
423 *
424 * It is possible that the selection anchors are different but still result in a zero-width
425 * selection. In this case, you may want to still show the selection anchors, but not allow for
426 * a user to try and copy zero characters. To test for whether the anchors are equal, use
427 * [isTriviallyCollapsedSelection].
428 */
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800429 internal fun isNonEmptySelection(): Boolean {
430 val selection = selection ?: return false
Grant Toepferd681b5c2023-06-14 11:24:02 -0700431 if (selection.start == selection.end) {
432 return false
433 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800434
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700435 if (selection.start.selectableId == selection.end.selectableId) {
436 // Selection is in the same selectable, but not the same anchors,
437 // so there must be some selected text.
438 return true
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800439 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700440
441 // All subselections associated with a selectable must be an empty selection.
442 return selectionRegistrar.sort(requireContainerCoordinates()).fastAny { selectable ->
443 selectionRegistrar.subselections[selectable.selectableId]
444 ?.run { start.offset != end.offset }
445 ?: false
446 }
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800447 }
448
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800449 internal fun getSelectedText(): AnnotatedString? {
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700450 if (selection == null || selectionRegistrar.subselections.isEmpty()) {
451 return null
452 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800453
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700454 return buildAnnotatedString {
455 selectionRegistrar.sort(requireContainerCoordinates()).fastForEach { selectable ->
456 selectionRegistrar.subselections[selectable.selectableId]?.let { subSelection ->
457 val currentText = selectable.getText()
458 val currentSelectedText = if (subSelection.handlesCrossed) {
459 currentText.subSequence(
460 subSelection.end.offset,
461 subSelection.start.offset
462 )
463 } else {
464 currentText.subSequence(
465 subSelection.start.offset,
466 subSelection.end.offset
467 )
468 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800469
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700470 append(currentSelectedText)
471 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800472 }
473 }
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800474 }
475
Qingqing Dengde023cc2020-04-24 14:23:41 -0700476 internal fun copy() {
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700477 getSelectedText()?.takeIf { it.isNotEmpty() }?.let { clipboardManager?.setText(it) }
Qingqing Dengde023cc2020-04-24 14:23:41 -0700478 }
479
480 /**
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800481 * Whether toolbar should be shown right now.
482 * Examples: Show toolbar after user finishes selection.
483 * Hide it during selection.
484 * Hide it when no selection exists.
485 */
486 internal var showToolbar = false
487 internal set(value) {
488 field = value
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700489 updateSelectionToolbar()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800490 }
491
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700492 private fun toolbarCopy() {
493 copy()
494 onRelease()
495 }
496
497 private fun updateSelectionToolbar() {
498 if (!hasFocus) {
499 return
500 }
501
502 val textToolbar = textToolbar ?: return
503 if (showToolbar && isInTouchMode && isNonEmptySelection()) {
504 val rect = getContentRect() ?: return
505 textToolbar.showMenu(rect = rect, onCopyRequested = ::toolbarCopy)
506 } else if (textToolbar.status == TextToolbarStatus.Shown) {
507 textToolbar.hide()
508 }
509 }
510
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800511 /**
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700512 * Calculate selected region as [Rect].
513 * The result is the smallest [Rect] that encapsulates the entire selection,
514 * coerced into visible bounds.
Qingqing Dengde023cc2020-04-24 14:23:41 -0700515 */
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700516 private fun getContentRect(): Rect? {
517 selection ?: return null
518 val containerCoordinates = containerLayoutCoordinates ?: return null
519 if (!containerCoordinates.isAttached) return null
520 val visibleBounds = containerCoordinates.visibleBounds()
521
522 var anyExists = false
523 var rootLeft = Float.POSITIVE_INFINITY
524 var rootTop = Float.POSITIVE_INFINITY
525 var rootRight = Float.NEGATIVE_INFINITY
526 var rootBottom = Float.NEGATIVE_INFINITY
527
528 val sortedSelectables = selectionRegistrar.sort(requireContainerCoordinates())
529 .fastFilter {
530 it.selectableId in selectionRegistrar.subselections
531 }
532
533 if (sortedSelectables.isEmpty()) {
534 return null
535 }
536
537 val selectedSelectables = if (sortedSelectables.size == 1) {
538 sortedSelectables
539 } else {
540 listOf(sortedSelectables.first(), sortedSelectables.last())
541 }
542
543 selectedSelectables.fastForEach { selectable ->
544 val subSelection = selectionRegistrar.subselections[selectable.selectableId]
545 ?: return@fastForEach
546
547 val coordinates = selectable.getLayoutCoordinates()
548 ?: return@fastForEach
549
550 with(subSelection) {
551 if (start.offset == end.offset) {
552 return@fastForEach
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800553 }
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700554
555 val minOffset = minOf(start.offset, end.offset)
556 val maxOffset = maxOf(start.offset, end.offset)
557
558 var left = Float.POSITIVE_INFINITY
559 var top = Float.POSITIVE_INFINITY
560 var right = Float.NEGATIVE_INFINITY
561 var bottom = Float.NEGATIVE_INFINITY
562 for (i in intArrayOf(minOffset, maxOffset)) {
563 val rect = selectable.getBoundingBox(i)
564 left = minOf(left, rect.left)
565 top = minOf(top, rect.top)
566 right = maxOf(right, rect.right)
567 bottom = maxOf(bottom, rect.bottom)
568 }
569
570 val localTopLeft = Offset(left, top)
571 val localBottomRight = Offset(right, bottom)
572
573 val containerTopLeft =
574 containerCoordinates.localPositionOf(coordinates, localTopLeft)
575 val containerBottomRight =
576 containerCoordinates.localPositionOf(coordinates, localBottomRight)
577
578 val rootVisibleTopLeft =
579 containerCoordinates.localToRoot(containerTopLeft.coerceIn(visibleBounds))
580 val rootVisibleBottomRight =
581 containerCoordinates.localToRoot(containerBottomRight.coerceIn(visibleBounds))
582
583 rootLeft = minOf(rootLeft, rootVisibleTopLeft.x)
584 rootTop = minOf(rootTop, rootVisibleTopLeft.y)
585 rootRight = maxOf(rootRight, rootVisibleBottomRight.x)
586 rootBottom = maxOf(rootBottom, rootVisibleBottomRight.y)
587 anyExists = true
588 }
Qingqing Denga89450d2020-04-03 19:11:49 -0700589 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700590
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700591 if (!anyExists) {
592 return null
Qingqing Dengde023cc2020-04-24 14:23:41 -0700593 }
Qingqing Dengde023cc2020-04-24 14:23:41 -0700594
Grant Toepfer427dbdf2023-09-05 18:38:42 -0700595 rootBottom += HandleHeight.value * 4
596 return Rect(rootLeft, rootTop, rootRight, rootBottom)
Qingqing Dengde023cc2020-04-24 14:23:41 -0700597 }
598
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800599 // This is for PressGestureDetector to cancel the selection.
600 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800601 selectionRegistrar.subselections = emptyMap()
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800602 showToolbar = false
haoyue04245e2021-03-08 14:52:56 -0800603 if (selection != null) {
604 onSelectionChange(null)
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800605 if (isInTouchMode) {
606 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
607 }
haoyue04245e2021-03-08 14:52:56 -0800608 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800609 }
610
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800611 fun handleDragObserver(isStartHandle: Boolean): TextDragObserver = object : TextDragObserver {
612 override fun onDown(point: Offset) {
Zach Klippenstein63870892022-01-14 12:45:18 -0800613 val selection = selection ?: return
614 val anchor = if (isStartHandle) selection.start else selection.end
615 val selectable = getAnchorSelectable(anchor) ?: return
616 // The LayoutCoordinates of the composable where the drag gesture should begin. This
617 // is used to convert the position of the beginning of the drag gesture from the
618 // composable coordinates to selection container coordinates.
619 val beginLayoutCoordinates = selectable.getLayoutCoordinates() ?: return
620
621 // The position of the character where the drag gesture should begin. This is in
622 // the composable coordinates.
623 val beginCoordinates = getAdjustedCoordinates(
624 selectable.getHandlePosition(
625 selection = selection, isStartHandle = isStartHandle
626 )
627 )
628
629 // Convert the position where drag gesture begins from composable coordinates to
630 // selection container coordinates.
631 currentDragPosition = requireContainerCoordinates().localPositionOf(
632 beginLayoutCoordinates,
633 beginCoordinates
634 )
635 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800636 showToolbar = false
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800637 }
638
639 override fun onStart(startPoint: Offset) {
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800640 val selection = selection!!
641 val startSelectable =
642 selectionRegistrar.selectableMap[selection.start.selectableId]
643 val endSelectable =
644 selectionRegistrar.selectableMap[selection.end.selectableId]
645 // The LayoutCoordinates of the composable where the drag gesture should begin. This
646 // is used to convert the position of the beginning of the drag gesture from the
647 // composable coordinates to selection container coordinates.
648 val beginLayoutCoordinates = if (isStartHandle) {
649 startSelectable?.getLayoutCoordinates()!!
650 } else {
651 endSelectable?.getLayoutCoordinates()!!
652 }
653
654 // The position of the character where the drag gesture should begin. This is in
655 // the composable coordinates.
656 val beginCoordinates = getAdjustedCoordinates(
657 if (isStartHandle) {
658 startSelectable!!.getHandlePosition(
659 selection = selection, isStartHandle = true
660 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700661 } else {
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800662 endSelectable!!.getHandlePosition(
663 selection = selection, isStartHandle = false
664 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700665 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800666 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700667
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800668 // Convert the position where drag gesture begins from composable coordinates to
669 // selection container coordinates.
670 dragBeginPosition = requireContainerCoordinates().localPositionOf(
671 beginLayoutCoordinates,
672 beginCoordinates
673 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700674
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800675 // Zero out the total distance that being dragged.
676 dragTotalDistance = Offset.Zero
677 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700678
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800679 override fun onDrag(delta: Offset) {
680 dragTotalDistance += delta
681 val endPosition = dragBeginPosition + dragTotalDistance
682 val consumed = updateSelection(
683 newPosition = endPosition,
684 previousPosition = dragBeginPosition,
685 isStartHandle = isStartHandle,
686 adjustment = SelectionAdjustment.CharacterWithWordAccelerate
687 )
688 if (consumed) {
689 dragBeginPosition = endPosition
Nader Jawad6df06122020-06-03 15:27:08 -0700690 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700691 }
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800692 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700693
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800694 private fun done() {
695 showToolbar = true
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800696 draggingHandle = null
Zach Klippenstein63870892022-01-14 12:45:18 -0800697 currentDragPosition = null
Zach Klippenstein72e3e512022-01-14 12:24:09 -0800698 }
haoyue6d80a12020-12-02 16:04:52 -0800699
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800700 override fun onUp() = done()
701 override fun onStop() = done()
702 override fun onCancel() = done()
Qingqing Deng6f56a912019-05-13 10:10:37 -0700703 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700704
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700705 /**
706 * Detect tap without consuming the up event.
707 */
708 private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
George Mount32de9dd2022-10-05 14:51:06 -0700709 awaitEachGesture {
710 waitForUpOrCancellation()?.let {
711 onTap(it.position)
Ralston Da Silvade62bc62021-06-02 17:46:44 -0700712 }
713 }
714 }
715
716 private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
717 return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
718 }
719
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700720 private fun convertToContainerCoordinates(
721 layoutCoordinates: LayoutCoordinates,
722 offset: Offset
Grant Toepferd681b5c2023-06-14 11:24:02 -0700723 ): Offset {
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700724 val coordinates = containerLayoutCoordinates
Grant Toepferd681b5c2023-06-14 11:24:02 -0700725 if (coordinates == null || !coordinates.isAttached) return Offset.Unspecified
George Mount77ca2a22020-12-11 17:46:19 +0000726 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700727 }
728
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700729 /**
730 * Cancel the previous selection and start a new selection at the given [position].
731 * It's used for long-press, double-click, triple-click and so on to start selection.
732 *
733 * @param position initial position of the selection. Both start and end handle is considered
734 * at this position.
735 * @param isStartHandle whether it's considered as the start handle moving. This parameter
736 * will influence the [SelectionAdjustment]'s behavior. For example,
737 * [SelectionAdjustment.Character] only adjust the moving handle.
738 * @param adjustment the selection adjustment.
739 */
740 private fun startSelection(
741 position: Offset,
742 isStartHandle: Boolean,
743 adjustment: SelectionAdjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700744 ) {
Grant Toepferd681b5c2023-06-14 11:24:02 -0700745 previousSelectionLayout = null
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700746 updateSelection(
747 startHandlePosition = position,
748 endHandlePosition = position,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700749 previousHandlePosition = Offset.Unspecified,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700750 isStartHandle = isStartHandle,
751 adjustment = adjustment
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700752 )
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700753 }
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700754
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700755 /**
756 * Updates the selection after one of the selection handle moved.
757 *
758 * @param newPosition the new position of the moving selection handle.
759 * @param previousPosition the previous position of the moving selection handle.
760 * @param isStartHandle whether the moving selection handle is the start handle.
761 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
762 * produce the final selection range.
763 *
764 * @return a boolean representing whether the movement is consumed.
765 *
766 * @see SelectionAdjustment
767 */
768 internal fun updateSelection(
769 newPosition: Offset?,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700770 previousPosition: Offset,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700771 isStartHandle: Boolean,
772 adjustment: SelectionAdjustment,
773 ): Boolean {
774 if (newPosition == null) return false
775 val otherHandlePosition = selection?.let { selection ->
776 val otherSelectableId = if (isStartHandle) {
777 selection.end.selectableId
778 } else {
779 selection.start.selectableId
780 }
781 val otherSelectable =
782 selectionRegistrar.selectableMap[otherSelectableId] ?: return@let null
783 convertToContainerCoordinates(
784 otherSelectable.getLayoutCoordinates()!!,
785 getAdjustedCoordinates(
786 otherSelectable.getHandlePosition(selection, !isStartHandle)
787 )
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700788 )
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700789 } ?: return false
790
791 val startHandlePosition = if (isStartHandle) newPosition else otherHandlePosition
792 val endHandlePosition = if (isStartHandle) otherHandlePosition else newPosition
793
794 return updateSelection(
795 startHandlePosition = startHandlePosition,
796 endHandlePosition = endHandlePosition,
797 previousHandlePosition = previousPosition,
798 isStartHandle = isStartHandle,
799 adjustment = adjustment
800 )
801 }
802
803 /**
804 * Updates the selection after one of the selection handle moved.
805 *
806 * To make sure that [SelectionAdjustment] works correctly, it's expected that only one
807 * selection handle is updated each time. The only exception is that when a new selection is
808 * started. In this case, [previousHandlePosition] is always null.
809 *
810 * @param startHandlePosition the position of the start selection handle.
811 * @param endHandlePosition the position of the end selection handle.
812 * @param previousHandlePosition the position of the moving handle before the update.
813 * @param isStartHandle whether the moving selection handle is the start handle.
814 * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
815 * produce the final selection range.
816 *
817 * @return a boolean representing whether the movement is consumed. It's useful for the case
818 * where a selection handle is updating consecutively. When the return value is true, it's
819 * expected that the caller will update the [startHandlePosition] to be the given
820 * [endHandlePosition] in following calls.
821 *
822 * @see SelectionAdjustment
823 */
824 internal fun updateSelection(
825 startHandlePosition: Offset,
826 endHandlePosition: Offset,
Grant Toepferd681b5c2023-06-14 11:24:02 -0700827 previousHandlePosition: Offset,
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700828 isStartHandle: Boolean,
829 adjustment: SelectionAdjustment,
830 ): Boolean {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800831 draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
Zach Klippenstein63870892022-01-14 12:45:18 -0800832 currentDragPosition = if (isStartHandle) startHandlePosition else endHandlePosition
Haoyu Zhang0020dd72021-08-19 19:21:03 -0700833
Grant Toepferd681b5c2023-06-14 11:24:02 -0700834 val selectionLayout = getSelectionLayout(
835 startHandlePosition = startHandlePosition,
836 endHandlePosition = endHandlePosition,
837 previousHandlePosition = previousHandlePosition,
838 isStartHandle = isStartHandle,
839 )
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800840
Grant Toepferd681b5c2023-06-14 11:24:02 -0700841 if (!selectionLayout.shouldRecomputeSelection(previousSelectionLayout)) {
842 return false
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700843 }
Grant Toepferd681b5c2023-06-14 11:24:02 -0700844
845 val newSelection = adjustment.adjust(selectionLayout)
846 if (newSelection != selection) {
847 selectionChanged(selectionLayout, newSelection)
848 }
849 previousSelectionLayout = selectionLayout
850 return true
Haoyu Zhanga78608e2021-08-03 17:10:18 -0700851 }
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200852
Grant Toepferd681b5c2023-06-14 11:24:02 -0700853 private fun getSelectionLayout(
854 startHandlePosition: Offset,
855 endHandlePosition: Offset,
856 previousHandlePosition: Offset,
857 isStartHandle: Boolean,
858 ): SelectionLayout {
859 val containerCoordinates = requireContainerCoordinates()
860 val sortedSelectables = selectionRegistrar.sort(containerCoordinates)
861
862 val idToIndexMap = mutableMapOf<Long, Int>()
863 sortedSelectables.fastForEachIndexed { index, selectable ->
864 idToIndexMap[selectable.selectableId] = index
865 }
866
867 val selectableIdOrderingComparator = compareBy<Long> { idToIndexMap[it] }
868
869 // if previous handle is null, then treat this as a new selection.
870 val previousSelection = if (previousHandlePosition.isUnspecified) null else selection
871 val builder = SelectionLayoutBuilder(
872 startHandlePosition = startHandlePosition,
873 endHandlePosition = endHandlePosition,
874 previousHandlePosition = previousHandlePosition,
875 containerCoordinates = containerCoordinates,
876 isStartHandle = isStartHandle,
877 previousSelection = previousSelection,
878 selectableIdOrderingComparator = selectableIdOrderingComparator,
879 )
880
881 sortedSelectables.fastForEach {
882 it.appendSelectableInfoToBuilder(builder)
883 }
884
885 return builder.build()
886 }
887
888 private fun selectionChanged(selectionLayout: SelectionLayout, newSelection: Selection) {
889 if (shouldPerformHaptics()) {
890 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
891 }
892 selectionRegistrar.subselections = selectionLayout.createSubSelections(newSelection)
893 onSelectionChange(newSelection)
894 }
895
896 @VisibleForTesting
897 internal fun shouldPerformHaptics(): Boolean =
898 isInTouchMode && selectionRegistrar.selectables.fastAny { it.getText().isNotEmpty() }
899
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200900 fun contextMenuOpenAdjustment(position: Offset) {
901 val isEmptySelection = selection?.toTextRange()?.collapsed ?: true
902 // TODO(b/209483184) the logic should be more complex here, it should check that current
Grant Toepferfefbd7a2023-03-03 15:02:55 -0800903 // selection doesn't include click position
Andrey Rudenkof3906a02021-10-12 13:23:40 +0200904 if (isEmptySelection) {
905 startSelection(
906 position = position,
907 isStartHandle = true,
908 adjustment = SelectionAdjustment.Word
909 )
910 }
911 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700912}
913
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200914internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700915 return lhs?.merge(rhs) ?: rhs
916}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800917
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100918internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
919
Zach Klippensteinadabe342021-11-11 16:38:13 -0800920internal expect fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier
921
Zach Klippenstein9c32bdf2021-11-11 16:38:13 -0800922internal fun calculateSelectionMagnifierCenterAndroid(
923 manager: SelectionManager,
924 magnifierSize: IntSize
925): Offset {
Zach Klippensteinadabe342021-11-11 16:38:13 -0800926 val selection = manager.selection ?: return Offset.Unspecified
927 return when (manager.draggingHandle) {
928 null -> return Offset.Unspecified
Grant Toepferd681b5c2023-06-14 11:24:02 -0700929 Handle.SelectionStart -> getMagnifierCenter(manager, magnifierSize, selection.start)
930 Handle.SelectionEnd -> getMagnifierCenter(manager, magnifierSize, selection.end)
Zach Klippensteinadabe342021-11-11 16:38:13 -0800931 Handle.Cursor -> error("SelectionContainer does not support cursor")
932 }
933}
934
Grant Toepferd681b5c2023-06-14 11:24:02 -0700935private fun getMagnifierCenter(
936 manager: SelectionManager,
937 magnifierSize: IntSize,
938 anchor: AnchorInfo
939): Offset {
940 val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
941 val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
942 val selectableCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified
943 val offset = anchor.offset
944
945 if (offset > selectable.getLastVisibleOffset()) return Offset.Unspecified
946
947 // The horizontal position doesn't snap to cursor positions but should directly track the
948 // actual drag.
949 val localDragPosition = selectableCoordinates.localPositionOf(
950 containerCoordinates,
951 manager.currentDragPosition!!
952 )
953 val dragX = localDragPosition.x
954
955 // But it is constrained by the horizontal bounds of the current line.
956 val lineRange = selectable.getRangeOfLineContaining(offset)
957 val textConstrainedX = if (lineRange.collapsed) {
958 // A collapsed range implies the text is empty.
959 // line left and right are equal for this offset, so use either
960 selectable.getLineLeft(offset)
961 } else {
962 val lineStartX = selectable.getLineLeft(lineRange.start)
963 val lineEndX = selectable.getLineRight(lineRange.end - 1)
964 // in RTL/BiDi, lineStartX may be larger than lineEndX
965 val minX = minOf(lineStartX, lineEndX)
966 val maxX = maxOf(lineStartX, lineEndX)
967 dragX.coerceIn(minX, maxX)
968 }
969
970 // selectable couldn't determine horizontals
971 if (textConstrainedX == -1f) return Offset.Unspecified
972
973 // Hide the magnifier when dragged too far (outside the horizontal bounds of how big the
974 // magnifier actually is). See
975 // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Editor.java;l=5228-5231;drc=2fdb6bd709be078b72f011334362456bb758922c
976 if ((dragX - textConstrainedX).absoluteValue > magnifierSize.width / 2) {
977 return Offset.Unspecified
978 }
979
980 val lineCenterY = selectable.getCenterYForOffset(offset)
981
982 // selectable couldn't determine the line center
983 if (lineCenterY == -1f) return Offset.Unspecified
984
985 return containerCoordinates.localPositionOf(
986 sourceCoordinates = selectableCoordinates,
987 relativeToSource = Offset(textConstrainedX, lineCenterY)
988 )
989}
990
haoyue2678c62020-12-09 08:39:12 -0800991/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800992internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800993 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
994 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
995 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000996 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800997 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000998 windowToLocal(boundsInWindow.topLeft),
999 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -08001000 )
1001}
1002
haoyuac341f02021-01-22 22:01:56 -08001003internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -08001004 offset.x in left..right && offset.y in top..bottom