[go: nahoru, domu]

blob: ce33357684dec306777758775ae22055c6a18ed5 [file] [log] [blame]
Qingqing Deng6f56a912019-05-13 10:10:37 -07001/*
2 * Copyright 2019 The Android Open Source Project
3 *
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
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010017package androidx.compose.ui.selection
Qingqing Deng6f56a912019-05-13 10:10:37 -070018
Louis Pullen-Freilich1f10a592020-07-24 16:35:14 +010019import androidx.compose.runtime.State
20import androidx.compose.runtime.getValue
21import androidx.compose.runtime.mutableStateOf
22import androidx.compose.runtime.setValue
23import androidx.compose.runtime.structuralEqualityPolicy
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010024import androidx.compose.ui.geometry.Offset
25import androidx.compose.ui.geometry.Rect
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010026import androidx.compose.ui.gesture.DragObserver
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010027import androidx.compose.ui.hapticfeedback.HapticFeedback
28import androidx.compose.ui.hapticfeedback.HapticFeedbackType
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010029import androidx.compose.ui.layout.LayoutCoordinates
30import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010031import androidx.compose.ui.platform.TextToolbar
32import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010033import androidx.compose.ui.text.AnnotatedString
haoyue6d80a12020-12-02 16:04:52 -080034import androidx.compose.ui.text.ExperimentalTextApi
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010035import androidx.compose.ui.text.InternalTextApi
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010036import androidx.compose.ui.text.length
37import androidx.compose.ui.text.subSequence
Nader Jawade6a9b332020-05-21 13:49:20 -070038import kotlin.math.max
39import kotlin.math.min
Qingqing Deng6f56a912019-05-13 10:10:37 -070040
Qingqing Deng35f97ea2019-09-18 19:24:37 -070041/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010042 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070043 */
haoyue6d80a12020-12-02 16:04:52 -080044@OptIn(
45 InternalTextApi::class,
46 ExperimentalTextApi::class
47)
Siyamed Sinir700df8452019-10-22 20:23:58 -070048internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Qingqing Deng6f56a912019-05-13 10:10:37 -070049 /**
50 * The current selection.
51 */
52 var selection: Selection? = null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010053 set(value) {
54 field = value
55 updateHandleOffsets()
56 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070057
58 /**
59 * The manager will invoke this every time it comes to the conclusion that the selection should
60 * change. The expectation is that this callback will end up causing `setSelection` to get
61 * called. This is what makes this a "controlled component".
62 */
63 var onSelectionChange: (Selection?) -> Unit = {}
64
65 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080066 * [HapticFeedback] handle to perform haptic feedback.
67 */
68 var hapticFeedBack: HapticFeedback? = null
69
70 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070071 * [ClipboardManager] to perform clipboard features.
72 */
73 var clipboardManager: ClipboardManager? = null
74
75 /**
Qingqing Denga89450d2020-04-03 19:11:49 -070076 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
77 */
78 var textToolbar: TextToolbar? = null
79
80 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070081 * Layout Coordinates of the selection container.
82 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010083 var containerLayoutCoordinates: LayoutCoordinates? = null
84 set(value) {
85 field = value
86 updateHandleOffsets()
Qingqing Dengde023cc2020-04-24 14:23:41 -070087 updateSelectionToolbarPosition()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010088 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070089
90 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070091 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
92 * recalculated.
93 */
Nader Jawad6df06122020-06-03 15:27:08 -070094 private var dragBeginPosition = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -070095
96 /**
97 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
98 * it will be zeroed out.
99 */
Nader Jawad6df06122020-06-03 15:27:08 -0700100 private var dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700101
102 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100103 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
104 * is null when handle shouldn't be displayed.
105 * It is a [State] so reading it during the composition will cause recomposition every time
106 * the position has been changed.
107 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700108 var startHandlePosition by mutableStateOf<Offset?>(
109 null,
110 policy = structuralEqualityPolicy()
111 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100112 private set
113
114 /**
115 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
116 * is null when handle shouldn't be displayed.
117 * It is a [State] so reading it during the composition will cause recomposition every time
118 * the position has been changed.
119 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700120 var endHandlePosition by mutableStateOf<Offset?>(
121 null,
122 policy = structuralEqualityPolicy()
123 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100124 private set
125
126 init {
127 selectionRegistrar.onPositionChangeCallback = {
128 updateHandleOffsets()
haoyue6d80a12020-12-02 16:04:52 -0800129 updateSelectionToolbarPosition()
130 }
131
132 selectionRegistrar.onSelectionUpdateStartCallback = { layoutCoordinates, startPosition ->
133 updateSelection(
134 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
135 endPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
136 isStartHandle = true,
137 longPress = true
138 )
Qingqing Dengde023cc2020-04-24 14:23:41 -0700139 hideSelectionToolbar()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100140 }
haoyue6d80a12020-12-02 16:04:52 -0800141
142 selectionRegistrar.onSelectionUpdateCallback =
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700143 { layoutCoordinates, startPosition, endPosition ->
144 updateSelection(
145 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
146 endPosition = convertToContainerCoordinates(layoutCoordinates, endPosition),
147 isStartHandle = true,
148 longPress = true
149 )
150 }
haoyue6d80a12020-12-02 16:04:52 -0800151
152 selectionRegistrar.onSelectionUpdateEndCallback = {
153 showSelectionToolbar()
154 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100155 }
156
157 private fun updateHandleOffsets() {
158 val selection = selection
159 val containerCoordinates = containerLayoutCoordinates
160 if (selection != null && containerCoordinates != null && containerCoordinates.isAttached) {
161 val startLayoutCoordinates = selection.start.selectable.getLayoutCoordinates()
162 val endLayoutCoordinates = selection.end.selectable.getLayoutCoordinates()
163
164 if (startLayoutCoordinates != null && endLayoutCoordinates != null) {
165 startHandlePosition = containerCoordinates.childToLocal(
166 startLayoutCoordinates,
167 selection.start.selectable.getHandlePosition(
168 selection = selection,
169 isStartHandle = true
170 )
171 )
172 endHandlePosition = containerCoordinates.childToLocal(
173 endLayoutCoordinates,
174 selection.end.selectable.getHandlePosition(
175 selection = selection,
176 isStartHandle = false
177 )
178 )
179 return
180 }
181 }
182 startHandlePosition = null
183 endHandlePosition = null
184 }
185
186 /**
187 * Returns non-nullable [containerLayoutCoordinates].
188 */
189 internal fun requireContainerCoordinates(): LayoutCoordinates {
190 val coordinates = containerLayoutCoordinates
191 require(coordinates != null)
192 require(coordinates.isAttached)
193 return coordinates
194 }
195
196 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -0700197 * Iterates over the handlers, gets the selection for each Composable, and merges all the
198 * returned [Selection]s.
199 *
Nader Jawad6df06122020-06-03 15:27:08 -0700200 * @param startPosition [Offset] for the start of the selection
201 * @param endPosition [Offset] for the end of the selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800202 * @param longPress the selection is a result of long press
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800203 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700204 *
205 * @return [Selection] object which is constructed by combining all Composables that are
206 * selected.
207 */
Qingqing Deng5819ff62019-11-18 15:26:23 -0800208 // This function is internal for testing purposes.
209 internal fun mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700210 startPosition: Offset,
211 endPosition: Offset,
Siyamed Sinir0100f122019-11-16 00:23:12 -0800212 longPress: Boolean = false,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800213 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800214 isStartHandle: Boolean = true
Siyamed Sinir472c3162019-10-21 23:41:00 -0700215 ): Selection? {
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800216
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100217 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng247f2b42019-12-12 19:48:37 -0800218 .fold(null) { mergedSelection: Selection?,
Jeff Gastona3bbb7e2020-09-18 15:11:15 -0400219 handler: Selectable ->
Qingqing Deng247f2b42019-12-12 19:48:37 -0800220 merge(
221 mergedSelection,
222 handler.getSelection(
223 startPosition = startPosition,
224 endPosition = endPosition,
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100225 containerLayoutCoordinates = requireContainerCoordinates(),
Qingqing Deng247f2b42019-12-12 19:48:37 -0800226 longPress = longPress,
227 previousSelection = previousSelection,
228 isStartHandle = isStartHandle
229 )
Siyamed Sinir700df8452019-10-22 20:23:58 -0700230 )
Qingqing Deng247f2b42019-12-12 19:48:37 -0800231 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800232 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
233 HapticFeedbackType.TextHandleMove
234 )
235 return newSelection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700236 }
237
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800238 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100239 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800240 var selectedText: AnnotatedString? = null
241
242 selection?.let {
243 for (handler in selectables) {
244 // Continue if the current selectable is before the selection starts.
245 if (handler != it.start.selectable && handler != it.end.selectable &&
246 selectedText == null
247 ) continue
248
249 val currentSelectedText = getCurrentSelectedText(
250 selectable = handler,
251 selection = it
252 )
253 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
254
255 // Break if the current selectable is the last selected selectable.
256 if (handler == it.end.selectable && !it.handlesCrossed ||
257 handler == it.start.selectable && it.handlesCrossed
258 ) break
259 }
260 }
261 return selectedText
262 }
263
Qingqing Dengde023cc2020-04-24 14:23:41 -0700264 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700265 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700266 selectedText?.let { clipboardManager?.setText(it) }
267 }
268
269 /**
270 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
271 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
272 * the copy method as a callback when "copy" is clicked.
273 */
274 internal fun showSelectionToolbar() {
275 selection?.let {
Qingqing Deng40519572020-07-10 13:25:36 -0700276 textToolbar?.showMenu(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700277 getContentRect(),
Qingqing Deng40519572020-07-10 13:25:36 -0700278 onCopyRequested = {
279 copy()
280 onRelease()
281 }
Qingqing Dengce140562020-04-24 14:46:22 -0700282 )
Qingqing Denga89450d2020-04-03 19:11:49 -0700283 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700284 }
285
haoyue6d80a12020-12-02 16:04:52 -0800286 internal fun hideSelectionToolbar() {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700287 if (textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800288 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700289 }
290 }
291
292 private fun updateSelectionToolbarPosition() {
293 if (textToolbar?.status == TextToolbarStatus.Shown) {
294 showSelectionToolbar()
295 }
296 }
297
298 /**
299 * Calculate selected region as [Rect]. The top is the top of the first selected
300 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
301 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
302 */
303 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700304 val selection = selection ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700305 val startLayoutCoordinates =
Nader Jawad8432b8d2020-07-27 14:51:23 -0700306 selection.start.selectable.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700307 val endLayoutCoordinates =
Nader Jawad8432b8d2020-07-27 14:51:23 -0700308 selection.end.selectable.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700309
310 val localLayoutCoordinates = containerLayoutCoordinates
311 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
312 var startOffset = localLayoutCoordinates.childToLocal(
313 startLayoutCoordinates,
314 selection.start.selectable.getHandlePosition(
315 selection = selection,
316 isStartHandle = true
317 )
318 )
319 var endOffset = localLayoutCoordinates.childToLocal(
320 endLayoutCoordinates,
321 selection.end.selectable.getHandlePosition(
322 selection = selection,
323 isStartHandle = false
324 )
325 )
326
327 startOffset = localLayoutCoordinates.localToRoot(startOffset)
328 endOffset = localLayoutCoordinates.localToRoot(endOffset)
329
330 val left = min(startOffset.x, endOffset.x)
331 val right = max(startOffset.x, endOffset.x)
332
333 var startTop = localLayoutCoordinates.childToLocal(
334 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700335 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700336 0f,
337 selection.start.selectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700338 )
339 )
340
341 var endTop = localLayoutCoordinates.childToLocal(
342 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700343 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700344 0.0f,
345 selection.end.selectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700346 )
347 )
348
349 startTop = localLayoutCoordinates.localToRoot(startTop)
350 endTop = localLayoutCoordinates.localToRoot(endTop)
351
352 val top = min(startTop.y, endTop.y)
Nader Jawad16e330a2020-05-21 21:21:01 -0700353 val bottom = max(startOffset.y, endOffset.y) + (HANDLE_HEIGHT.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700354
355 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700356 left,
357 top,
358 right,
359 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700360 )
361 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700362 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700363 }
364
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800365 // This is for PressGestureDetector to cancel the selection.
366 fun onRelease() {
367 // Call mergeSelections with an out of boundary input to inform all text widgets to
368 // cancel their individual selection.
Qingqing Denga5d80952019-10-11 16:46:52 -0700369 mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700370 startPosition = Offset(-1f, -1f),
371 endPosition = Offset(-1f, -1f),
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800372 previousSelection = selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800373 )
haoyue6d80a12020-12-02 16:04:52 -0800374 hideSelectionToolbar()
Siyamed Sinire810eab2019-11-22 12:36:38 -0800375 if (selection != null) onSelectionChange(null)
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800376 }
377
Siyamed Sinir472c3162019-10-21 23:41:00 -0700378 fun handleDragObserver(isStartHandle: Boolean): DragObserver {
Qingqing Deng6f56a912019-05-13 10:10:37 -0700379 return object : DragObserver {
Nader Jawad6df06122020-06-03 15:27:08 -0700380 override fun onStart(downPosition: Offset) {
haoyue6d80a12020-12-02 16:04:52 -0800381 hideSelectionToolbar()
Siyamed Sinir472c3162019-10-21 23:41:00 -0700382 val selection = selection!!
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100383 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700384 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100385 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700386 val beginLayoutCoordinates = if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800387 selection.start.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700388 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800389 selection.end.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700390 }
391
Qingqing Deng6f56a912019-05-13 10:10:37 -0700392 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100393 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700394 val beginCoordinates = getAdjustedCoordinates(
haoyue6d80a12020-12-02 16:04:52 -0800395 if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800396 selection.start.selectable.getHandlePosition(
397 selection = selection, isStartHandle = true
haoyue6d80a12020-12-02 16:04:52 -0800398 )
399 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800400 selection.end.selectable.getHandlePosition(
401 selection = selection, isStartHandle = false
402 )
haoyue6d80a12020-12-02 16:04:52 -0800403 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700404 )
405
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100406 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700407 // selection container coordinates.
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100408 dragBeginPosition = requireContainerCoordinates().childToLocal(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700409 beginLayoutCoordinates,
410 beginCoordinates
411 )
412
413 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700414 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700415 }
416
Nader Jawad6df06122020-06-03 15:27:08 -0700417 override fun onDrag(dragDistance: Offset): Offset {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700418 val selection = selection!!
Qingqing Deng6f56a912019-05-13 10:10:37 -0700419 dragTotalDistance += dragDistance
420
Siyamed Sinir472c3162019-10-21 23:41:00 -0700421 val currentStart = if (isStartHandle) {
422 dragBeginPosition + dragTotalDistance
423 } else {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100424 requireContainerCoordinates().childToLocal(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800425 selection.start.selectable.getLayoutCoordinates()!!,
426 getAdjustedCoordinates(
427 selection.start.selectable.getHandlePosition(
428 selection = selection,
429 isStartHandle = true
430 )
431 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700432 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700433 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700434
435 val currentEnd = if (isStartHandle) {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100436 requireContainerCoordinates().childToLocal(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800437 selection.end.selectable.getLayoutCoordinates()!!,
438 getAdjustedCoordinates(
439 selection.end.selectable.getHandlePosition(
440 selection = selection,
441 isStartHandle = false
442 )
443 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700444 )
445 } else {
446 dragBeginPosition + dragTotalDistance
447 }
448
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700449 updateSelection(
Qingqing Denga5d80952019-10-11 16:46:52 -0700450 startPosition = currentStart,
451 endPosition = currentEnd,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800452 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700453 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700454 return dragDistance
455 }
haoyue6d80a12020-12-02 16:04:52 -0800456
457 override fun onStop(velocity: Offset) {
458 showSelectionToolbar()
459 }
460
461 override fun onCancel() {
462 showSelectionToolbar()
463 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700464 }
465 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700466
467 private fun convertToContainerCoordinates(
468 layoutCoordinates: LayoutCoordinates,
469 offset: Offset
470 ): Offset? {
471 val coordinates = containerLayoutCoordinates
472 if (coordinates == null || !coordinates.isAttached) return null
473 return requireContainerCoordinates().childToLocal(layoutCoordinates, offset)
474 }
475
476 private fun updateSelection(
477 startPosition: Offset?,
478 endPosition: Offset?,
479 longPress: Boolean = false,
480 isStartHandle: Boolean = true
481 ) {
482 if (startPosition == null || endPosition == null) return
483 val newSelection = mergeSelections(
484 startPosition = startPosition,
485 endPosition = endPosition,
486 longPress = longPress,
487 isStartHandle = isStartHandle,
488 previousSelection = selection
489 )
490 if (newSelection != selection) onSelectionChange(newSelection)
491 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700492}
493
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200494internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700495 return lhs?.merge(rhs) ?: rhs
496}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800497
haoyue6d80a12020-12-02 16:04:52 -0800498@OptIn(ExperimentalTextApi::class)
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200499internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800500 selectable: Selectable,
501 selection: Selection
502): AnnotatedString {
503 val currentText = selectable.getText()
504
505 return if (
506 selectable != selection.start.selectable &&
507 selectable != selection.end.selectable
508 ) {
509 // Select the full text content if the current selectable is between the
510 // start and the end selectables.
511 currentText
512 } else if (
513 selectable == selection.start.selectable &&
514 selectable == selection.end.selectable
515 ) {
516 // Select partial text content if the current selectable is the start and
517 // the end selectable.
518 if (selection.handlesCrossed) {
519 currentText.subSequence(selection.end.offset, selection.start.offset)
520 } else {
521 currentText.subSequence(selection.start.offset, selection.end.offset)
522 }
523 } else if (selectable == selection.start.selectable) {
524 // Select partial text content if the current selectable is the start
525 // selectable.
526 if (selection.handlesCrossed) {
527 currentText.subSequence(0, selection.start.offset)
528 } else {
529 currentText.subSequence(selection.start.offset, currentText.length)
530 }
531 } else {
532 // Selectable partial text content if the current selectable is the end
533 // selectable.
534 if (selection.handlesCrossed) {
535 currentText.subSequence(selection.end.offset, currentText.length)
536 } else {
537 currentText.subSequence(0, selection.end.offset)
538 }
539 }
540}