[go: nahoru, domu]

blob: 53acc0bdd96bc56695abebda0be624d1481b7b4f [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
Louis Pullen-Freilich534385a2020-08-14 14:10:12 +010043import androidx.compose.ui.platform.ClipboardManager
Louis Pullen-Freilicha03fd6c2020-07-24 23:26:29 +010044import androidx.compose.ui.platform.TextToolbar
45import androidx.compose.ui.platform.TextToolbarStatus
Louis Pullen-Freilichab194752020-07-21 22:21:22 +010046import androidx.compose.ui.text.AnnotatedString
Nader Jawade6a9b332020-05-21 13:49:20 -070047import kotlin.math.max
48import kotlin.math.min
Qingqing Deng6f56a912019-05-13 10:10:37 -070049
Qingqing Deng35f97ea2019-09-18 19:24:37 -070050/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010051 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070052 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070053internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Qingqing Deng6f56a912019-05-13 10:10:37 -070054 /**
55 * The current selection.
56 */
57 var selection: Selection? = null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010058 set(value) {
59 field = value
haoyu9085c882020-12-08 12:01:06 -080060 if (value != null) {
61 updateHandleOffsets()
62 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +010063 }
Qingqing Deng6f56a912019-05-13 10:10:37 -070064
65 /**
Andrey Rudenko8b4981e2021-01-29 15:31:59 +010066 * Is touch mode active
67 */
68 var touchMode: Boolean = true
69
70 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070071 * The manager will invoke this every time it comes to the conclusion that the selection should
72 * change. The expectation is that this callback will end up causing `setSelection` to get
73 * called. This is what makes this a "controlled component".
74 */
75 var onSelectionChange: (Selection?) -> Unit = {}
76
77 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080078 * [HapticFeedback] handle to perform haptic feedback.
79 */
80 var hapticFeedBack: HapticFeedback? = null
81
82 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070083 * [ClipboardManager] to perform clipboard features.
84 */
85 var clipboardManager: ClipboardManager? = null
86
87 /**
Qingqing Denga89450d2020-04-03 19:11:49 -070088 * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
89 */
90 var textToolbar: TextToolbar? = null
91
92 /**
haoyuc40d02752021-01-25 17:32:47 -080093 * Focus requester used to request focus when selection becomes active.
94 */
95 var focusRequester: FocusRequester = FocusRequester()
96
97 /**
haoyu3c3fb452021-02-18 01:01:14 -080098 * Return true if the corresponding SelectionContainer is focused.
haoyuc40d02752021-01-25 17:32:47 -080099 */
haoyu3c3fb452021-02-18 01:01:14 -0800100 var hasFocus: Boolean by mutableStateOf(false)
haoyuc40d02752021-01-25 17:32:47 -0800101
102 /**
103 * Modifier for selection container.
104 */
105 val modifier get() = Modifier
106 .onGloballyPositioned { containerLayoutCoordinates = it }
107 .focusRequester(focusRequester)
108 .onFocusChanged { focusState ->
haoyu3c3fb452021-02-18 01:01:14 -0800109 if (!focusState.isFocused && hasFocus) {
haoyuc40d02752021-01-25 17:32:47 -0800110 onRelease()
111 }
haoyu3c3fb452021-02-18 01:01:14 -0800112 hasFocus = focusState.isFocused
haoyuc40d02752021-01-25 17:32:47 -0800113 }
haoyu3c3fb452021-02-18 01:01:14 -0800114 .focusable()
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100115 .onKeyEvent {
116 if (isCopyKeyEvent(it)) {
117 copy()
118 true
119 } else {
120 false
121 }
122 }
haoyuc40d02752021-01-25 17:32:47 -0800123
124 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700125 * Layout Coordinates of the selection container.
126 */
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100127 var containerLayoutCoordinates: LayoutCoordinates? = null
128 set(value) {
129 field = value
haoyu3c3fb452021-02-18 01:01:14 -0800130 if (hasFocus) {
131 updateHandleOffsets()
132 updateSelectionToolbarPosition()
133 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100134 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700135
136 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -0700137 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
138 * recalculated.
139 */
Nader Jawad6df06122020-06-03 15:27:08 -0700140 private var dragBeginPosition = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700141
142 /**
143 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
144 * it will be zeroed out.
145 */
Nader Jawad6df06122020-06-03 15:27:08 -0700146 private var dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700147
148 /**
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100149 * The calculated position of the start handle in the [SelectionContainer] coordinates. It
150 * is null when handle shouldn't be displayed.
151 * It is a [State] so reading it during the composition will cause recomposition every time
152 * the position has been changed.
153 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700154 var startHandlePosition by mutableStateOf<Offset?>(
155 null,
156 policy = structuralEqualityPolicy()
157 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100158 private set
159
160 /**
161 * The calculated position of the end handle in the [SelectionContainer] coordinates. It
162 * is null when handle shouldn't be displayed.
163 * It is a [State] so reading it during the composition will cause recomposition every time
164 * the position has been changed.
165 */
Chuck Jazdzewski0a90de92020-05-21 10:03:47 -0700166 var endHandlePosition by mutableStateOf<Offset?>(
167 null,
168 policy = structuralEqualityPolicy()
169 )
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100170 private set
171
172 init {
173 selectionRegistrar.onPositionChangeCallback = {
174 updateHandleOffsets()
haoyue6d80a12020-12-02 16:04:52 -0800175 updateSelectionToolbarPosition()
176 }
177
178 selectionRegistrar.onSelectionUpdateStartCallback = { layoutCoordinates, startPosition ->
179 updateSelection(
180 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
181 endPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
182 isStartHandle = true,
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100183 longPress = touchMode
haoyue6d80a12020-12-02 16:04:52 -0800184 )
haoyuc40d02752021-01-25 17:32:47 -0800185 focusRequester.requestFocus()
haoyu3c3fb452021-02-18 01:01:14 -0800186 hideSelectionToolbar()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100187 }
haoyue6d80a12020-12-02 16:04:52 -0800188
189 selectionRegistrar.onSelectionUpdateCallback =
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700190 { layoutCoordinates, startPosition, endPosition ->
191 updateSelection(
192 startPosition = convertToContainerCoordinates(layoutCoordinates, startPosition),
193 endPosition = convertToContainerCoordinates(layoutCoordinates, endPosition),
haoyu70aed732020-12-15 07:10:56 -0800194 isStartHandle = false,
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100195 longPress = touchMode
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700196 )
197 }
haoyue6d80a12020-12-02 16:04:52 -0800198
199 selectionRegistrar.onSelectionUpdateEndCallback = {
200 showSelectionToolbar()
201 }
haoyu9085c882020-12-08 12:01:06 -0800202
haoyue04245e2021-03-08 14:52:56 -0800203 selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
204 if (selectableKey in selectionRegistrar.subselections) {
haoyu9085c882020-12-08 12:01:06 -0800205 // clear the selection range of each Selectable.
206 onRelease()
207 selection = null
208 }
209 }
haoyue04245e2021-03-08 14:52:56 -0800210
211 selectionRegistrar.afterSelectableUnsubscribe = { selectableKey ->
212 if (
213 selectableKey == selection?.start?.selectableId ||
214 selectableKey == selection?.end?.selectableId
215 ) {
216 // The selectable that contains a selection handle just unsubscribed.
217 // Hide selection handles for now
218 startHandlePosition = null
219 endHandlePosition = null
220 }
221 }
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100222 }
223
224 private fun updateHandleOffsets() {
225 val selection = selection
226 val containerCoordinates = containerLayoutCoordinates
haoyue04245e2021-03-08 14:52:56 -0800227 val startSelectable = selection?.start?.selectableId?.let {
228 selectionRegistrar.selectableMap[it]
229 }
230 val endSelectable = selection?.end?.selectableId?.let {
231 selectionRegistrar.selectableMap[it]
232 }
233 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
234 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
haoyue2678c62020-12-09 08:39:12 -0800235 if (
236 selection == null ||
237 containerCoordinates == null ||
238 !containerCoordinates.isAttached ||
239 startLayoutCoordinates == null ||
240 endLayoutCoordinates == null
241 ) {
242 this.startHandlePosition = null
243 this.endHandlePosition = null
244 return
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100245 }
haoyue2678c62020-12-09 08:39:12 -0800246
George Mount77ca2a22020-12-11 17:46:19 +0000247 val startHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800248 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800249 startSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800250 selection = selection,
251 isStartHandle = true
252 )
253 )
George Mount77ca2a22020-12-11 17:46:19 +0000254 val endHandlePosition = containerCoordinates.localPositionOf(
haoyue2678c62020-12-09 08:39:12 -0800255 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800256 endSelectable.getHandlePosition(
haoyue2678c62020-12-09 08:39:12 -0800257 selection = selection,
258 isStartHandle = false
259 )
260 )
261
262 val visibleBounds = containerCoordinates.visibleBounds()
263 this.startHandlePosition =
264 if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
265 this.endHandlePosition =
266 if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100267 }
268
269 /**
270 * Returns non-nullable [containerLayoutCoordinates].
271 */
272 internal fun requireContainerCoordinates(): LayoutCoordinates {
273 val coordinates = containerLayoutCoordinates
274 require(coordinates != null)
275 require(coordinates.isAttached)
276 return coordinates
277 }
278
279 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -0700280 * Iterates over the handlers, gets the selection for each Composable, and merges all the
281 * returned [Selection]s.
282 *
Nader Jawad6df06122020-06-03 15:27:08 -0700283 * @param startPosition [Offset] for the start of the selection
284 * @param endPosition [Offset] for the end of the selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800285 * @param longPress the selection is a result of long press
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800286 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700287 *
haoyue04245e2021-03-08 14:52:56 -0800288 * @return a [Pair] of a [Selection] object which is constructed by combining all
289 * composables that are selected and a [Map] from selectable key to [Selection]s on the
290 * [Selectable] corresponding to the that key.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700291 */
Qingqing Deng5819ff62019-11-18 15:26:23 -0800292 // This function is internal for testing purposes.
293 internal fun mergeSelections(
Nader Jawad6df06122020-06-03 15:27:08 -0700294 startPosition: Offset,
295 endPosition: Offset,
Siyamed Sinir0100f122019-11-16 00:23:12 -0800296 longPress: Boolean = false,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800297 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800298 isStartHandle: Boolean = true
haoyue04245e2021-03-08 14:52:56 -0800299 ): Pair<Selection?, Map<Long, Selection>> {
300 val subselections = mutableMapOf<Long, Selection>()
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100301 val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
haoyue04245e2021-03-08 14:52:56 -0800302 .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
303 val selection = selectable.getSelection(
304 startPosition = startPosition,
305 endPosition = endPosition,
306 containerLayoutCoordinates = requireContainerCoordinates(),
307 longPress = longPress,
308 previousSelection = previousSelection,
309 isStartHandle = isStartHandle
Siyamed Sinir700df8452019-10-22 20:23:58 -0700310 )
haoyue04245e2021-03-08 14:52:56 -0800311 selection?.let { subselections[selectable.selectableId] = it }
312 merge(mergedSelection, selection)
Qingqing Deng247f2b42019-12-12 19:48:37 -0800313 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800314 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
315 HapticFeedbackType.TextHandleMove
316 )
haoyue04245e2021-03-08 14:52:56 -0800317 return Pair(newSelection, subselections)
Siyamed Sinir472c3162019-10-21 23:41:00 -0700318 }
319
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800320 internal fun getSelectedText(): AnnotatedString? {
Andrey Kulikovb8c7e052020-04-15 19:20:09 +0100321 val selectables = selectionRegistrar.sort(requireContainerCoordinates())
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800322 var selectedText: AnnotatedString? = null
323
324 selection?.let {
Louis Pullen-Freilichc6e92aa2021-03-08 00:42:16 +0000325 for (i in selectables.indices) {
haoyue04245e2021-03-08 14:52:56 -0800326 val selectable = selectables[i]
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800327 // Continue if the current selectable is before the selection starts.
haoyue04245e2021-03-08 14:52:56 -0800328 if (selectable.selectableId != it.start.selectableId &&
329 selectable.selectableId != it.end.selectableId &&
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800330 selectedText == null
331 ) continue
332
333 val currentSelectedText = getCurrentSelectedText(
haoyue04245e2021-03-08 14:52:56 -0800334 selectable = selectable,
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800335 selection = it
336 )
337 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
338
339 // Break if the current selectable is the last selected selectable.
haoyue04245e2021-03-08 14:52:56 -0800340 if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
341 selectable.selectableId == it.start.selectableId && it.handlesCrossed
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800342 ) break
343 }
344 }
345 return selectedText
346 }
347
Qingqing Dengde023cc2020-04-24 14:23:41 -0700348 internal fun copy() {
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700349 val selectedText = getSelectedText()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700350 selectedText?.let { clipboardManager?.setText(it) }
351 }
352
353 /**
354 * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
355 * to make the FloatingToolbar show up in the proper place. In addition, this function passes
356 * the copy method as a callback when "copy" is clicked.
357 */
358 internal fun showSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800359 if (hasFocus) {
360 selection?.let {
361 textToolbar?.showMenu(
362 getContentRect(),
363 onCopyRequested = {
364 copy()
365 onRelease()
366 }
367 )
368 }
Qingqing Denga89450d2020-04-03 19:11:49 -0700369 }
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700370 }
371
haoyue6d80a12020-12-02 16:04:52 -0800372 internal fun hideSelectionToolbar() {
haoyu3c3fb452021-02-18 01:01:14 -0800373 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
haoyue6d80a12020-12-02 16:04:52 -0800374 textToolbar?.hide()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700375 }
376 }
377
378 private fun updateSelectionToolbarPosition() {
haoyu3c3fb452021-02-18 01:01:14 -0800379 if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
Qingqing Dengde023cc2020-04-24 14:23:41 -0700380 showSelectionToolbar()
381 }
382 }
383
384 /**
385 * Calculate selected region as [Rect]. The top is the top of the first selected
386 * line, and the bottom is the bottom of the last selected line. The left is the leftmost
387 * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
388 */
389 private fun getContentRect(): Rect {
Nader Jawad8432b8d2020-07-27 14:51:23 -0700390 val selection = selection ?: return Rect.Zero
haoyue04245e2021-03-08 14:52:56 -0800391 val startSelectable = selectionRegistrar.selectableMap[selection.start.selectableId]
392 val endSelectable = selectionRegistrar.selectableMap[selection.start.selectableId]
393 val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
394 val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700395
396 val localLayoutCoordinates = containerLayoutCoordinates
397 if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
George Mount77ca2a22020-12-11 17:46:19 +0000398 var startOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700399 startLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800400 startSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700401 selection = selection,
402 isStartHandle = true
403 )
404 )
George Mount77ca2a22020-12-11 17:46:19 +0000405 var endOffset = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700406 endLayoutCoordinates,
haoyue04245e2021-03-08 14:52:56 -0800407 endSelectable.getHandlePosition(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700408 selection = selection,
409 isStartHandle = false
410 )
411 )
412
413 startOffset = localLayoutCoordinates.localToRoot(startOffset)
414 endOffset = localLayoutCoordinates.localToRoot(endOffset)
415
416 val left = min(startOffset.x, endOffset.x)
417 val right = max(startOffset.x, endOffset.x)
418
George Mount77ca2a22020-12-11 17:46:19 +0000419 var startTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700420 startLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700421 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700422 0f,
haoyue04245e2021-03-08 14:52:56 -0800423 startSelectable.getBoundingBox(selection.start.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700424 )
425 )
426
George Mount77ca2a22020-12-11 17:46:19 +0000427 var endTop = localLayoutCoordinates.localPositionOf(
Qingqing Dengde023cc2020-04-24 14:23:41 -0700428 endLayoutCoordinates,
Nader Jawad6df06122020-06-03 15:27:08 -0700429 Offset(
Nader Jawade6a9b332020-05-21 13:49:20 -0700430 0.0f,
haoyue04245e2021-03-08 14:52:56 -0800431 endSelectable.getBoundingBox(selection.end.offset).top
Qingqing Dengde023cc2020-04-24 14:23:41 -0700432 )
433 )
434
435 startTop = localLayoutCoordinates.localToRoot(startTop)
436 endTop = localLayoutCoordinates.localToRoot(endTop)
437
438 val top = min(startTop.y, endTop.y)
Nader Jawad16e330a2020-05-21 21:21:01 -0700439 val bottom = max(startOffset.y, endOffset.y) + (HANDLE_HEIGHT.value * 4.0).toFloat()
Qingqing Dengde023cc2020-04-24 14:23:41 -0700440
441 return Rect(
Nader Jawade6a9b332020-05-21 13:49:20 -0700442 left,
443 top,
444 right,
445 bottom
Qingqing Dengde023cc2020-04-24 14:23:41 -0700446 )
447 }
Nader Jawad8432b8d2020-07-27 14:51:23 -0700448 return Rect.Zero
Qingqing Dengde023cc2020-04-24 14:23:41 -0700449 }
450
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800451 // This is for PressGestureDetector to cancel the selection.
452 fun onRelease() {
haoyue04245e2021-03-08 14:52:56 -0800453 selectionRegistrar.subselections = emptyMap()
haoyue6d80a12020-12-02 16:04:52 -0800454 hideSelectionToolbar()
haoyue04245e2021-03-08 14:52:56 -0800455 if (selection != null) {
456 onSelectionChange(null)
457 hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
458 }
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800459 }
460
Siyamed Sinir472c3162019-10-21 23:41:00 -0700461 fun handleDragObserver(isStartHandle: Boolean): DragObserver {
Qingqing Deng6f56a912019-05-13 10:10:37 -0700462 return object : DragObserver {
Nader Jawad6df06122020-06-03 15:27:08 -0700463 override fun onStart(downPosition: Offset) {
haoyue6d80a12020-12-02 16:04:52 -0800464 hideSelectionToolbar()
Siyamed Sinir472c3162019-10-21 23:41:00 -0700465 val selection = selection!!
haoyue04245e2021-03-08 14:52:56 -0800466 val startSelectable =
467 selectionRegistrar.selectableMap[selection.start.selectableId]
468 val endSelectable =
469 selectionRegistrar.selectableMap[selection.end.selectableId]
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100470 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700471 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100472 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700473 val beginLayoutCoordinates = if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800474 startSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700475 } else {
haoyue04245e2021-03-08 14:52:56 -0800476 endSelectable?.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700477 }
478
Qingqing Deng6f56a912019-05-13 10:10:37 -0700479 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100480 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700481 val beginCoordinates = getAdjustedCoordinates(
haoyue6d80a12020-12-02 16:04:52 -0800482 if (isStartHandle) {
haoyue04245e2021-03-08 14:52:56 -0800483 startSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800484 selection = selection, isStartHandle = true
haoyue6d80a12020-12-02 16:04:52 -0800485 )
486 } else {
haoyue04245e2021-03-08 14:52:56 -0800487 endSelectable!!.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800488 selection = selection, isStartHandle = false
489 )
haoyue6d80a12020-12-02 16:04:52 -0800490 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700491 )
492
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100493 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700494 // selection container coordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000495 dragBeginPosition = requireContainerCoordinates().localPositionOf(
Qingqing Deng6f56a912019-05-13 10:10:37 -0700496 beginLayoutCoordinates,
497 beginCoordinates
498 )
499
500 // Zero out the total distance that being dragged.
Nader Jawad6df06122020-06-03 15:27:08 -0700501 dragTotalDistance = Offset.Zero
Qingqing Deng6f56a912019-05-13 10:10:37 -0700502 }
503
Nader Jawad6df06122020-06-03 15:27:08 -0700504 override fun onDrag(dragDistance: Offset): Offset {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700505 val selection = selection!!
Qingqing Deng6f56a912019-05-13 10:10:37 -0700506 dragTotalDistance += dragDistance
haoyue04245e2021-03-08 14:52:56 -0800507 val startSelectable =
508 selectionRegistrar.selectableMap[selection.start.selectableId]
509 val endSelectable =
510 selectionRegistrar.selectableMap[selection.end.selectableId]
Siyamed Sinir472c3162019-10-21 23:41:00 -0700511 val currentStart = if (isStartHandle) {
512 dragBeginPosition + dragTotalDistance
513 } else {
George Mount77ca2a22020-12-11 17:46:19 +0000514 requireContainerCoordinates().localPositionOf(
haoyue04245e2021-03-08 14:52:56 -0800515 startSelectable?.getLayoutCoordinates()!!,
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800516 getAdjustedCoordinates(
haoyue04245e2021-03-08 14:52:56 -0800517 startSelectable.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800518 selection = selection,
519 isStartHandle = true
520 )
521 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700522 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700523 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700524
525 val currentEnd = if (isStartHandle) {
George Mount77ca2a22020-12-11 17:46:19 +0000526 requireContainerCoordinates().localPositionOf(
haoyue04245e2021-03-08 14:52:56 -0800527 endSelectable?.getLayoutCoordinates()!!,
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800528 getAdjustedCoordinates(
haoyue04245e2021-03-08 14:52:56 -0800529 endSelectable.getHandlePosition(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800530 selection = selection,
531 isStartHandle = false
532 )
533 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700534 )
535 } else {
536 dragBeginPosition + dragTotalDistance
537 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700538 updateSelection(
Qingqing Denga5d80952019-10-11 16:46:52 -0700539 startPosition = currentStart,
540 endPosition = currentEnd,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800541 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700542 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700543 return dragDistance
544 }
haoyue6d80a12020-12-02 16:04:52 -0800545
546 override fun onStop(velocity: Offset) {
547 showSelectionToolbar()
548 }
549
550 override fun onCancel() {
551 showSelectionToolbar()
552 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700553 }
554 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700555
556 private fun convertToContainerCoordinates(
557 layoutCoordinates: LayoutCoordinates,
558 offset: Offset
559 ): Offset? {
560 val coordinates = containerLayoutCoordinates
561 if (coordinates == null || !coordinates.isAttached) return null
George Mount77ca2a22020-12-11 17:46:19 +0000562 return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700563 }
564
565 private fun updateSelection(
566 startPosition: Offset?,
567 endPosition: Offset?,
568 longPress: Boolean = false,
569 isStartHandle: Boolean = true
570 ) {
571 if (startPosition == null || endPosition == null) return
haoyue04245e2021-03-08 14:52:56 -0800572 val (newSelection, newSubselection) = mergeSelections(
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700573 startPosition = startPosition,
574 endPosition = endPosition,
575 longPress = longPress,
576 isStartHandle = isStartHandle,
577 previousSelection = selection
578 )
haoyue04245e2021-03-08 14:52:56 -0800579 if (newSelection != selection) {
580 selectionRegistrar.subselections = newSubselection
581 onSelectionChange(newSelection)
582 }
Qingqing Deng739ec3a2020-09-09 15:40:30 -0700583 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700584}
585
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200586internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
Siyamed Sinir700df8452019-10-22 20:23:58 -0700587 return lhs?.merge(rhs) ?: rhs
588}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800589
Andrey Rudenko8b4981e2021-01-29 15:31:59 +0100590internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
591
Andrey Rudenko5dc7aad2020-09-18 10:33:37 +0200592internal fun getCurrentSelectedText(
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800593 selectable: Selectable,
594 selection: Selection
595): AnnotatedString {
596 val currentText = selectable.getText()
597
598 return if (
haoyue04245e2021-03-08 14:52:56 -0800599 selectable.selectableId != selection.start.selectableId &&
600 selectable.selectableId != selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800601 ) {
602 // Select the full text content if the current selectable is between the
603 // start and the end selectables.
604 currentText
605 } else if (
haoyue04245e2021-03-08 14:52:56 -0800606 selectable.selectableId == selection.start.selectableId &&
607 selectable.selectableId == selection.end.selectableId
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800608 ) {
609 // Select partial text content if the current selectable is the start and
610 // the end selectable.
611 if (selection.handlesCrossed) {
612 currentText.subSequence(selection.end.offset, selection.start.offset)
613 } else {
614 currentText.subSequence(selection.start.offset, selection.end.offset)
615 }
haoyue04245e2021-03-08 14:52:56 -0800616 } else if (selectable.selectableId == selection.start.selectableId) {
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800617 // Select partial text content if the current selectable is the start
618 // selectable.
619 if (selection.handlesCrossed) {
620 currentText.subSequence(0, selection.start.offset)
621 } else {
622 currentText.subSequence(selection.start.offset, currentText.length)
623 }
624 } else {
625 // Selectable partial text content if the current selectable is the end
626 // selectable.
627 if (selection.handlesCrossed) {
628 currentText.subSequence(selection.end.offset, currentText.length)
629 } else {
630 currentText.subSequence(0, selection.end.offset)
631 }
632 }
633}
haoyue2678c62020-12-09 08:39:12 -0800634
635/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
haoyuac341f02021-01-22 22:01:56 -0800636internal fun LayoutCoordinates.visibleBounds(): Rect {
haoyue2678c62020-12-09 08:39:12 -0800637 // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
638 // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
639 // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
George Mount77ca2a22020-12-11 17:46:19 +0000640 val boundsInWindow = boundsInWindow()
haoyue2678c62020-12-09 08:39:12 -0800641 return Rect(
George Mount77ca2a22020-12-11 17:46:19 +0000642 windowToLocal(boundsInWindow.topLeft),
643 windowToLocal(boundsInWindow.bottomRight)
haoyue2678c62020-12-09 08:39:12 -0800644 )
645}
646
haoyuac341f02021-01-22 22:01:56 -0800647internal fun Rect.containsInclusive(offset: Offset): Boolean =
haoyue2678c62020-12-09 08:39:12 -0800648 offset.x in left..right && offset.y in top..bottom