[go: nahoru, domu]

blob: 63a8a6779f68ef60fd2d3f703b9bdc9cf14e8bca [file] [log] [blame]
Qingqing Deng6f56a912019-05-13 10:10:37 -07001/*
haoyuac341f02021-01-22 22:01:56 -08002 * Copyright 2021 The Android Open Source Project
Qingqing Deng6f56a912019-05-13 10:10:37 -07003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
haoyuac341f02021-01-22 22:01:56 -080017package androidx.compose.foundation.text.selection
Qingqing Deng6f56a912019-05-13 10:10:37 -070018
haoyuc40d02752021-01-25 17:32:47 -080019import androidx.compose.foundation.InteractionState
20import androidx.compose.foundation.focusable
Louis Pullen-Freilich1f10a592020-07-24 16:35:14 +010021import androidx.compose.runtime.State
22import androidx.compose.runtime.getValue
23import androidx.compose.runtime.mutableStateOf
24import androidx.compose.runtime.setValue
25import androidx.compose.runtime.structuralEqualityPolicy
haoyuc40d02752021-01-25 17:32:47 -080026import androidx.compose.ui.Modifier
27import androidx.compose.ui.focus.FocusRequester
28import androidx.compose.ui.focus.focusRequester
29import androidx.compose.ui.focus.isFocused
30import androidx.compose.ui.focus.onFocusChanged
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010031import androidx.compose.ui.geometry.Offset
32import androidx.compose.ui.geometry.Rect
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010033import androidx.compose.ui.gesture.DragObserver
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010034import androidx.compose.ui.hapticfeedback.HapticFeedback
35import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010036import androidx.compose.ui.layout.LayoutCoordinates
George Mount77ca2a22020-12-11 17:46:19 +000037import androidx.compose.ui.layout.boundsInWindow
haoyuc40d02752021-01-25 17:32:47 -080038import androidx.compose.ui.layout.onGloballyPositioned
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010039import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010040import androidx.compose.ui.platform.TextToolbar
41import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010042import androidx.compose.ui.text.AnnotatedString
haoyue6d80a12020-12-02 16:04:52 -080043import androidx.compose.ui.text.ExperimentalTextApi
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010044import androidx.compose.ui.text.InternalTextApi
Nader Jawade6a9b332020-05-21 13:49:20 -070045import kotlin.math.max
46import kotlin.math.min
Qingqing Deng6f56a912019-05-13 10:10:37 -070047
Qingqing Deng35f97ea2019-09-18 19:24:37 -070048/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010049 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070050 */
haoyue6d80a12020-12-02 16:04:52 -080051@OptIn(
52 InternalTextApi::class,
53 ExperimentalTextApi::class
54)
Siyamed Sinir700df8452019-10-22 20:23:58 -070055internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Qingqing Deng6f56a912019-05-13 10:10:37 -070056 /**
57 * The current selection.
58 */
59 var selection: Selection? = null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010060 set(value) {
61 field = value
haoyu9085c882020-12-08 12:01:06 -080062 if (value != null) {
63 updateHandleOffsets()
64 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010065 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070066
67 /**
68 * The manager will invoke this every time it comes to the conclusion that the selection should
69 * change. The expectation is that this callback will end up causing `setSelection` to get
70 * called. This is what makes this a "controlled component".
71 */
72 var onSelectionChange: (Selection?) -> Unit = {}
73
74 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080075 * [HapticFeedback] handle to perform haptic feedback.
76 */
77 var hapticFeedBack: HapticFeedback? = null
78
79 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070080 * [ClipboardManager] to perform clipboard features.
81 */
82 var clipboardManager: ClipboardManager? = null
83
84 /**
Qingqing Denga89450d2020-04-03 19:11:49 -070085 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
86 */
87 var textToolbar: TextToolbar? = null
88
89 /**
haoyuc40d02752021-01-25 17:32:47 -080090 * Focus requester used to request focus when selection becomes active.
91 */
92 var focusRequester: FocusRequester = FocusRequester()
93
94 /**
95 * InteractionState corresponds to the focusRequester, it will return trun.
96 */
97 val interactionState: InteractionState = InteractionState()
98
99 /**
100 * Modifier for selection container.
101 */
102 val modifier get() = Modifier
103 .onGloballyPositioned { containerLayoutCoordinates = it }
104 .focusRequester(focusRequester)
105 .onFocusChanged { focusState ->
106 if (!focusState.isFocused) {
107 onRelease()
108 }
109 }
110 .focusable(interactionState = interactionState)
111
112 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700113 * Layout Coordinates of the selection container.
114 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100115 var containerLayoutCoordinates: LayoutCoordinates? = null
116 set(value) {
117 field = value
118 updateHandleOffsets()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700119 updateSelectionToolbarPosition()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100120 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700121
122 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700123 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
124 * recalculated.
125 */
Nader Jawad6df06122020-06-03 15:27:08 -0700126 private var dragBeginPosition = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700127
128 /**
129 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
130 * it will be zeroed out.
131 */
Nader Jawad6df06122020-06-03 15:27:08 -0700132 private var dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700133
134 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100135 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
136 * is null when handle shouldn't be displayed.
137 * It is a [State] so reading it during the composition will cause recomposition every time
138 * the position has been changed.
139 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700140 var startHandlePosition by mutableStateOf<Offset?>(
141 null,
142 policy = structuralEqualityPolicy()
143 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100144 private set
145
146 /**
147 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
148 * is null when handle shouldn't be displayed.
149 * It is a [State] so reading it during the composition will cause recomposition every time
150 * the position has been changed.
151 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700152 var endHandlePosition by mutableStateOf<Offset?>(
153 null,
154 policy = structuralEqualityPolicy()
155 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100156 private set
157
158 init {
159 selectionRegistrar.onPositionChangeCallback = {
160 updateHandleOffsets()
haoyue6d80a12020-12-02 16:04:52 -0800161 updateSelectionToolbarPosition()
162 }
163
164 selectionRegistrar.onSelectionUpdateStartCallback = { layoutCoordinates, startPosition ->
165 updateSelection(
166 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
167 endPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
168 isStartHandle = true,
169 longPress = true
170 )
Qingqing Dengde023cc2020-04-24 14:23:41 -0700171 hideSelectionToolbar()
haoyuc40d02752021-01-25 17:32:47 -0800172 focusRequester.requestFocus()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100173 }
haoyue6d80a12020-12-02 16:04:52 -0800174
175 selectionRegistrar.onSelectionUpdateCallback =
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700176 { layoutCoordinates, startPosition, endPosition ->
177 updateSelection(
178 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
179 endPosition = convertToContainerCoordinates(layoutCoordinates, endPosition),
haoyu70aed732020-12-15 07:10:56 -0800180 isStartHandle = false,
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700181 longPress = true
182 )
183 }
haoyue6d80a12020-12-02 16:04:52 -0800184
185 selectionRegistrar.onSelectionUpdateEndCallback = {
186 showSelectionToolbar()
187 }
haoyu9085c882020-12-08 12:01:06 -0800188
189 selectionRegistrar.onSelectableChangeCallback = { selectable ->
190 if (selectable in selectionRegistrar.selectables) {
191 // clear the selection range of each Selectable.
192 onRelease()
193 selection = null
194 }
195 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100196 }
197
198 private fun updateHandleOffsets() {
199 val selection = selection
200 val containerCoordinates = containerLayoutCoordinates
haoyue2678c62020-12-09 08:39:12 -0800201 val startLayoutCoordinates = selection?.start?.selectable?.getLayoutCoordinates()
202 val endLayoutCoordinates = selection?.end?.selectable?.getLayoutCoordinates()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100203
haoyue2678c62020-12-09 08:39:12 -0800204 if (
205 selection == null ||
206 containerCoordinates == null ||
207 !containerCoordinates.isAttached ||
208 startLayoutCoordinates == null ||
209 endLayoutCoordinates == null
210 ) {
211 this.startHandlePosition = null
212 this.endHandlePosition = null
213 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100214 }
haoyue2678c62020-12-09 08:39:12 -0800215
George Mount77ca2a22020-12-11 17:46:19 +0000216 val startHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800217 startLayoutCoordinates,
218 selection.start.selectable.getHandlePosition(
219 selection = selection,
220 isStartHandle = true
221 )
222 )
George Mount77ca2a22020-12-11 17:46:19 +0000223 val endHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800224 endLayoutCoordinates,
225 selection.end.selectable.getHandlePosition(
226 selection = selection,
227 isStartHandle = false
228 )
229 )
230
231 val visibleBounds = containerCoordinates.visibleBounds()
232 this.startHandlePosition =
233 if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
234 this.endHandlePosition =
235 if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100236 }
237
238 /**
239 * Returns non-nullable [containerLayoutCoordinates].
240 */
241 internal fun requireContainerCoordinates(): LayoutCoordinates {
242 val coordinates = containerLayoutCoordinates
243 require(coordinates != null)
244 require(coordinates.isAttached)
245 return coordinates
246 }
247
248 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -0700249 * Iterates over the handlers, gets the selection for each Composable, and merges all the
250 * returned [Selection]s.
251 *
Nader Jawad6df06122020-06-03 15:27:08 -0700252 * @param startPosition [Offset] for the start of the selection
253 * @param endPosition [Offset] for the end of the selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800254 * @param longPress the selection is a result of long press
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800255 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700256 *
257 * @return [Selection] object which is constructed by combining all Composables that are
258 * selected.
259 */
Qingqing Deng5819ff62019-11-18 15:26:23 -0800260 // This function is internal for testing purposes.
261 internal fun mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700262 startPosition: Offset,
263 endPosition: Offset,
Siyamed Sinir0100f122019-11-16 00:23:12 -0800264 longPress: Boolean = false,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800265 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800266 isStartHandle: Boolean = true
Siyamed Sinir472c3162019-10-21 23:41:00 -0700267 ): Selection? {
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800268
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100269 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
haoyuac341f02021-01-22 22:01:56 -0800270 .fold(null) { mergedSelection: Selection?, handler: Selectable ->
Qingqing Deng247f2b42019-12-12 19:48:37 -0800271 merge(
272 mergedSelection,
273 handler.getSelection(
274 startPosition = startPosition,
275 endPosition = endPosition,
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100276 containerLayoutCoordinates = requireContainerCoordinates(),
Qingqing Deng247f2b42019-12-12 19:48:37 -0800277 longPress = longPress,
278 previousSelection = previousSelection,
279 isStartHandle = isStartHandle
280 )
Siyamed Sinir700df8452019-10-22 20:23:58 -0700281 )
Qingqing Deng247f2b42019-12-12 19:48:37 -0800282 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800283 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
284 HapticFeedbackType.TextHandleMove
285 )
286 return newSelection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700287 }
288
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800289 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100290 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800291 var selectedText: AnnotatedString? = null
292
293 selection?.let {
294 for (handler in selectables) {
295 // Continue if the current selectable is before the selection starts.
296 if (handler != it.start.selectable && handler != it.end.selectable &&
297 selectedText == null
298 ) continue
299
300 val currentSelectedText = getCurrentSelectedText(
301 selectable = handler,
302 selection = it
303 )
304 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
305
306 // Break if the current selectable is the last selected selectable.
307 if (handler == it.end.selectable && !it.handlesCrossed ||
308 handler == it.start.selectable && it.handlesCrossed
309 ) break
310 }
311 }
312 return selectedText
313 }
314
Qingqing Dengde023cc2020-04-24 14:23:41 -0700315 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700316 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700317 selectedText?.let { clipboardManager?.setText(it) }
318 }
319
320 /**
321 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
322 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
323 * the copy method as a callback when "copy" is clicked.
324 */
325 internal fun showSelectionToolbar() {
326 selection?.let {
Qingqing Deng40519572020-07-10 13:25:36 -0700327 textToolbar?.showMenu(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700328 getContentRect(),
Qingqing Deng40519572020-07-10 13:25:36 -0700329 onCopyRequested = {
330 copy()
331 onRelease()
332 }
Qingqing Dengce140562020-04-24 14:46:22 -0700333 )
Qingqing Denga89450d2020-04-03 19:11:49 -0700334 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700335 }
336
haoyue6d80a12020-12-02 16:04:52 -0800337 internal fun hideSelectionToolbar() {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700338 if (textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800339 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700340 }
341 }
342
343 private fun updateSelectionToolbarPosition() {
344 if (textToolbar?.status == TextToolbarStatus.Shown) {
345 showSelectionToolbar()
346 }
347 }
348
349 /**
350 * Calculate selected region as [Rect]. The top is the top of the first selected
351 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
352 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
353 */
354 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700355 val selection = selection ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700356 val startLayoutCoordinates =
Nader Jawad8432b8d2020-07-27 14:51:23 -0700357 selection.start.selectable.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700358 val endLayoutCoordinates =
Nader Jawad8432b8d2020-07-27 14:51:23 -0700359 selection.end.selectable.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700360
361 val localLayoutCoordinates = containerLayoutCoordinates
362 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000363 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700364 startLayoutCoordinates,
365 selection.start.selectable.getHandlePosition(
366 selection = selection,
367 isStartHandle = true
368 )
369 )
George Mount77ca2a22020-12-11 17:46:19 +0000370 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700371 endLayoutCoordinates,
372 selection.end.selectable.getHandlePosition(
373 selection = selection,
374 isStartHandle = false
375 )
376 )
377
378 startOffset = localLayoutCoordinates.localToRoot(startOffset)
379 endOffset = localLayoutCoordinates.localToRoot(endOffset)
380
381 val left = min(startOffset.x, endOffset.x)
382 val right = max(startOffset.x, endOffset.x)
383
George Mount77ca2a22020-12-11 17:46:19 +0000384 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700385 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700386 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700387 0f,
388 selection.start.selectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700389 )
390 )
391
George Mount77ca2a22020-12-11 17:46:19 +0000392 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700393 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700394 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700395 0.0f,
396 selection.end.selectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700397 )
398 )
399
400 startTop = localLayoutCoordinates.localToRoot(startTop)
401 endTop = localLayoutCoordinates.localToRoot(endTop)
402
403 val top = min(startTop.y, endTop.y)
Nader Jawad16e330a2020-05-21 21:21:01 -0700404 val bottom = max(startOffset.y, endOffset.y) + (HANDLE_HEIGHT.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700405
406 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700407 left,
408 top,
409 right,
410 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700411 )
412 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700413 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700414 }
415
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800416 // This is for PressGestureDetector to cancel the selection.
417 fun onRelease() {
haoyuc40d02752021-01-25 17:32:47 -0800418 if (containerLayoutCoordinates?.isAttached == true) {
419 // Call mergeSelections with an out of boundary input to inform all text widgets to
420 // cancel their individual selection.
421 mergeSelections(
422 startPosition = Offset(-1f, -1f),
423 endPosition = Offset(-1f, -1f),
424 previousSelection = selection
425 )
426 }
haoyue6d80a12020-12-02 16:04:52 -0800427 hideSelectionToolbar()
Siyamed Sinire810eab2019-11-22 12:36:38 -0800428 if (selection != null) onSelectionChange(null)
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800429 }
430
Siyamed Sinir472c3162019-10-21 23:41:00 -0700431 fun handleDragObserver(isStartHandle: Boolean): DragObserver {
Qingqing Deng6f56a912019-05-13 10:10:37 -0700432 return object : DragObserver {
Nader Jawad6df06122020-06-03 15:27:08 -0700433 override fun onStart(downPosition: Offset) {
haoyue6d80a12020-12-02 16:04:52 -0800434 hideSelectionToolbar()
Siyamed Sinir472c3162019-10-21 23:41:00 -0700435 val selection = selection!!
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100436 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700437 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100438 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700439 val beginLayoutCoordinates = if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800440 selection.start.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700441 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800442 selection.end.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700443 }
444
Qingqing Deng6f56a912019-05-13 10:10:37 -0700445 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100446 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700447 val beginCoordinates = getAdjustedCoordinates(
haoyue6d80a12020-12-02 16:04:52 -0800448 if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800449 selection.start.selectable.getHandlePosition(
450 selection = selection, isStartHandle = true
haoyue6d80a12020-12-02 16:04:52 -0800451 )
452 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800453 selection.end.selectable.getHandlePosition(
454 selection = selection, isStartHandle = false
455 )
haoyue6d80a12020-12-02 16:04:52 -0800456 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700457 )
458
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100459 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700460 // selection container coordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000461 dragBeginPosition = requireContainerCoordinates().localPositionOf(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700462 beginLayoutCoordinates,
463 beginCoordinates
464 )
465
466 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700467 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700468 }
469
Nader Jawad6df06122020-06-03 15:27:08 -0700470 override fun onDrag(dragDistance: Offset): Offset {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700471 val selection = selection!!
Qingqing Deng6f56a912019-05-13 10:10:37 -0700472 dragTotalDistance += dragDistance
473
Siyamed Sinir472c3162019-10-21 23:41:00 -0700474 val currentStart = if (isStartHandle) {
475 dragBeginPosition + dragTotalDistance
476 } else {
George Mount77ca2a22020-12-11 17:46:19 +0000477 requireContainerCoordinates().localPositionOf(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800478 selection.start.selectable.getLayoutCoordinates()!!,
479 getAdjustedCoordinates(
480 selection.start.selectable.getHandlePosition(
481 selection = selection,
482 isStartHandle = true
483 )
484 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700485 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700486 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700487
488 val currentEnd = if (isStartHandle) {
George Mount77ca2a22020-12-11 17:46:19 +0000489 requireContainerCoordinates().localPositionOf(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800490 selection.end.selectable.getLayoutCoordinates()!!,
491 getAdjustedCoordinates(
492 selection.end.selectable.getHandlePosition(
493 selection = selection,
494 isStartHandle = false
495 )
496 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700497 )
498 } else {
499 dragBeginPosition + dragTotalDistance
500 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700501 updateSelection(
Qingqing Denga5d80952019-10-11 16:46:52 -0700502 startPosition = currentStart,
503 endPosition = currentEnd,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800504 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700505 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700506 return dragDistance
507 }
haoyue6d80a12020-12-02 16:04:52 -0800508
509 override fun onStop(velocity: Offset) {
510 showSelectionToolbar()
511 }
512
513 override fun onCancel() {
514 showSelectionToolbar()
515 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700516 }
517 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700518
519 private fun convertToContainerCoordinates(
520 layoutCoordinates: LayoutCoordinates,
521 offset: Offset
522 ): Offset? {
523 val coordinates = containerLayoutCoordinates
524 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000525 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700526 }
527
528 private fun updateSelection(
529 startPosition: Offset?,
530 endPosition: Offset?,
531 longPress: Boolean = false,
532 isStartHandle: Boolean = true
533 ) {
534 if (startPosition == null || endPosition == null) return
535 val newSelection = mergeSelections(
536 startPosition = startPosition,
537 endPosition = endPosition,
538 longPress = longPress,
539 isStartHandle = isStartHandle,
540 previousSelection = selection
541 )
542 if (newSelection != selection) onSelectionChange(newSelection)
543 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700544}
545
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200546internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700547 return lhs?.merge(rhs) ?: rhs
548}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800549
haoyue6d80a12020-12-02 16:04:52 -0800550@OptIn(ExperimentalTextApi::class)
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200551internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800552 selectable: Selectable,
553 selection: Selection
554): AnnotatedString {
555 val currentText = selectable.getText()
556
557 return if (
558 selectable != selection.start.selectable &&
559 selectable != selection.end.selectable
560 ) {
561 // Select the full text content if the current selectable is between the
562 // start and the end selectables.
563 currentText
564 } else if (
565 selectable == selection.start.selectable &&
566 selectable == selection.end.selectable
567 ) {
568 // Select partial text content if the current selectable is the start and
569 // the end selectable.
570 if (selection.handlesCrossed) {
571 currentText.subSequence(selection.end.offset, selection.start.offset)
572 } else {
573 currentText.subSequence(selection.start.offset, selection.end.offset)
574 }
575 } else if (selectable == selection.start.selectable) {
576 // Select partial text content if the current selectable is the start
577 // selectable.
578 if (selection.handlesCrossed) {
579 currentText.subSequence(0, selection.start.offset)
580 } else {
581 currentText.subSequence(selection.start.offset, currentText.length)
582 }
583 } else {
584 // Selectable partial text content if the current selectable is the end
585 // selectable.
586 if (selection.handlesCrossed) {
587 currentText.subSequence(selection.end.offset, currentText.length)
588 } else {
589 currentText.subSequence(0, selection.end.offset)
590 }
591 }
592}
haoyue2678c62020-12-09 08:39:12 -0800593
594/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800595internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800596 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
597 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
598 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000599 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800600 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000601 windowToLocal(boundsInWindow.topLeft),
602 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800603 )
604}
605
haoyuac341f02021-01-22 22:01:56 -0800606internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800607 offset.x in left..right && offset.y in top..bottom