[go: nahoru, domu]

blob: 50db4c21f37406ae0e62b42c30d76f5e3b0829ff [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
haoyu9085c882020-12-08 12:01:06 -080055 if (value != null) {
56 updateHandleOffsets()
57 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010058 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070059
60 /**
61 * The manager will invoke this every time it comes to the conclusion that the selection should
62 * change. The expectation is that this callback will end up causing `setSelection` to get
63 * called. This is what makes this a "controlled component".
64 */
65 var onSelectionChange: (Selection?) -> Unit = {}
66
67 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080068 * [HapticFeedback] handle to perform haptic feedback.
69 */
70 var hapticFeedBack: HapticFeedback? = null
71
72 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070073 * [ClipboardManager] to perform clipboard features.
74 */
75 var clipboardManager: ClipboardManager? = null
76
77 /**
Qingqing Denga89450d2020-04-03 19:11:49 -070078 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
79 */
80 var textToolbar: TextToolbar? = null
81
82 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070083 * Layout Coordinates of the selection container.
84 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010085 var containerLayoutCoordinates: LayoutCoordinates? = null
86 set(value) {
87 field = value
88 updateHandleOffsets()
Qingqing Dengde023cc2020-04-24 14:23:41 -070089 updateSelectionToolbarPosition()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010090 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070091
92 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070093 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
94 * recalculated.
95 */
Nader Jawad6df06122020-06-03 15:27:08 -070096 private var dragBeginPosition = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -070097
98 /**
99 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
100 * it will be zeroed out.
101 */
Nader Jawad6df06122020-06-03 15:27:08 -0700102 private var dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700103
104 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100105 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
106 * is null when handle shouldn't be displayed.
107 * It is a [State] so reading it during the composition will cause recomposition every time
108 * the position has been changed.
109 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700110 var startHandlePosition by mutableStateOf<Offset?>(
111 null,
112 policy = structuralEqualityPolicy()
113 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100114 private set
115
116 /**
117 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
118 * is null when handle shouldn't be displayed.
119 * It is a [State] so reading it during the composition will cause recomposition every time
120 * the position has been changed.
121 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700122 var endHandlePosition by mutableStateOf<Offset?>(
123 null,
124 policy = structuralEqualityPolicy()
125 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100126 private set
127
128 init {
129 selectionRegistrar.onPositionChangeCallback = {
130 updateHandleOffsets()
haoyue6d80a12020-12-02 16:04:52 -0800131 updateSelectionToolbarPosition()
132 }
133
134 selectionRegistrar.onSelectionUpdateStartCallback = { layoutCoordinates, startPosition ->
135 updateSelection(
136 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
137 endPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
138 isStartHandle = true,
139 longPress = true
140 )
Qingqing Dengde023cc2020-04-24 14:23:41 -0700141 hideSelectionToolbar()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100142 }
haoyue6d80a12020-12-02 16:04:52 -0800143
144 selectionRegistrar.onSelectionUpdateCallback =
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700145 { layoutCoordinates, startPosition, endPosition ->
146 updateSelection(
147 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
148 endPosition = convertToContainerCoordinates(layoutCoordinates, endPosition),
149 isStartHandle = true,
150 longPress = true
151 )
152 }
haoyue6d80a12020-12-02 16:04:52 -0800153
154 selectionRegistrar.onSelectionUpdateEndCallback = {
155 showSelectionToolbar()
156 }
haoyu9085c882020-12-08 12:01:06 -0800157
158 selectionRegistrar.onSelectableChangeCallback = { selectable ->
159 if (selectable in selectionRegistrar.selectables) {
160 // clear the selection range of each Selectable.
161 onRelease()
162 selection = null
163 }
164 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100165 }
166
167 private fun updateHandleOffsets() {
168 val selection = selection
169 val containerCoordinates = containerLayoutCoordinates
170 if (selection != null && containerCoordinates != null && containerCoordinates.isAttached) {
171 val startLayoutCoordinates = selection.start.selectable.getLayoutCoordinates()
172 val endLayoutCoordinates = selection.end.selectable.getLayoutCoordinates()
173
174 if (startLayoutCoordinates != null && endLayoutCoordinates != null) {
175 startHandlePosition = containerCoordinates.childToLocal(
176 startLayoutCoordinates,
177 selection.start.selectable.getHandlePosition(
178 selection = selection,
179 isStartHandle = true
180 )
181 )
182 endHandlePosition = containerCoordinates.childToLocal(
183 endLayoutCoordinates,
184 selection.end.selectable.getHandlePosition(
185 selection = selection,
186 isStartHandle = false
187 )
188 )
189 return
190 }
191 }
192 startHandlePosition = null
193 endHandlePosition = null
194 }
195
196 /**
197 * Returns non-nullable [containerLayoutCoordinates].
198 */
199 internal fun requireContainerCoordinates(): LayoutCoordinates {
200 val coordinates = containerLayoutCoordinates
201 require(coordinates != null)
202 require(coordinates.isAttached)
203 return coordinates
204 }
205
206 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -0700207 * Iterates over the handlers, gets the selection for each Composable, and merges all the
208 * returned [Selection]s.
209 *
Nader Jawad6df06122020-06-03 15:27:08 -0700210 * @param startPosition [Offset] for the start of the selection
211 * @param endPosition [Offset] for the end of the selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800212 * @param longPress the selection is a result of long press
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800213 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700214 *
215 * @return [Selection] object which is constructed by combining all Composables that are
216 * selected.
217 */
Qingqing Deng5819ff62019-11-18 15:26:23 -0800218 // This function is internal for testing purposes.
219 internal fun mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700220 startPosition: Offset,
221 endPosition: Offset,
Siyamed Sinir0100f122019-11-16 00:23:12 -0800222 longPress: Boolean = false,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800223 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800224 isStartHandle: Boolean = true
Siyamed Sinir472c3162019-10-21 23:41:00 -0700225 ): Selection? {
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800226
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100227 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng247f2b42019-12-12 19:48:37 -0800228 .fold(null) { mergedSelection: Selection?,
Jeff Gastona3bbb7e2020-09-18 15:11:15 -0400229 handler: Selectable ->
Qingqing Deng247f2b42019-12-12 19:48:37 -0800230 merge(
231 mergedSelection,
232 handler.getSelection(
233 startPosition = startPosition,
234 endPosition = endPosition,
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100235 containerLayoutCoordinates = requireContainerCoordinates(),
Qingqing Deng247f2b42019-12-12 19:48:37 -0800236 longPress = longPress,
237 previousSelection = previousSelection,
238 isStartHandle = isStartHandle
239 )
Siyamed Sinir700df8452019-10-22 20:23:58 -0700240 )
Qingqing Deng247f2b42019-12-12 19:48:37 -0800241 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800242 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
243 HapticFeedbackType.TextHandleMove
244 )
245 return newSelection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700246 }
247
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800248 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100249 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800250 var selectedText: AnnotatedString? = null
251
252 selection?.let {
253 for (handler in selectables) {
254 // Continue if the current selectable is before the selection starts.
255 if (handler != it.start.selectable && handler != it.end.selectable &&
256 selectedText == null
257 ) continue
258
259 val currentSelectedText = getCurrentSelectedText(
260 selectable = handler,
261 selection = it
262 )
263 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
264
265 // Break if the current selectable is the last selected selectable.
266 if (handler == it.end.selectable && !it.handlesCrossed ||
267 handler == it.start.selectable && it.handlesCrossed
268 ) break
269 }
270 }
271 return selectedText
272 }
273
Qingqing Dengde023cc2020-04-24 14:23:41 -0700274 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700275 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700276 selectedText?.let { clipboardManager?.setText(it) }
277 }
278
279 /**
280 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
281 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
282 * the copy method as a callback when "copy" is clicked.
283 */
284 internal fun showSelectionToolbar() {
285 selection?.let {
Qingqing Deng40519572020-07-10 13:25:36 -0700286 textToolbar?.showMenu(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700287 getContentRect(),
Qingqing Deng40519572020-07-10 13:25:36 -0700288 onCopyRequested = {
289 copy()
290 onRelease()
291 }
Qingqing Dengce140562020-04-24 14:46:22 -0700292 )
Qingqing Denga89450d2020-04-03 19:11:49 -0700293 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700294 }
295
haoyue6d80a12020-12-02 16:04:52 -0800296 internal fun hideSelectionToolbar() {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700297 if (textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800298 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700299 }
300 }
301
302 private fun updateSelectionToolbarPosition() {
303 if (textToolbar?.status == TextToolbarStatus.Shown) {
304 showSelectionToolbar()
305 }
306 }
307
308 /**
309 * Calculate selected region as [Rect]. The top is the top of the first selected
310 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
311 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
312 */
313 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700314 val selection = selection ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700315 val startLayoutCoordinates =
Nader Jawad8432b8d2020-07-27 14:51:23 -0700316 selection.start.selectable.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700317 val endLayoutCoordinates =
Nader Jawad8432b8d2020-07-27 14:51:23 -0700318 selection.end.selectable.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700319
320 val localLayoutCoordinates = containerLayoutCoordinates
321 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
322 var startOffset = localLayoutCoordinates.childToLocal(
323 startLayoutCoordinates,
324 selection.start.selectable.getHandlePosition(
325 selection = selection,
326 isStartHandle = true
327 )
328 )
329 var endOffset = localLayoutCoordinates.childToLocal(
330 endLayoutCoordinates,
331 selection.end.selectable.getHandlePosition(
332 selection = selection,
333 isStartHandle = false
334 )
335 )
336
337 startOffset = localLayoutCoordinates.localToRoot(startOffset)
338 endOffset = localLayoutCoordinates.localToRoot(endOffset)
339
340 val left = min(startOffset.x, endOffset.x)
341 val right = max(startOffset.x, endOffset.x)
342
343 var startTop = localLayoutCoordinates.childToLocal(
344 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700345 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700346 0f,
347 selection.start.selectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700348 )
349 )
350
351 var endTop = localLayoutCoordinates.childToLocal(
352 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700353 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700354 0.0f,
355 selection.end.selectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700356 )
357 )
358
359 startTop = localLayoutCoordinates.localToRoot(startTop)
360 endTop = localLayoutCoordinates.localToRoot(endTop)
361
362 val top = min(startTop.y, endTop.y)
Nader Jawad16e330a2020-05-21 21:21:01 -0700363 val bottom = max(startOffset.y, endOffset.y) + (HANDLE_HEIGHT.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700364
365 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700366 left,
367 top,
368 right,
369 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700370 )
371 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700372 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700373 }
374
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800375 // This is for PressGestureDetector to cancel the selection.
376 fun onRelease() {
377 // Call mergeSelections with an out of boundary input to inform all text widgets to
378 // cancel their individual selection.
Qingqing Denga5d80952019-10-11 16:46:52 -0700379 mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700380 startPosition = Offset(-1f, -1f),
381 endPosition = Offset(-1f, -1f),
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800382 previousSelection = selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800383 )
haoyue6d80a12020-12-02 16:04:52 -0800384 hideSelectionToolbar()
Siyamed Sinire810eab2019-11-22 12:36:38 -0800385 if (selection != null) onSelectionChange(null)
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800386 }
387
Siyamed Sinir472c3162019-10-21 23:41:00 -0700388 fun handleDragObserver(isStartHandle: Boolean): DragObserver {
Qingqing Deng6f56a912019-05-13 10:10:37 -0700389 return object : DragObserver {
Nader Jawad6df06122020-06-03 15:27:08 -0700390 override fun onStart(downPosition: Offset) {
haoyue6d80a12020-12-02 16:04:52 -0800391 hideSelectionToolbar()
Siyamed Sinir472c3162019-10-21 23:41:00 -0700392 val selection = selection!!
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100393 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700394 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100395 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700396 val beginLayoutCoordinates = if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800397 selection.start.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700398 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800399 selection.end.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700400 }
401
Qingqing Deng6f56a912019-05-13 10:10:37 -0700402 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100403 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700404 val beginCoordinates = getAdjustedCoordinates(
haoyue6d80a12020-12-02 16:04:52 -0800405 if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800406 selection.start.selectable.getHandlePosition(
407 selection = selection, isStartHandle = true
haoyue6d80a12020-12-02 16:04:52 -0800408 )
409 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800410 selection.end.selectable.getHandlePosition(
411 selection = selection, isStartHandle = false
412 )
haoyue6d80a12020-12-02 16:04:52 -0800413 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700414 )
415
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100416 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700417 // selection container coordinates.
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100418 dragBeginPosition = requireContainerCoordinates().childToLocal(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700419 beginLayoutCoordinates,
420 beginCoordinates
421 )
422
423 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700424 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700425 }
426
Nader Jawad6df06122020-06-03 15:27:08 -0700427 override fun onDrag(dragDistance: Offset): Offset {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700428 val selection = selection!!
Qingqing Deng6f56a912019-05-13 10:10:37 -0700429 dragTotalDistance += dragDistance
430
Siyamed Sinir472c3162019-10-21 23:41:00 -0700431 val currentStart = if (isStartHandle) {
432 dragBeginPosition + dragTotalDistance
433 } else {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100434 requireContainerCoordinates().childToLocal(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800435 selection.start.selectable.getLayoutCoordinates()!!,
436 getAdjustedCoordinates(
437 selection.start.selectable.getHandlePosition(
438 selection = selection,
439 isStartHandle = true
440 )
441 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700442 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700443 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700444
445 val currentEnd = if (isStartHandle) {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100446 requireContainerCoordinates().childToLocal(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800447 selection.end.selectable.getLayoutCoordinates()!!,
448 getAdjustedCoordinates(
449 selection.end.selectable.getHandlePosition(
450 selection = selection,
451 isStartHandle = false
452 )
453 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700454 )
455 } else {
456 dragBeginPosition + dragTotalDistance
457 }
458
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700459 updateSelection(
Qingqing Denga5d80952019-10-11 16:46:52 -0700460 startPosition = currentStart,
461 endPosition = currentEnd,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800462 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700463 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700464 return dragDistance
465 }
haoyue6d80a12020-12-02 16:04:52 -0800466
467 override fun onStop(velocity: Offset) {
468 showSelectionToolbar()
469 }
470
471 override fun onCancel() {
472 showSelectionToolbar()
473 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700474 }
475 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700476
477 private fun convertToContainerCoordinates(
478 layoutCoordinates: LayoutCoordinates,
479 offset: Offset
480 ): Offset? {
481 val coordinates = containerLayoutCoordinates
482 if (coordinates == null || !coordinates.isAttached) return null
483 return requireContainerCoordinates().childToLocal(layoutCoordinates, offset)
484 }
485
486 private fun updateSelection(
487 startPosition: Offset?,
488 endPosition: Offset?,
489 longPress: Boolean = false,
490 isStartHandle: Boolean = true
491 ) {
492 if (startPosition == null || endPosition == null) return
493 val newSelection = mergeSelections(
494 startPosition = startPosition,
495 endPosition = endPosition,
496 longPress = longPress,
497 isStartHandle = isStartHandle,
498 previousSelection = selection
499 )
500 if (newSelection != selection) onSelectionChange(newSelection)
501 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700502}
503
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200504internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700505 return lhs?.merge(rhs) ?: rhs
506}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800507
haoyue6d80a12020-12-02 16:04:52 -0800508@OptIn(ExperimentalTextApi::class)
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200509internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800510 selectable: Selectable,
511 selection: Selection
512): AnnotatedString {
513 val currentText = selectable.getText()
514
515 return if (
516 selectable != selection.start.selectable &&
517 selectable != selection.end.selectable
518 ) {
519 // Select the full text content if the current selectable is between the
520 // start and the end selectables.
521 currentText
522 } else if (
523 selectable == selection.start.selectable &&
524 selectable == selection.end.selectable
525 ) {
526 // Select partial text content if the current selectable is the start and
527 // the end selectable.
528 if (selection.handlesCrossed) {
529 currentText.subSequence(selection.end.offset, selection.start.offset)
530 } else {
531 currentText.subSequence(selection.start.offset, selection.end.offset)
532 }
533 } else if (selectable == selection.start.selectable) {
534 // Select partial text content if the current selectable is the start
535 // selectable.
536 if (selection.handlesCrossed) {
537 currentText.subSequence(0, selection.start.offset)
538 } else {
539 currentText.subSequence(selection.start.offset, currentText.length)
540 }
541 } else {
542 // Selectable partial text content if the current selectable is the end
543 // selectable.
544 if (selection.handlesCrossed) {
545 currentText.subSequence(selection.end.offset, currentText.length)
546 } else {
547 currentText.subSequence(0, selection.end.offset)
548 }
549 }
550}