[go: nahoru, domu]

blob: 96d7c2ed151b3b199adf5df832cf72cfd0b009ce [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
Matvei Malkovc287d1b2021-01-26 15:04:34 +000017@file:Suppress("DEPRECATION")
18
haoyuac341f02021-01-22 22:01:56 -080019package androidx.compose.foundation.text.selection
Qingqing Deng6f56a912019-05-13 10:10:37 -070020
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +000021import androidx.compose.foundation.fastFold
haoyuc40d02752021-01-25 17:32:47 -080022import androidx.compose.foundation.focusable
Louis Pullen-Freilich1f10a592020-07-24 16:35:14 +010023import androidx.compose.runtime.State
24import androidx.compose.runtime.getValue
25import androidx.compose.runtime.mutableStateOf
26import androidx.compose.runtime.setValue
27import androidx.compose.runtime.structuralEqualityPolicy
haoyuc40d02752021-01-25 17:32:47 -080028import androidx.compose.ui.Modifier
29import androidx.compose.ui.focus.FocusRequester
30import androidx.compose.ui.focus.focusRequester
31import androidx.compose.ui.focus.isFocused
32import androidx.compose.ui.focus.onFocusChanged
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010033import androidx.compose.ui.geometry.Offset
34import androidx.compose.ui.geometry.Rect
Matvei Malkov0506aa72021-02-07 17:02:32 +000035import androidx.compose.foundation.legacygestures.DragObserver
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010036import androidx.compose.ui.hapticfeedback.HapticFeedback
37import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010038import androidx.compose.ui.input.key.KeyEvent
39import androidx.compose.ui.input.key.onKeyEvent
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010040import androidx.compose.ui.layout.LayoutCoordinates
George Mount77ca2a22020-12-11 17:46:19 +000041import androidx.compose.ui.layout.boundsInWindow
haoyuc40d02752021-01-25 17:32:47 -080042import androidx.compose.ui.layout.onGloballyPositioned
haoyu7ad5ea32021-03-22 10:36:35 -070043import androidx.compose.ui.layout.positionInWindow
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010044import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010045import androidx.compose.ui.platform.TextToolbar
46import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010047import androidx.compose.ui.text.AnnotatedString
Nader Jawade6a9b332020-05-21 13:49:20 -070048import kotlin.math.max
49import kotlin.math.min
Qingqing Deng6f56a912019-05-13 10:10:37 -070050
Qingqing Deng35f97ea2019-09-18 19:24:37 -070051/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010052 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070053 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070054internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Qingqing Deng6f56a912019-05-13 10:10:37 -070055 /**
56 * The current selection.
57 */
58 var selection: Selection? = null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010059 set(value) {
60 field = value
haoyu9085c882020-12-08 12:01:06 -080061 if (value != null) {
62 updateHandleOffsets()
63 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010064 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070065
66 /**
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010067 * Is touch mode active
68 */
69 var touchMode: Boolean = true
70
71 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070072 * The manager will invoke this every time it comes to the conclusion that the selection should
73 * change. The expectation is that this callback will end up causing `setSelection` to get
74 * called. This is what makes this a "controlled component".
75 */
76 var onSelectionChange: (Selection?) -> Unit = {}
77
78 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080079 * [HapticFeedback] handle to perform haptic feedback.
80 */
81 var hapticFeedBack: HapticFeedback? = null
82
83 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070084 * [ClipboardManager] to perform clipboard features.
85 */
86 var clipboardManager: ClipboardManager? = null
87
88 /**
Qingqing Denga89450d2020-04-03 19:11:49 -070089 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
90 */
91 var textToolbar: TextToolbar? = null
92
93 /**
haoyuc40d02752021-01-25 17:32:47 -080094 * Focus requester used to request focus when selection becomes active.
95 */
96 var focusRequester: FocusRequester = FocusRequester()
97
98 /**
haoyu3c3fb452021-02-18 01:01:14 -080099 * Return true if the corresponding SelectionContainer is focused.
haoyuc40d02752021-01-25 17:32:47 -0800100 */
haoyu3c3fb452021-02-18 01:01:14 -0800101 var hasFocus: Boolean by mutableStateOf(false)
haoyuc40d02752021-01-25 17:32:47 -0800102
103 /**
104 * Modifier for selection container.
105 */
106 val modifier get() = Modifier
107 .onGloballyPositioned { containerLayoutCoordinates = it }
108 .focusRequester(focusRequester)
109 .onFocusChanged { focusState ->
haoyu3c3fb452021-02-18 01:01:14 -0800110 if (!focusState.isFocused && hasFocus) {
haoyuc40d02752021-01-25 17:32:47 -0800111 onRelease()
112 }
haoyu3c3fb452021-02-18 01:01:14 -0800113 hasFocus = focusState.isFocused
haoyuc40d02752021-01-25 17:32:47 -0800114 }
haoyu3c3fb452021-02-18 01:01:14 -0800115 .focusable()
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100116 .onKeyEvent {
117 if (isCopyKeyEvent(it)) {
118 copy()
119 true
120 } else {
121 false
122 }
123 }
haoyuc40d02752021-01-25 17:32:47 -0800124
haoyu7ad5ea32021-03-22 10:36:35 -0700125 private var previousPosition: Offset? = null
haoyuc40d02752021-01-25 17:32:47 -0800126 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700127 * Layout Coordinates of the selection container.
128 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100129 var containerLayoutCoordinates: LayoutCoordinates? = null
130 set(value) {
131 field = value
haoyu2c6e9842021-03-30 17:39:04 -0700132 if (hasFocus && selection != null) {
133 val positionInWindow = value?.positionInWindow()
134 if (previousPosition != positionInWindow) {
135 previousPosition = positionInWindow
136 updateHandleOffsets()
137 updateSelectionToolbarPosition()
138 }
haoyu3c3fb452021-02-18 01:01:14 -0800139 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100140 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700141
142 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700143 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
144 * recalculated.
145 */
Nader Jawad6df06122020-06-03 15:27:08 -0700146 private var dragBeginPosition = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700147
148 /**
149 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
150 * it will be zeroed out.
151 */
Nader Jawad6df06122020-06-03 15:27:08 -0700152 private var dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700153
154 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100155 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
156 * is null when handle shouldn't be displayed.
157 * It is a [State] so reading it during the composition will cause recomposition every time
158 * the position has been changed.
159 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700160 var startHandlePosition by mutableStateOf<Offset?>(
161 null,
162 policy = structuralEqualityPolicy()
163 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100164 private set
165
166 /**
167 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
168 * is null when handle shouldn't be displayed.
169 * It is a [State] so reading it during the composition will cause recomposition every time
170 * the position has been changed.
171 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700172 var endHandlePosition by mutableStateOf<Offset?>(
173 null,
174 policy = structuralEqualityPolicy()
175 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100176 private set
177
178 init {
haoyu7ad5ea32021-03-22 10:36:35 -0700179 selectionRegistrar.onPositionChangeCallback = { selectableId ->
180 if (
181 selectableId == selection?.start?.selectableId ||
182 selectableId == selection?.end?.selectableId
183 ) {
184 updateHandleOffsets()
185 updateSelectionToolbarPosition()
186 }
haoyue6d80a12020-12-02 16:04:52 -0800187 }
188
189 selectionRegistrar.onSelectionUpdateStartCallback = { layoutCoordinates, startPosition ->
190 updateSelection(
191 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
192 endPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
193 isStartHandle = true,
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100194 longPress = touchMode
haoyue6d80a12020-12-02 16:04:52 -0800195 )
haoyuc40d02752021-01-25 17:32:47 -0800196 focusRequester.requestFocus()
haoyu3c3fb452021-02-18 01:01:14 -0800197 hideSelectionToolbar()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100198 }
haoyue6d80a12020-12-02 16:04:52 -0800199
200 selectionRegistrar.onSelectionUpdateCallback =
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700201 { layoutCoordinates, startPosition, endPosition ->
202 updateSelection(
203 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
204 endPosition = convertToContainerCoordinates(layoutCoordinates, endPosition),
haoyu70aed732020-12-15 07:10:56 -0800205 isStartHandle = false,
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100206 longPress = touchMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700207 )
208 }
haoyue6d80a12020-12-02 16:04:52 -0800209
210 selectionRegistrar.onSelectionUpdateEndCallback = {
211 showSelectionToolbar()
212 }
haoyu9085c882020-12-08 12:01:06 -0800213
haoyue04245e2021-03-08 14:52:56 -0800214 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
215 if (selectableKey in selectionRegistrar.subselections) {
haoyu9085c882020-12-08 12:01:06 -0800216 // clear the selection range of each Selectable.
217 onRelease()
218 selection = null
219 }
220 }
haoyue04245e2021-03-08 14:52:56 -0800221
222 selectionRegistrar.afterSelectableUnsubscribe = { selectableKey ->
223 if (
224 selectableKey == selection?.start?.selectableId ||
225 selectableKey == selection?.end?.selectableId
226 ) {
227 // The selectable that contains a selection handle just unsubscribed.
228 // Hide selection handles for now
229 startHandlePosition = null
230 endHandlePosition = null
231 }
232 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100233 }
234
235 private fun updateHandleOffsets() {
236 val selection = selection
237 val containerCoordinates = containerLayoutCoordinates
haoyue04245e2021-03-08 14:52:56 -0800238 val startSelectable = selection?.start?.selectableId?.let {
239 selectionRegistrar.selectableMap[it]
240 }
241 val endSelectable = selection?.end?.selectableId?.let {
242 selectionRegistrar.selectableMap[it]
243 }
244 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
245 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
haoyue2678c62020-12-09 08:39:12 -0800246 if (
247 selection == null ||
248 containerCoordinates == null ||
249 !containerCoordinates.isAttached ||
250 startLayoutCoordinates == null ||
251 endLayoutCoordinates == null
252 ) {
253 this.startHandlePosition = null
254 this.endHandlePosition = null
255 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100256 }
haoyue2678c62020-12-09 08:39:12 -0800257
George Mount77ca2a22020-12-11 17:46:19 +0000258 val startHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800259 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800260 startSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800261 selection = selection,
262 isStartHandle = true
263 )
264 )
George Mount77ca2a22020-12-11 17:46:19 +0000265 val endHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800266 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800267 endSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800268 selection = selection,
269 isStartHandle = false
270 )
271 )
272
273 val visibleBounds = containerCoordinates.visibleBounds()
274 this.startHandlePosition =
275 if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
276 this.endHandlePosition =
277 if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100278 }
279
280 /**
281 * Returns non-nullable [containerLayoutCoordinates].
282 */
283 internal fun requireContainerCoordinates(): LayoutCoordinates {
284 val coordinates = containerLayoutCoordinates
285 require(coordinates != null)
286 require(coordinates.isAttached)
287 return coordinates
288 }
289
290 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -0700291 * Iterates over the handlers, gets the selection for each Composable, and merges all the
292 * returned [Selection]s.
293 *
Nader Jawad6df06122020-06-03 15:27:08 -0700294 * @param startPosition [Offset] for the start of the selection
295 * @param endPosition [Offset] for the end of the selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800296 * @param longPress the selection is a result of long press
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800297 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700298 *
haoyue04245e2021-03-08 14:52:56 -0800299 * @return a [Pair] of a [Selection] object which is constructed by combining all
300 * composables that are selected and a [Map] from selectable key to [Selection]s on the
301 * [Selectable] corresponding to the that key.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700302 */
Qingqing Deng5819ff62019-11-18 15:26:23 -0800303 // This function is internal for testing purposes.
304 internal fun mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700305 startPosition: Offset,
306 endPosition: Offset,
Siyamed Sinir0100f122019-11-16 00:23:12 -0800307 longPress: Boolean = false,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800308 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800309 isStartHandle: Boolean = true
haoyue04245e2021-03-08 14:52:56 -0800310 ): Pair<Selection?, Map<Long, Selection>> {
311 val subselections = mutableMapOf<Long, Selection>()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100312 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
haoyue04245e2021-03-08 14:52:56 -0800313 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
314 val selection = selectable.getSelection(
315 startPosition = startPosition,
316 endPosition = endPosition,
317 containerLayoutCoordinates = requireContainerCoordinates(),
318 longPress = longPress,
319 previousSelection = previousSelection,
320 isStartHandle = isStartHandle
Siyamed Sinir700df8452019-10-22 20:23:58 -0700321 )
haoyue04245e2021-03-08 14:52:56 -0800322 selection?.let { subselections[selectable.selectableId] = it }
323 merge(mergedSelection, selection)
Qingqing Deng247f2b42019-12-12 19:48:37 -0800324 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800325 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
326 HapticFeedbackType.TextHandleMove
327 )
haoyue04245e2021-03-08 14:52:56 -0800328 return Pair(newSelection, subselections)
Siyamed Sinir472c3162019-10-21 23:41:00 -0700329 }
330
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800331 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100332 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800333 var selectedText: AnnotatedString? = null
334
335 selection?.let {
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +0000336 for (i in selectables.indices) {
haoyue04245e2021-03-08 14:52:56 -0800337 val selectable = selectables[i]
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800338 // Continue if the current selectable is before the selection starts.
haoyue04245e2021-03-08 14:52:56 -0800339 if (selectable.selectableId != it.start.selectableId &&
340 selectable.selectableId != it.end.selectableId &&
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800341 selectedText == null
342 ) continue
343
344 val currentSelectedText = getCurrentSelectedText(
haoyue04245e2021-03-08 14:52:56 -0800345 selectable = selectable,
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800346 selection = it
347 )
348 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
349
350 // Break if the current selectable is the last selected selectable.
haoyue04245e2021-03-08 14:52:56 -0800351 if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
352 selectable.selectableId == it.start.selectableId && it.handlesCrossed
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800353 ) break
354 }
355 }
356 return selectedText
357 }
358
Qingqing Dengde023cc2020-04-24 14:23:41 -0700359 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700360 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700361 selectedText?.let { clipboardManager?.setText(it) }
362 }
363
364 /**
365 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
366 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
367 * the copy method as a callback when "copy" is clicked.
368 */
369 internal fun showSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800370 if (hasFocus) {
371 selection?.let {
372 textToolbar?.showMenu(
373 getContentRect(),
374 onCopyRequested = {
375 copy()
376 onRelease()
377 }
378 )
379 }
Qingqing Denga89450d2020-04-03 19:11:49 -0700380 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700381 }
382
haoyue6d80a12020-12-02 16:04:52 -0800383 internal fun hideSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800384 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800385 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700386 }
387 }
388
389 private fun updateSelectionToolbarPosition() {
haoyu3c3fb452021-02-18 01:01:14 -0800390 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700391 showSelectionToolbar()
392 }
393 }
394
395 /**
396 * Calculate selected region as [Rect]. The top is the top of the first selected
397 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
398 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
399 */
400 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700401 val selection = selection ?: return Rect.Zero
haoyue04245e2021-03-08 14:52:56 -0800402 val startSelectable = selectionRegistrar.selectableMap[selection.start.selectableId]
403 val endSelectable = selectionRegistrar.selectableMap[selection.start.selectableId]
404 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
405 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700406
407 val localLayoutCoordinates = containerLayoutCoordinates
408 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000409 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700410 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800411 startSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700412 selection = selection,
413 isStartHandle = true
414 )
415 )
George Mount77ca2a22020-12-11 17:46:19 +0000416 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700417 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800418 endSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700419 selection = selection,
420 isStartHandle = false
421 )
422 )
423
424 startOffset = localLayoutCoordinates.localToRoot(startOffset)
425 endOffset = localLayoutCoordinates.localToRoot(endOffset)
426
427 val left = min(startOffset.x, endOffset.x)
428 val right = max(startOffset.x, endOffset.x)
429
George Mount77ca2a22020-12-11 17:46:19 +0000430 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700431 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700432 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700433 0f,
haoyue04245e2021-03-08 14:52:56 -0800434 startSelectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700435 )
436 )
437
George Mount77ca2a22020-12-11 17:46:19 +0000438 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700439 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700440 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700441 0.0f,
haoyue04245e2021-03-08 14:52:56 -0800442 endSelectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700443 )
444 )
445
446 startTop = localLayoutCoordinates.localToRoot(startTop)
447 endTop = localLayoutCoordinates.localToRoot(endTop)
448
449 val top = min(startTop.y, endTop.y)
Nader Jawad16e330a2020-05-21 21:21:01 -0700450 val bottom = max(startOffset.y, endOffset.y) + (HANDLE_HEIGHT.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700451
452 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700453 left,
454 top,
455 right,
456 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700457 )
458 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700459 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700460 }
461
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800462 // This is for PressGestureDetector to cancel the selection.
463 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800464 selectionRegistrar.subselections = emptyMap()
haoyue6d80a12020-12-02 16:04:52 -0800465 hideSelectionToolbar()
haoyue04245e2021-03-08 14:52:56 -0800466 if (selection != null) {
467 onSelectionChange(null)
468 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
469 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800470 }
471
Siyamed Sinir472c3162019-10-21 23:41:00 -0700472 fun handleDragObserver(isStartHandle: Boolean): DragObserver {
Qingqing Deng6f56a912019-05-13 10:10:37 -0700473 return object : DragObserver {
Nader Jawad6df06122020-06-03 15:27:08 -0700474 override fun onStart(downPosition: Offset) {
haoyue6d80a12020-12-02 16:04:52 -0800475 hideSelectionToolbar()
Siyamed Sinir472c3162019-10-21 23:41:00 -0700476 val selection = selection!!
haoyue04245e2021-03-08 14:52:56 -0800477 val startSelectable =
478 selectionRegistrar.selectableMap[selection.start.selectableId]
479 val endSelectable =
480 selectionRegistrar.selectableMap[selection.end.selectableId]
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100481 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700482 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100483 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700484 val beginLayoutCoordinates = if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800485 startSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700486 } else {
haoyue04245e2021-03-08 14:52:56 -0800487 endSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700488 }
489
Qingqing Deng6f56a912019-05-13 10:10:37 -0700490 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100491 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700492 val beginCoordinates = getAdjustedCoordinates(
haoyue6d80a12020-12-02 16:04:52 -0800493 if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800494 startSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800495 selection = selection, isStartHandle = true
haoyue6d80a12020-12-02 16:04:52 -0800496 )
497 } else {
haoyue04245e2021-03-08 14:52:56 -0800498 endSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800499 selection = selection, isStartHandle = false
500 )
haoyue6d80a12020-12-02 16:04:52 -0800501 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700502 )
503
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100504 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700505 // selection container coordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000506 dragBeginPosition = requireContainerCoordinates().localPositionOf(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700507 beginLayoutCoordinates,
508 beginCoordinates
509 )
510
511 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700512 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700513 }
514
Nader Jawad6df06122020-06-03 15:27:08 -0700515 override fun onDrag(dragDistance: Offset): Offset {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700516 val selection = selection!!
Qingqing Deng6f56a912019-05-13 10:10:37 -0700517 dragTotalDistance += dragDistance
haoyue04245e2021-03-08 14:52:56 -0800518 val startSelectable =
519 selectionRegistrar.selectableMap[selection.start.selectableId]
520 val endSelectable =
521 selectionRegistrar.selectableMap[selection.end.selectableId]
Siyamed Sinir472c3162019-10-21 23:41:00 -0700522 val currentStart = if (isStartHandle) {
523 dragBeginPosition + dragTotalDistance
524 } else {
George Mount77ca2a22020-12-11 17:46:19 +0000525 requireContainerCoordinates().localPositionOf(
haoyue04245e2021-03-08 14:52:56 -0800526 startSelectable?.getLayoutCoordinates()!!,
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800527 getAdjustedCoordinates(
haoyue04245e2021-03-08 14:52:56 -0800528 startSelectable.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800529 selection = selection,
530 isStartHandle = true
531 )
532 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700533 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700534 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700535
536 val currentEnd = if (isStartHandle) {
George Mount77ca2a22020-12-11 17:46:19 +0000537 requireContainerCoordinates().localPositionOf(
haoyue04245e2021-03-08 14:52:56 -0800538 endSelectable?.getLayoutCoordinates()!!,
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800539 getAdjustedCoordinates(
haoyue04245e2021-03-08 14:52:56 -0800540 endSelectable.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800541 selection = selection,
542 isStartHandle = false
543 )
544 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700545 )
546 } else {
547 dragBeginPosition + dragTotalDistance
548 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700549 updateSelection(
Qingqing Denga5d80952019-10-11 16:46:52 -0700550 startPosition = currentStart,
551 endPosition = currentEnd,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800552 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700553 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700554 return dragDistance
555 }
haoyue6d80a12020-12-02 16:04:52 -0800556
557 override fun onStop(velocity: Offset) {
558 showSelectionToolbar()
559 }
560
561 override fun onCancel() {
562 showSelectionToolbar()
563 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700564 }
565 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700566
567 private fun convertToContainerCoordinates(
568 layoutCoordinates: LayoutCoordinates,
569 offset: Offset
570 ): Offset? {
571 val coordinates = containerLayoutCoordinates
572 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000573 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700574 }
575
576 private fun updateSelection(
577 startPosition: Offset?,
578 endPosition: Offset?,
579 longPress: Boolean = false,
580 isStartHandle: Boolean = true
581 ) {
582 if (startPosition == null || endPosition == null) return
haoyue04245e2021-03-08 14:52:56 -0800583 val (newSelection, newSubselection) = mergeSelections(
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700584 startPosition = startPosition,
585 endPosition = endPosition,
586 longPress = longPress,
587 isStartHandle = isStartHandle,
588 previousSelection = selection
589 )
haoyue04245e2021-03-08 14:52:56 -0800590 if (newSelection != selection) {
591 selectionRegistrar.subselections = newSubselection
592 onSelectionChange(newSelection)
593 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700594 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700595}
596
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200597internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700598 return lhs?.merge(rhs) ?: rhs
599}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800600
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100601internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
602
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200603internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800604 selectable: Selectable,
605 selection: Selection
606): AnnotatedString {
607 val currentText = selectable.getText()
608
609 return if (
haoyue04245e2021-03-08 14:52:56 -0800610 selectable.selectableId != selection.start.selectableId &&
611 selectable.selectableId != selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800612 ) {
613 // Select the full text content if the current selectable is between the
614 // start and the end selectables.
615 currentText
616 } else if (
haoyue04245e2021-03-08 14:52:56 -0800617 selectable.selectableId == selection.start.selectableId &&
618 selectable.selectableId == selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800619 ) {
620 // Select partial text content if the current selectable is the start and
621 // the end selectable.
622 if (selection.handlesCrossed) {
623 currentText.subSequence(selection.end.offset, selection.start.offset)
624 } else {
625 currentText.subSequence(selection.start.offset, selection.end.offset)
626 }
haoyue04245e2021-03-08 14:52:56 -0800627 } else if (selectable.selectableId == selection.start.selectableId) {
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800628 // Select partial text content if the current selectable is the start
629 // selectable.
630 if (selection.handlesCrossed) {
631 currentText.subSequence(0, selection.start.offset)
632 } else {
633 currentText.subSequence(selection.start.offset, currentText.length)
634 }
635 } else {
636 // Selectable partial text content if the current selectable is the end
637 // selectable.
638 if (selection.handlesCrossed) {
639 currentText.subSequence(selection.end.offset, currentText.length)
640 } else {
641 currentText.subSequence(0, selection.end.offset)
642 }
643 }
644}
haoyue2678c62020-12-09 08:39:12 -0800645
646/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800647internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800648 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
649 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
650 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000651 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800652 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000653 windowToLocal(boundsInWindow.topLeft),
654 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800655 )
656}
657
haoyuac341f02021-01-22 22:01:56 -0800658internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800659 offset.x in left..right && offset.y in top..bottom