1 /*
2  * Copyright 2024 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 
17 package androidx.compose.foundation.text.input.internal
18 
19 import android.graphics.PointF
20 import android.os.CancellationSignal
21 import android.view.inputmethod.DeleteGesture
22 import android.view.inputmethod.DeleteRangeGesture
23 import android.view.inputmethod.HandwritingGesture
24 import android.view.inputmethod.InputConnection
25 import android.view.inputmethod.InsertGesture
26 import android.view.inputmethod.JoinOrSplitGesture
27 import android.view.inputmethod.PreviewableHandwritingGesture
28 import android.view.inputmethod.RemoveSpaceGesture
29 import android.view.inputmethod.SelectGesture
30 import android.view.inputmethod.SelectRangeGesture
31 import androidx.annotation.RequiresApi
32 import androidx.compose.foundation.text.LegacyTextFieldState
33 import androidx.compose.foundation.text.input.TextHighlightType
34 import androidx.compose.foundation.text.selection.TextFieldSelectionManager
35 import androidx.compose.ui.geometry.Offset
36 import androidx.compose.ui.geometry.Rect
37 import androidx.compose.ui.graphics.toComposeRect
38 import androidx.compose.ui.layout.LayoutCoordinates
39 import androidx.compose.ui.platform.ViewConfiguration
40 import androidx.compose.ui.text.AnnotatedString
41 import androidx.compose.ui.text.MultiParagraph
42 import androidx.compose.ui.text.TextGranularity
43 import androidx.compose.ui.text.TextInclusionStrategy
44 import androidx.compose.ui.text.TextLayoutResult
45 import androidx.compose.ui.text.TextRange
46 import androidx.compose.ui.text.input.CommitTextCommand
47 import androidx.compose.ui.text.input.DeleteSurroundingTextCommand
48 import androidx.compose.ui.text.input.EditCommand
49 import androidx.compose.ui.text.input.EditingBuffer
50 import androidx.compose.ui.text.input.SetSelectionCommand
51 import androidx.compose.ui.text.substring
52 import kotlin.math.max
53 import kotlin.math.min
54 
55 @RequiresApi(34)
56 internal object HandwritingGestureApi34 {
performHandwritingGesturenull57     internal fun TransformedTextFieldState.performHandwritingGesture(
58         handwritingGesture: HandwritingGesture,
59         layoutState: TextLayoutState,
60         updateSelectionState: (() -> Unit)?,
61         viewConfiguration: ViewConfiguration?
62     ): Int {
63         return when (handwritingGesture) {
64             is SelectGesture ->
65                 performSelectGesture(handwritingGesture, layoutState, updateSelectionState)
66             is DeleteGesture -> performDeleteGesture(handwritingGesture, layoutState)
67             is SelectRangeGesture ->
68                 performSelectRangeGesture(handwritingGesture, layoutState, updateSelectionState)
69             is DeleteRangeGesture -> performDeleteRangeGesture(handwritingGesture, layoutState)
70             is JoinOrSplitGesture ->
71                 performJoinOrSplitGesture(handwritingGesture, layoutState, viewConfiguration)
72             is InsertGesture ->
73                 performInsertGesture(handwritingGesture, layoutState, viewConfiguration)
74             is RemoveSpaceGesture ->
75                 performRemoveSpaceGesture(handwritingGesture, layoutState, viewConfiguration)
76             else -> InputConnection.HANDWRITING_GESTURE_RESULT_UNSUPPORTED
77         }
78     }
79 
previewHandwritingGesturenull80     internal fun TransformedTextFieldState.previewHandwritingGesture(
81         handwritingGesture: PreviewableHandwritingGesture,
82         layoutState: TextLayoutState,
83         cancellationSignal: CancellationSignal?
84     ): Boolean {
85         when (handwritingGesture) {
86             is SelectGesture -> previewSelectGesture(handwritingGesture, layoutState)
87             is DeleteGesture -> previewDeleteGesture(handwritingGesture, layoutState)
88             is SelectRangeGesture -> previewSelectRangeGesture(handwritingGesture, layoutState)
89             is DeleteRangeGesture -> previewDeleteRangeGesture(handwritingGesture, layoutState)
90             else -> return false
91         }
92         cancellationSignal?.setOnCancelListener { editUntransformedTextAsUser { clearHighlight() } }
93         return true
94     }
95 
TransformedTextFieldStatenull96     private fun TransformedTextFieldState.performSelectGesture(
97         gesture: SelectGesture,
98         layoutState: TextLayoutState,
99         updateSelectionState: (() -> Unit)?,
100     ): Int {
101         val rangeInTransformedText =
102             layoutState
103                 .getRangeForScreenRect(
104                     gesture.selectionArea.toComposeRect(),
105                     gesture.granularity.toTextGranularity(),
106                     TextInclusionStrategy.ContainsCenter
107                 )
108                 .apply { if (collapsed) return fallback(gesture) }
109 
110         selectCharsIn(rangeInTransformedText)
111         updateSelectionState?.invoke()
112         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
113     }
114 
TransformedTextFieldStatenull115     private fun TransformedTextFieldState.previewSelectGesture(
116         gesture: SelectGesture,
117         layoutState: TextLayoutState
118     ) {
119         highlightRange(
120             layoutState.getRangeForScreenRect(
121                 gesture.selectionArea.toComposeRect(),
122                 gesture.granularity.toTextGranularity(),
123                 TextInclusionStrategy.ContainsCenter
124             ),
125             TextHighlightType.HandwritingSelectPreview
126         )
127     }
128 
performDeleteGesturenull129     private fun TransformedTextFieldState.performDeleteGesture(
130         gesture: DeleteGesture,
131         layoutState: TextLayoutState
132     ): Int {
133         val granularity = gesture.granularity.toTextGranularity()
134         val rangeInTransformedText =
135             layoutState
136                 .getRangeForScreenRect(
137                     gesture.deletionArea.toComposeRect(),
138                     granularity,
139                     TextInclusionStrategy.ContainsCenter
140                 )
141                 .apply { if (collapsed) return fallback(gesture) }
142 
143         performDeletion(
144             rangeInTransformedText = rangeInTransformedText,
145             adjustRange = (granularity == TextGranularity.Word)
146         )
147         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
148     }
149 
previewDeleteGesturenull150     private fun TransformedTextFieldState.previewDeleteGesture(
151         gesture: DeleteGesture,
152         layoutState: TextLayoutState
153     ) {
154         highlightRange(
155             layoutState.getRangeForScreenRect(
156                 gesture.deletionArea.toComposeRect(),
157                 gesture.granularity.toTextGranularity(),
158                 TextInclusionStrategy.ContainsCenter
159             ),
160             TextHighlightType.HandwritingDeletePreview
161         )
162     }
163 
performSelectRangeGesturenull164     private fun TransformedTextFieldState.performSelectRangeGesture(
165         gesture: SelectRangeGesture,
166         layoutState: TextLayoutState,
167         updateSelectionState: (() -> Unit)?,
168     ): Int {
169         val rangeInTransformedText =
170             layoutState
171                 .getRangeForScreenRects(
172                     gesture.selectionStartArea.toComposeRect(),
173                     gesture.selectionEndArea.toComposeRect(),
174                     gesture.granularity.toTextGranularity(),
175                     TextInclusionStrategy.ContainsCenter
176                 )
177                 .apply { if (collapsed) return fallback(gesture) }
178 
179         selectCharsIn(rangeInTransformedText)
180         updateSelectionState?.invoke()
181         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
182     }
183 
TransformedTextFieldStatenull184     private fun TransformedTextFieldState.previewSelectRangeGesture(
185         gesture: SelectRangeGesture,
186         layoutState: TextLayoutState
187     ) {
188         highlightRange(
189             layoutState.getRangeForScreenRects(
190                 gesture.selectionStartArea.toComposeRect(),
191                 gesture.selectionEndArea.toComposeRect(),
192                 gesture.granularity.toTextGranularity(),
193                 TextInclusionStrategy.ContainsCenter
194             ),
195             TextHighlightType.HandwritingSelectPreview
196         )
197     }
198 
performDeleteRangeGesturenull199     private fun TransformedTextFieldState.performDeleteRangeGesture(
200         gesture: DeleteRangeGesture,
201         layoutState: TextLayoutState
202     ): Int {
203         val granularity = gesture.granularity.toTextGranularity()
204         val rangeInTransformedText =
205             layoutState
206                 .getRangeForScreenRects(
207                     gesture.deletionStartArea.toComposeRect(),
208                     gesture.deletionEndArea.toComposeRect(),
209                     granularity,
210                     TextInclusionStrategy.ContainsCenter
211                 )
212                 .apply { if (collapsed) return fallback(gesture) }
213 
214         performDeletion(
215             rangeInTransformedText = rangeInTransformedText,
216             adjustRange = granularity == TextGranularity.Word
217         )
218         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
219     }
220 
previewDeleteRangeGesturenull221     private fun TransformedTextFieldState.previewDeleteRangeGesture(
222         gesture: DeleteRangeGesture,
223         layoutState: TextLayoutState
224     ) {
225         highlightRange(
226             layoutState.getRangeForScreenRects(
227                 gesture.deletionStartArea.toComposeRect(),
228                 gesture.deletionEndArea.toComposeRect(),
229                 gesture.granularity.toTextGranularity(),
230                 TextInclusionStrategy.ContainsCenter
231             ),
232             TextHighlightType.HandwritingDeletePreview
233         )
234     }
235 
TransformedTextFieldStatenull236     private fun TransformedTextFieldState.performJoinOrSplitGesture(
237         gesture: JoinOrSplitGesture,
238         layoutState: TextLayoutState,
239         viewConfiguration: ViewConfiguration?
240     ): Int {
241         // Fail when there is an output transformation.
242         // If output transformation inserts some spaces to the text, we can't remove them.
243         // Do nothing is the best choice in this case.
244         // We don't fallback either because the user's intention is unlikely inserting characters.
245         if (outputText !== untransformedText) {
246             return InputConnection.HANDWRITING_GESTURE_RESULT_FAILED
247         }
248 
249         val offset =
250             layoutState.getOffsetForHandwritingGesture(
251                 pointInScreen = gesture.joinOrSplitPoint.toOffset(),
252                 viewConfiguration = viewConfiguration
253             )
254 
255         // TODO(332963121): support gesture at BiDi boundaries.
256         if (offset == -1 || layoutState.layoutResult?.isBiDiBoundary(offset) == true) {
257             return fallback(gesture)
258         }
259 
260         val textRange = visualText.rangeOfWhitespaces(offset)
261 
262         if (textRange.collapsed) {
263             replaceText(" ", textRange)
264         } else {
265             performDeletion(rangeInTransformedText = textRange, adjustRange = false)
266         }
267         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
268     }
269 
performInsertGesturenull270     private fun TransformedTextFieldState.performInsertGesture(
271         gesture: InsertGesture,
272         layoutState: TextLayoutState,
273         viewConfiguration: ViewConfiguration?
274     ): Int {
275         val offset =
276             layoutState.getOffsetForHandwritingGesture(
277                 pointInScreen = gesture.insertionPoint.toOffset(),
278                 viewConfiguration = viewConfiguration
279             )
280 
281         // TODO(332963121): support gesture at BiDi boundaries.
282         if (offset == -1) {
283             return fallback(gesture)
284         }
285 
286         replaceText(gesture.textToInsert, TextRange(offset))
287         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
288     }
289 
performRemoveSpaceGesturenull290     private fun TransformedTextFieldState.performRemoveSpaceGesture(
291         gesture: RemoveSpaceGesture,
292         layoutState: TextLayoutState,
293         viewConfiguration: ViewConfiguration?
294     ): Int {
295         val range =
296             layoutState.layoutResult
297                 .getRangeForRemoveSpaceGesture(
298                     startPointInScreen = gesture.startPoint.toOffset(),
299                     endPointerInScreen = gesture.endPoint.toOffset(),
300                     layoutCoordinates = layoutState.textLayoutNodeCoordinates,
301                     viewConfiguration = viewConfiguration
302                 )
303                 .apply { if (collapsed) return fallback(gesture) }
304 
305         var firstMatchStart = -1
306         var lastMatchEnd = -1
307         val newText =
308             visualText.substring(range).replace(Regex("\\s+")) {
309                 if (firstMatchStart == -1) {
310                     firstMatchStart = it.range.first
311                 }
312                 lastMatchEnd = it.range.last + 1
313                 ""
314             }
315 
316         // No whitespace is found in the target range, fallback instead
317         if (firstMatchStart == -1 || lastMatchEnd == -1) {
318             return fallback(gesture)
319         }
320 
321         // We only replace the part of the text that changes.
322         // e.g. The text is "AB CD EF GH IJ" and the original gesture range is "CD EF GH"
323         // We'll replace " EF " to "EF" instead of replacing "CD EF GH" to "CDEFGH".
324         // By doing so, it'll place the cursor at the index of the last removed space.
325         // In the example above, the cursor should be placed after character 'F'.
326         val finalRange = TextRange(range.start + firstMatchStart, range.start + lastMatchEnd)
327         // Remove the unchanged part from the newText as well. Characters before firstMatchStart
328         // and characters after the lastMatchEnd are removed.
329         val finalNewText =
330             newText.substring(
331                 startIndex = firstMatchStart,
332                 endIndex = newText.length - (range.length - lastMatchEnd)
333             )
334 
335         replaceText(finalNewText, finalRange)
336         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
337     }
338 
performDeletionnull339     private fun TransformedTextFieldState.performDeletion(
340         rangeInTransformedText: TextRange,
341         adjustRange: Boolean
342     ) {
343         val finalRange =
344             if (adjustRange) {
345                 rangeInTransformedText.adjustHandwritingDeleteGestureRange(visualText)
346             } else {
347                 rangeInTransformedText
348             }
349         replaceText("", finalRange)
350     }
351 
TransformedTextFieldStatenull352     private fun TransformedTextFieldState.fallback(gesture: HandwritingGesture): Int {
353         editUntransformedTextAsUser { clearHighlight() }
354 
355         val fallbackText =
356             gesture.fallbackText ?: return InputConnection.HANDWRITING_GESTURE_RESULT_FAILED
357 
358         replaceSelectedText(
359             newText = fallbackText,
360             clearComposition = true,
361         )
362         return InputConnection.HANDWRITING_GESTURE_RESULT_FALLBACK
363     }
364 
highlightRangenull365     private fun TransformedTextFieldState.highlightRange(
366         range: TextRange,
367         type: TextHighlightType
368     ) {
369         if (range.collapsed) {
370             editUntransformedTextAsUser { clearHighlight() }
371         } else {
372             highlightCharsIn(type, range)
373         }
374     }
375 
performHandwritingGesturenull376     internal fun LegacyTextFieldState.performHandwritingGesture(
377         gesture: HandwritingGesture,
378         textFieldSelectionManager: TextFieldSelectionManager?,
379         viewConfiguration: ViewConfiguration?,
380         editCommandConsumer: (EditCommand) -> Unit
381     ): Int {
382         val text = untransformedText ?: return InputConnection.HANDWRITING_GESTURE_RESULT_FAILED
383         if (text != layoutResult?.value?.layoutInput?.text) {
384             // The text is transformed or layout is null, handwriting gesture failed.
385             return InputConnection.HANDWRITING_GESTURE_RESULT_FAILED
386         }
387         return when (gesture) {
388             is SelectGesture ->
389                 performSelectGesture(gesture, textFieldSelectionManager, editCommandConsumer)
390             is DeleteGesture -> performDeleteGesture(gesture, text, editCommandConsumer)
391             is SelectRangeGesture ->
392                 performSelectRangeGesture(gesture, textFieldSelectionManager, editCommandConsumer)
393             is DeleteRangeGesture -> performDeleteRangeGesture(gesture, text, editCommandConsumer)
394             is JoinOrSplitGesture ->
395                 performJoinOrSplitGesture(gesture, text, viewConfiguration, editCommandConsumer)
396             is InsertGesture ->
397                 performInsertGesture(gesture, viewConfiguration, editCommandConsumer)
398             is RemoveSpaceGesture ->
399                 performRemoveSpaceGesture(gesture, text, viewConfiguration, editCommandConsumer)
400             else -> InputConnection.HANDWRITING_GESTURE_RESULT_UNSUPPORTED
401         }
402     }
403 
previewHandwritingGesturenull404     internal fun LegacyTextFieldState.previewHandwritingGesture(
405         gesture: PreviewableHandwritingGesture,
406         textFieldSelectionManager: TextFieldSelectionManager?,
407         cancellationSignal: CancellationSignal?
408     ): Boolean {
409         val text = untransformedText ?: return false
410         if (text != layoutResult?.value?.layoutInput?.text) {
411             // The text is transformed or layout is null, handwriting gesture failed.
412             return false
413         }
414         when (gesture) {
415             is SelectGesture -> previewSelectGesture(gesture, textFieldSelectionManager)
416             is DeleteGesture -> previewDeleteGesture(gesture, textFieldSelectionManager)
417             is SelectRangeGesture -> previewSelectRangeGesture(gesture, textFieldSelectionManager)
418             is DeleteRangeGesture -> previewDeleteRangeGesture(gesture, textFieldSelectionManager)
419             else -> return false
420         }
421         cancellationSignal?.setOnCancelListener {
422             textFieldSelectionManager?.clearPreviewHighlight()
423         }
424         return true
425     }
426 
LegacyTextFieldStatenull427     private fun LegacyTextFieldState.performSelectGesture(
428         gesture: SelectGesture,
429         textSelectionManager: TextFieldSelectionManager?,
430         editCommandConsumer: (EditCommand) -> Unit
431     ): Int {
432         val range =
433             getRangeForScreenRect(
434                     gesture.selectionArea.toComposeRect(),
435                     gesture.granularity.toTextGranularity(),
436                     TextInclusionStrategy.ContainsCenter
437                 )
438                 .apply {
439                     if (collapsed) return fallbackOnLegacyTextField(gesture, editCommandConsumer)
440                 }
441 
442         performSelectionOnLegacyTextField(range, textSelectionManager, editCommandConsumer)
443         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
444     }
445 
LegacyTextFieldStatenull446     private fun LegacyTextFieldState.previewSelectGesture(
447         gesture: SelectGesture,
448         textFieldSelectionManager: TextFieldSelectionManager?
449     ) {
450         textFieldSelectionManager?.setSelectionPreviewHighlight(
451             getRangeForScreenRect(
452                 gesture.selectionArea.toComposeRect(),
453                 gesture.granularity.toTextGranularity(),
454                 TextInclusionStrategy.ContainsCenter
455             )
456         )
457     }
458 
performDeleteGesturenull459     private fun LegacyTextFieldState.performDeleteGesture(
460         gesture: DeleteGesture,
461         text: AnnotatedString,
462         editCommandConsumer: (EditCommand) -> Unit
463     ): Int {
464         val granularity = gesture.granularity.toTextGranularity()
465         val range =
466             getRangeForScreenRect(
467                     gesture.deletionArea.toComposeRect(),
468                     granularity,
469                     TextInclusionStrategy.ContainsCenter
470                 )
471                 .apply {
472                     if (collapsed) return fallbackOnLegacyTextField(gesture, editCommandConsumer)
473                 }
474 
475         performDeletionOnLegacyTextField(
476             range = range,
477             text = text,
478             adjustRange = granularity == TextGranularity.Word,
479             editCommandConsumer = editCommandConsumer
480         )
481         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
482     }
483 
previewDeleteGesturenull484     private fun LegacyTextFieldState.previewDeleteGesture(
485         gesture: DeleteGesture,
486         textFieldSelectionManager: TextFieldSelectionManager?
487     ) {
488         textFieldSelectionManager?.setDeletionPreviewHighlight(
489             getRangeForScreenRect(
490                 gesture.deletionArea.toComposeRect(),
491                 gesture.granularity.toTextGranularity(),
492                 TextInclusionStrategy.ContainsCenter
493             )
494         )
495     }
496 
performSelectRangeGesturenull497     private fun LegacyTextFieldState.performSelectRangeGesture(
498         gesture: SelectRangeGesture,
499         textSelectionManager: TextFieldSelectionManager?,
500         editCommandConsumer: (EditCommand) -> Unit
501     ): Int {
502         val range =
503             getRangeForScreenRects(
504                     gesture.selectionStartArea.toComposeRect(),
505                     gesture.selectionEndArea.toComposeRect(),
506                     gesture.granularity.toTextGranularity(),
507                     TextInclusionStrategy.ContainsCenter
508                 )
509                 .apply {
510                     if (collapsed) return fallbackOnLegacyTextField(gesture, editCommandConsumer)
511                 }
512 
513         performSelectionOnLegacyTextField(
514             range = range,
515             textSelectionManager = textSelectionManager,
516             editCommandConsumer = editCommandConsumer
517         )
518         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
519     }
520 
LegacyTextFieldStatenull521     private fun LegacyTextFieldState.previewSelectRangeGesture(
522         gesture: SelectRangeGesture,
523         textFieldSelectionManager: TextFieldSelectionManager?
524     ) {
525         textFieldSelectionManager?.setSelectionPreviewHighlight(
526             getRangeForScreenRects(
527                 gesture.selectionStartArea.toComposeRect(),
528                 gesture.selectionEndArea.toComposeRect(),
529                 gesture.granularity.toTextGranularity(),
530                 TextInclusionStrategy.ContainsCenter
531             )
532         )
533     }
534 
performDeleteRangeGesturenull535     private fun LegacyTextFieldState.performDeleteRangeGesture(
536         gesture: DeleteRangeGesture,
537         text: AnnotatedString,
538         editCommandConsumer: (EditCommand) -> Unit
539     ): Int {
540         val granularity = gesture.granularity.toTextGranularity()
541         val range =
542             getRangeForScreenRects(
543                     gesture.deletionStartArea.toComposeRect(),
544                     gesture.deletionEndArea.toComposeRect(),
545                     granularity,
546                     TextInclusionStrategy.ContainsCenter
547                 )
548                 .apply {
549                     if (collapsed) return fallbackOnLegacyTextField(gesture, editCommandConsumer)
550                 }
551         performDeletionOnLegacyTextField(
552             range = range,
553             text = text,
554             adjustRange = granularity == TextGranularity.Word,
555             editCommandConsumer = editCommandConsumer
556         )
557         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
558     }
559 
previewDeleteRangeGesturenull560     private fun LegacyTextFieldState.previewDeleteRangeGesture(
561         gesture: DeleteRangeGesture,
562         textFieldSelectionManager: TextFieldSelectionManager?
563     ) {
564         textFieldSelectionManager?.setDeletionPreviewHighlight(
565             getRangeForScreenRects(
566                 gesture.deletionStartArea.toComposeRect(),
567                 gesture.deletionEndArea.toComposeRect(),
568                 gesture.granularity.toTextGranularity(),
569                 TextInclusionStrategy.ContainsCenter
570             )
571         )
572     }
573 
LegacyTextFieldStatenull574     private fun LegacyTextFieldState.performJoinOrSplitGesture(
575         gesture: JoinOrSplitGesture,
576         text: AnnotatedString,
577         viewConfiguration: ViewConfiguration?,
578         editCommandConsumer: (EditCommand) -> Unit
579     ): Int {
580         if (viewConfiguration == null) {
581             return fallbackOnLegacyTextField(gesture, editCommandConsumer)
582         }
583 
584         val offset =
585             getOffsetForHandwritingGesture(
586                 pointInScreen = gesture.joinOrSplitPoint.toOffset(),
587                 viewConfiguration = viewConfiguration
588             )
589         // TODO(332963121): support gesture at BiDi boundaries.
590         if (offset == -1 || layoutResult?.value?.isBiDiBoundary(offset) == true) {
591             return fallbackOnLegacyTextField(gesture, editCommandConsumer)
592         }
593 
594         val range = text.rangeOfWhitespaces(offset)
595         if (range.collapsed) {
596             performInsertionOnLegacyTextField(range.start, " ", editCommandConsumer)
597         } else {
598             performDeletionOnLegacyTextField(
599                 range = range,
600                 text = text,
601                 adjustRange = false,
602                 editCommandConsumer = editCommandConsumer
603             )
604         }
605 
606         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
607     }
608 
LegacyTextFieldStatenull609     private fun LegacyTextFieldState.performInsertGesture(
610         gesture: InsertGesture,
611         viewConfiguration: ViewConfiguration?,
612         editCommandConsumer: (EditCommand) -> Unit
613     ): Int {
614         if (viewConfiguration == null) {
615             return fallbackOnLegacyTextField(gesture, editCommandConsumer)
616         }
617 
618         val offset =
619             getOffsetForHandwritingGesture(
620                 pointInScreen = gesture.insertionPoint.toOffset(),
621                 viewConfiguration = viewConfiguration
622             )
623         // TODO(332963121): support gesture at BiDi boundaries.
624         if (offset == -1 || layoutResult?.value?.isBiDiBoundary(offset) == true) {
625             return fallbackOnLegacyTextField(gesture, editCommandConsumer)
626         }
627 
628         performInsertionOnLegacyTextField(offset, gesture.textToInsert, editCommandConsumer)
629         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
630     }
631 
performRemoveSpaceGesturenull632     private fun LegacyTextFieldState.performRemoveSpaceGesture(
633         gesture: RemoveSpaceGesture,
634         text: AnnotatedString,
635         viewConfiguration: ViewConfiguration?,
636         editCommandConsumer: (EditCommand) -> Unit
637     ): Int {
638         val range =
639             layoutResult
640                 ?.value
641                 .getRangeForRemoveSpaceGesture(
642                     startPointInScreen = gesture.startPoint.toOffset(),
643                     endPointerInScreen = gesture.endPoint.toOffset(),
644                     layoutCoordinates = layoutCoordinates,
645                     viewConfiguration = viewConfiguration
646                 )
647                 .apply {
648                     if (collapsed) return fallbackOnLegacyTextField(gesture, editCommandConsumer)
649                 }
650 
651         var firstMatchStart = -1
652         var lastMatchEnd = -1
653         val newText =
654             text.substring(range).replace(Regex("\\s+")) {
655                 if (firstMatchStart == -1) {
656                     firstMatchStart = it.range.first
657                 }
658                 lastMatchEnd = it.range.last + 1
659                 ""
660             }
661 
662         // No whitespace is found in the target range, fallback instead
663         if (firstMatchStart == -1 || lastMatchEnd == -1) {
664             return fallbackOnLegacyTextField(gesture, editCommandConsumer)
665         }
666 
667         // We only replace the part of the text that changes.
668         // e.g. The text is "AB CD EF GH IJ" and the original gesture range is "CD EF GH"
669         // We'll replace " EF " to "EF" instead of replacing "CD EF GH" to "CDEFGH",
670         val replacedRangeStart = range.start + firstMatchStart
671         val replacedRangeEnd = range.start + lastMatchEnd
672         // Remove the unchanged part from the newText as well. Characters before firstMatchStart
673         // and characters after the lastMatchEnd are removed.
674         val finalNewText =
675             newText.substring(
676                 startIndex = firstMatchStart,
677                 endIndex = newText.length - (range.length - lastMatchEnd)
678             )
679 
680         editCommandConsumer.invoke(
681             compoundEditCommand(
682                 SetSelectionCommand(replacedRangeStart, replacedRangeEnd),
683                 CommitTextCommand(finalNewText, 1)
684             )
685         )
686         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS
687     }
688 
performInsertionOnLegacyTextFieldnull689     private fun performInsertionOnLegacyTextField(
690         offset: Int,
691         text: String,
692         editCommandConsumer: (EditCommand) -> Unit
693     ) {
694         editCommandConsumer.invoke(
695             compoundEditCommand(SetSelectionCommand(offset, offset), CommitTextCommand(text, 1))
696         )
697     }
698 
performSelectionOnLegacyTextFieldnull699     private fun performSelectionOnLegacyTextField(
700         range: TextRange,
701         textSelectionManager: TextFieldSelectionManager?,
702         editCommandConsumer: (EditCommand) -> Unit
703     ) {
704         editCommandConsumer.invoke(SetSelectionCommand(range.start, range.end))
705         textSelectionManager?.enterSelectionMode(showFloatingToolbar = true)
706     }
707 
performDeletionOnLegacyTextFieldnull708     private fun performDeletionOnLegacyTextField(
709         range: TextRange,
710         text: AnnotatedString,
711         adjustRange: Boolean,
712         editCommandConsumer: (EditCommand) -> Unit
713     ) {
714         val finalRange =
715             if (adjustRange) {
716                 range.adjustHandwritingDeleteGestureRange(text)
717             } else {
718                 range
719             }
720 
721         editCommandConsumer.invoke(
722             compoundEditCommand(
723                 SetSelectionCommand(finalRange.end, finalRange.end),
724                 DeleteSurroundingTextCommand(
725                     lengthAfterCursor = 0,
726                     lengthBeforeCursor = finalRange.length
727                 )
728             )
729         )
730     }
731 
fallbackOnLegacyTextFieldnull732     private fun fallbackOnLegacyTextField(
733         gesture: HandwritingGesture,
734         editCommandConsumer: (EditCommand) -> Unit
735     ): Int {
736         val fallbackText =
737             gesture.fallbackText ?: return InputConnection.HANDWRITING_GESTURE_RESULT_FAILED
738         editCommandConsumer.invoke(CommitTextCommand(fallbackText, newCursorPosition = 1))
739         return InputConnection.HANDWRITING_GESTURE_RESULT_FALLBACK
740     }
741 
742     /** Convert the Platform text granularity to Compose [TextGranularity] object. */
toTextGranularitynull743     private fun Int.toTextGranularity(): TextGranularity {
744         return when (this) {
745             HandwritingGesture.GRANULARITY_CHARACTER -> TextGranularity.Character
746             HandwritingGesture.GRANULARITY_WORD -> TextGranularity.Word
747             else -> TextGranularity.Character
748         }
749     }
750 }
751 
752 private const val LINE_FEED_CODE_POINT = '\n'.code
753 private const val NBSP_CODE_POINT = '\u00A0'.code
754 
755 /**
756  * For handwriting delete gestures with word granularity, adjust the start and end offsets to remove
757  * extra whitespace around the deleted text.
758  */
TextRangenull759 private fun TextRange.adjustHandwritingDeleteGestureRange(text: CharSequence): TextRange {
760     var start = this.start
761     var end = this.end
762 
763     // If the deleted text is at the start of the text, the behavior is the same as the case
764     // where the deleted text follows a new line character.
765     var codePointBeforeStart: Int =
766         if (start > 0) {
767             Character.codePointBefore(text, start)
768         } else {
769             LINE_FEED_CODE_POINT
770         }
771     // If the deleted text is at the end of the text, the behavior is the same as the case where
772     // the deleted text precedes a new line character.
773     var codePointAtEnd: Int =
774         if (end < text.length) {
775             Character.codePointAt(text, end)
776         } else {
777             LINE_FEED_CODE_POINT
778         }
779 
780     if (
781         codePointBeforeStart.isWhitespaceExceptNewline() &&
782             (codePointAtEnd.isWhitespace() || codePointAtEnd.isPunctuation())
783     ) {
784         // Remove whitespace (except new lines) before the deleted text, in these cases:
785         // - There is whitespace following the deleted text
786         //     e.g. "one [deleted] three" -> "one | three" -> "one| three"
787         // - There is punctuation following the deleted text
788         //     e.g. "one [deleted]!" -> "one |!" -> "one|!"
789         // - There is a new line following the deleted text
790         //     e.g. "one [deleted]\n" -> "one |\n" -> "one|\n"
791         // - The deleted text is at the end of the text
792         //     e.g. "one [deleted]" -> "one |" -> "one|"
793         // (The pipe | indicates the cursor position.)
794         do {
795             start -= Character.charCount(codePointBeforeStart)
796             if (start == 0) break
797             codePointBeforeStart = Character.codePointBefore(text, start)
798         } while (codePointBeforeStart.isWhitespaceExceptNewline())
799         return TextRange(start, end)
800     }
801 
802     if (
803         codePointAtEnd.isWhitespaceExceptNewline() &&
804             (codePointBeforeStart.isWhitespace() || codePointBeforeStart.isPunctuation())
805     ) {
806         // Remove whitespace (except new lines) after the deleted text, in these cases:
807         // - There is punctuation preceding the deleted text
808         //     e.g. "([deleted] two)" -> "(| two)" -> "(|two)"
809         // - There is a new line preceding the deleted text
810         //     e.g. "\n[deleted] two" -> "\n| two" -> "\n|two"
811         // - The deleted text is at the start of the text
812         //     e.g. "[deleted] two" -> "| two" -> "|two"
813         // (The pipe | indicates the cursor position.)
814         do {
815             end += Character.charCount(codePointAtEnd)
816             if (end == text.length) break
817             codePointAtEnd = Character.codePointAt(text, end)
818         } while (codePointAtEnd.isWhitespaceExceptNewline())
819         return TextRange(start, end)
820     }
821 
822     // Return the original range.
823     return this
824 }
825 
826 /**
827  * This helper method is copied from [android.text.TextUtils]. It returns true if the code point is
828  * a new line.
829  */
isNewlinenull830 private fun Int.isNewline(): Boolean {
831     val type = Character.getType(this)
832     return type == Character.PARAGRAPH_SEPARATOR.toInt() ||
833         type == Character.LINE_SEPARATOR.toInt() ||
834         this == LINE_FEED_CODE_POINT
835 }
836 
837 /**
838  * This helper method is copied from [android.text.TextUtils]. It returns true if the code point is
839  * a whitespace.
840  */
isWhitespacenull841 private fun Int.isWhitespace(): Boolean {
842     return Character.isWhitespace(this) || this == NBSP_CODE_POINT
843 }
844 
845 /**
846  * This helper method is copied from [android.text.TextUtils]. It returns true if the code point is
847  * a whitespace and not a new line.
848  */
isWhitespaceExceptNewlinenull849 private fun Int.isWhitespaceExceptNewline(): Boolean {
850     return isWhitespace() && !isNewline()
851 }
852 
853 /**
854  * This helper method is copied from [android.text.TextUtils]. It returns true if the code point is
855  * a punctuation.
856  */
isPunctuationnull857 private fun Int.isPunctuation(): Boolean {
858     val type = Character.getType(this)
859     return type == Character.CONNECTOR_PUNCTUATION.toInt() ||
860         type == Character.DASH_PUNCTUATION.toInt() ||
861         type == Character.END_PUNCTUATION.toInt() ||
862         type == Character.FINAL_QUOTE_PUNCTUATION.toInt() ||
863         type == Character.INITIAL_QUOTE_PUNCTUATION.toInt() ||
864         type == Character.OTHER_PUNCTUATION.toInt() ||
865         type == Character.START_PUNCTUATION.toInt()
866 }
867 
toOffsetnull868 private fun PointF.toOffset(): Offset = Offset(x, y)
869 
870 private fun TextLayoutState.getRangeForScreenRect(
871     rectInScreen: Rect,
872     granularity: TextGranularity,
873     inclusionStrategy: TextInclusionStrategy
874 ): TextRange {
875     return layoutResult
876         ?.multiParagraph
877         .getRangeForScreenRect(
878             rectInScreen,
879             textLayoutNodeCoordinates,
880             granularity,
881             inclusionStrategy
882         )
883 }
884 
TextLayoutStatenull885 private fun TextLayoutState.getRangeForScreenRects(
886     startRectInScreen: Rect,
887     endRectInScreen: Rect,
888     granularity: TextGranularity,
889     inclusionStrategy: TextInclusionStrategy
890 ): TextRange {
891     val startRange =
892         getRangeForScreenRect(startRectInScreen, granularity, inclusionStrategy).apply {
893             if (collapsed) return TextRange.Zero
894         }
895 
896     val endRange =
897         getRangeForScreenRect(endRectInScreen, granularity, inclusionStrategy).apply {
898             if (collapsed) return TextRange.Zero
899         }
900 
901     return enclosure(startRange, endRange)
902 }
903 
LegacyTextFieldStatenull904 private fun LegacyTextFieldState.getRangeForScreenRect(
905     rectInScreen: Rect,
906     granularity: TextGranularity,
907     inclusionStrategy: TextInclusionStrategy
908 ): TextRange {
909     return layoutResult
910         ?.value
911         ?.multiParagraph
912         .getRangeForScreenRect(rectInScreen, layoutCoordinates, granularity, inclusionStrategy)
913 }
914 
LegacyTextFieldStatenull915 private fun LegacyTextFieldState.getRangeForScreenRects(
916     startRectInScreen: Rect,
917     endRectInScreen: Rect,
918     granularity: TextGranularity,
919     inclusionStrategy: TextInclusionStrategy
920 ): TextRange {
921     val startRange =
922         getRangeForScreenRect(startRectInScreen, granularity, inclusionStrategy).apply {
923             if (collapsed) return TextRange.Zero
924         }
925 
926     val endRange =
927         getRangeForScreenRect(endRectInScreen, granularity, inclusionStrategy).apply {
928             if (collapsed) return TextRange.Zero
929         }
930 
931     return enclosure(startRange, endRange)
932 }
933 
rangeOfWhitespacesnull934 private fun CharSequence.rangeOfWhitespaces(offset: Int): TextRange {
935     var startOffset = offset
936     var endOffset = offset
937 
938     while (startOffset > 0) {
939         val codePointBeforeStart = codePointBefore(startOffset)
940         if (!codePointBeforeStart.isWhitespace()) {
941             break
942         }
943 
944         startOffset -= Character.charCount(codePointBeforeStart)
945     }
946 
947     while (endOffset < length) {
948         val codePointAtEnd = codePointAt(endOffset)
949         if (!codePointAtEnd.isWhitespace()) {
950             break
951         }
952         endOffset += charCount(codePointAtEnd)
953     }
954 
955     return TextRange(startOffset, endOffset)
956 }
957 
TextLayoutStatenull958 private fun TextLayoutState.getOffsetForHandwritingGesture(
959     pointInScreen: Offset,
960     viewConfiguration: ViewConfiguration?
961 ): Int {
962     return layoutResult
963         ?.multiParagraph
964         ?.getOffsetForHandwritingGesture(
965             pointInScreen,
966             textLayoutNodeCoordinates,
967             viewConfiguration
968         ) ?: -1
969 }
970 
LegacyTextFieldStatenull971 private fun LegacyTextFieldState.getOffsetForHandwritingGesture(
972     pointInScreen: Offset,
973     viewConfiguration: ViewConfiguration
974 ): Int {
975     return layoutResult
976         ?.value
977         ?.multiParagraph
978         ?.getOffsetForHandwritingGesture(pointInScreen, layoutCoordinates, viewConfiguration) ?: -1
979 }
980 
isBiDiBoundarynull981 private fun TextLayoutResult.isBiDiBoundary(offset: Int): Boolean {
982     val line = getLineForOffset(offset)
983     if (offset == getLineStart(line) || offset == getLineEnd(line)) {
984         return getParagraphDirection(offset) != getBidiRunDirection(offset)
985     }
986 
987     // Offset can't 0 or text.length at the moment.
988     return getBidiRunDirection(offset) != getBidiRunDirection(offset - 1)
989 }
990 
getRangeForScreenRectnull991 private fun MultiParagraph?.getRangeForScreenRect(
992     rectInScreen: Rect,
993     layoutCoordinates: LayoutCoordinates?,
994     granularity: TextGranularity,
995     inclusionStrategy: TextInclusionStrategy
996 ): TextRange {
997     if (this == null || layoutCoordinates == null) {
998         return TextRange.Zero
999     }
1000 
1001     val screenOriginInLocal = layoutCoordinates.screenToLocal(Offset.Zero)
1002     val localRect = rectInScreen.translate(screenOriginInLocal)
1003     return getRangeForRect(localRect, granularity, inclusionStrategy)
1004 }
1005 
getOffsetForHandwritingGesturenull1006 private fun MultiParagraph.getOffsetForHandwritingGesture(
1007     pointInScreen: Offset,
1008     layoutCoordinates: LayoutCoordinates?,
1009     viewConfiguration: ViewConfiguration?
1010 ): Int {
1011     val localPoint = layoutCoordinates?.screenToLocal(pointInScreen) ?: return -1
1012     val line = getLineForHandwritingGesture(localPoint, viewConfiguration)
1013     if (line == -1) return -1
1014 
1015     val adjustedPoint = localPoint.copy(y = (getLineTop(line) + getLineBottom(line)) / 2f)
1016     return getOffsetForPosition(adjustedPoint)
1017 }
1018 
TextLayoutResultnull1019 private fun TextLayoutResult?.getRangeForRemoveSpaceGesture(
1020     startPointInScreen: Offset,
1021     endPointerInScreen: Offset,
1022     layoutCoordinates: LayoutCoordinates?,
1023     viewConfiguration: ViewConfiguration?
1024 ): TextRange {
1025     if (this == null || layoutCoordinates == null) {
1026         return TextRange.Zero
1027     }
1028     val localStartPoint = layoutCoordinates.screenToLocal(startPointInScreen)
1029     val localEndPoint = layoutCoordinates.screenToLocal(endPointerInScreen)
1030     val startLine = multiParagraph.getLineForHandwritingGesture(localStartPoint, viewConfiguration)
1031     val endLine = multiParagraph.getLineForHandwritingGesture(localEndPoint, viewConfiguration)
1032     val line: Int
1033 
1034     if (startLine == -1) {
1035         // Both start and end point are out of the line margin. Return null.
1036         if (endLine == -1) return TextRange.Zero
1037         line = endLine
1038     } else {
1039         line =
1040             if (endLine == -1) {
1041                 startLine
1042             } else {
1043                 // RemoveSpaceGesture is a single line gesture, it can't be applied for multiple
1044                 // lines.
1045                 // If start point and end point belongs to different lines, select the top line.
1046                 min(startLine, endLine)
1047             }
1048     }
1049 
1050     val lineCenter = (getLineTop(line) + getLineBottom(line)) / 2
1051 
1052     val rect =
1053         Rect(
1054             left = min(localStartPoint.x, localEndPoint.x),
1055             top = lineCenter - 0.1f,
1056             right = max(localStartPoint.x, localEndPoint.x),
1057             bottom = lineCenter + 0.1f
1058         )
1059 
1060     return multiParagraph.getRangeForRect(
1061         rect,
1062         TextGranularity.Character,
1063         TextInclusionStrategy.AnyOverlap
1064     )
1065 }
1066 
getLineForHandwritingGesturenull1067 private fun MultiParagraph.getLineForHandwritingGesture(
1068     localPoint: Offset,
1069     viewConfiguration: ViewConfiguration?
1070 ): Int {
1071     val lineMargin = viewConfiguration?.handwritingGestureLineMargin ?: 0f
1072     val line = getLineForVerticalPosition(localPoint.y)
1073 
1074     if (
1075         localPoint.y < getLineTop(line) - lineMargin ||
1076             localPoint.y > getLineBottom(line) + lineMargin
1077     ) {
1078         // The point is not within lineMargin of a line.
1079         return -1
1080     }
1081     if (localPoint.x < -lineMargin || localPoint.x > width + lineMargin) {
1082         // The point is not within lineMargin of a line.
1083         return -1
1084     }
1085     return line
1086 }
1087 
compoundEditCommandnull1088 private fun compoundEditCommand(vararg editCommands: EditCommand): EditCommand {
1089     return object : EditCommand {
1090         override fun applyTo(buffer: EditingBuffer) {
1091             for (editCommand in editCommands) {
1092                 editCommand.applyTo(buffer)
1093             }
1094         }
1095     }
1096 }
1097 
1098 /** Return the minimum [TextRange] that contains the both given [TextRange]s. */
enclosurenull1099 private fun enclosure(a: TextRange, b: TextRange): TextRange {
1100     return TextRange(min(a.start, a.start), max(b.end, b.end))
1101 }
1102