1 /*
<lambda>null2  * 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 androidx.compose.foundation.ExperimentalFoundationApi
20 import androidx.compose.foundation.text.input.InputTransformation
21 import androidx.compose.foundation.text.input.OutputTransformation
22 import androidx.compose.foundation.text.input.TextFieldBuffer
23 import androidx.compose.foundation.text.input.TextFieldCharSequence
24 import androidx.compose.foundation.text.input.TextFieldState
25 import androidx.compose.foundation.text.input.TextHighlightType
26 import androidx.compose.foundation.text.input.delete
27 import androidx.compose.foundation.text.input.internal.IndexTransformationType.Deletion
28 import androidx.compose.foundation.text.input.internal.IndexTransformationType.Insertion
29 import androidx.compose.foundation.text.input.internal.IndexTransformationType.Replacement
30 import androidx.compose.foundation.text.input.internal.IndexTransformationType.Untransformed
31 import androidx.compose.foundation.text.input.internal.undo.TextFieldEditUndoBehavior
32 import androidx.compose.foundation.text.input.setSelectionCoerced
33 import androidx.compose.runtime.Stable
34 import androidx.compose.runtime.State
35 import androidx.compose.runtime.derivedStateOf
36 import androidx.compose.runtime.getValue
37 import androidx.compose.runtime.mutableStateOf
38 import androidx.compose.runtime.setValue
39 import androidx.compose.ui.text.TextRange
40 import kotlinx.coroutines.suspendCancellableCoroutine
41 
42 /**
43  * A mutable view of a [TextFieldState] where the text and selection values are transformed by an
44  * [OutputTransformation] and a [CodepointTransformation].
45  *
46  * [outputText] and [visualText] return the transformed text (see the explanation of phases below),
47  * with selection and composition mapped to the corresponding offsets from the untransformed text.
48  * The transformed text is cached in [derived states][derivedStateOf] and only recalculated when the
49  * [TextFieldState] changes or some state read by the transformation functions changes.
50  *
51  * This class defines methods for various operations that can be performed on the underlying
52  * [TextFieldState]. When possible, these methods should be used instead of editing the state
53  * directly, since this class ensures the correct offset mappings are used. If an operation is too
54  * complex to warrant a method here, use [editUntransformedTextAsUser] but be careful to make sure
55  * any offsets are mapped correctly.
56  *
57  * To map offsets from transformed to untransformed text or back, use the [mapFromTransformed] and
58  * [mapToTransformed] methods.
59  *
60  * All operations call [TextFieldState.editAsUser] internally and pass [inputTransformation].
61  *
62  * ## Text transformation phases
63  *
64  * Text is transformed in two phases:
65  * 1. The first phase applies [outputTransformation], and the resulting text, [outputText], is
66  *    published to the semantics system (consumed by a11y services and tests).
67  * 2. The second phase applies [codepointTransformation] and the resulting text, [visualText], is
68  *    laid out and drawn to the screen.
69  *
70  * Any transformations that change the semantics (in the generic sense, not Compose semantics)
71  * should be done in the first phase. Examples include adding prefixes or suffixes to the text or
72  * inserting formatting characters.
73  *
74  * The second phase should only be used for purely visual or layout transformations. Examples
75  * include password masking or inserting spaces for Scribe.
76  *
77  * In most cases, one or both phases will be noops. E.g., password fields will usually only use the
78  * second phase, and non-password fields will usually only use the first phase.
79  *
80  * Here's a diagram explaining the phases:
81  * ```
82  *  ┌──────────────────┐
83  *  │                  │
84  *  │  TextFieldState  │        "Sam"      - Semantics setSelection: relativeToOriginalText=true
85  *  │                  │
86  *  └──────────────────┘
87  *            │
88  *  OutputTransformation    s/^/Hello, /
89  *            │
90  *            ▼
91  *  ┌──────────────────┐
92  *  │                  │                   - Talkback
93  *  │   Output text    │    "Hello, Sam"   - Tests
94  *  │                  │                   - Semantics setSelection: relativeToOriginalText=false
95  *  └──────────────────┘
96  *            │
97  * CodepointTransformation     s/./•/g
98  *            │
99  *            ▼
100  *  ┌──────────────────┐
101  *  │                  │                   - Measured
102  *  │   Visual text    │    "••••••••••"   - Wrapping, ellipsis, etc.
103  *  │                  │                   - Drawn on screen
104  *  └──────────────────┘
105  * ```
106  */
107 @OptIn(ExperimentalFoundationApi::class)
108 @Stable
109 internal class TransformedTextFieldState(
110     private val textFieldState: TextFieldState,
111     private var inputTransformation: InputTransformation? = null,
112     private val codepointTransformation: CodepointTransformation? = null,
113     private val outputTransformation: OutputTransformation? = null,
114 ) {
115     private val outputTransformedText: State<TransformedText?>? =
116         // Don't allocate a derived state object if we don't need it, they're expensive.
117         outputTransformation?.let { transformation ->
118             derivedStateOf {
119                 // text is a state read. transformation may also perform state reads when ran.
120                 calculateTransformedText(
121                     untransformedValue = textFieldState.value,
122                     outputTransformation = transformation,
123                     wedgeAffinity = selectionWedgeAffinity
124                 )
125             }
126         }
127 
128     private val codepointTransformedText: State<TransformedText?>? =
129         codepointTransformation?.let { transformation ->
130             derivedStateOf {
131                 calculateTransformedText(
132                     // These are state reads. codepointTransformation may also perform state reads
133                     // when ran.
134                     untransformedValue = outputTransformedText?.value?.text ?: textFieldState.value,
135                     codepointTransformation = transformation,
136                     wedgeAffinity = selectionWedgeAffinity
137                 )
138             }
139         }
140 
141     /**
142      * The raw text in the underlying [TextFieldState]. This text does not have any
143      * [CodepointTransformation] applied.
144      */
145     val untransformedText: TextFieldCharSequence
146         get() = textFieldState.value
147 
148     /**
149      * The text that should be presented to the user in most cases. If an [OutputTransformation] is
150      * specified, this text has the transformation applied. If there's no transformation, this will
151      * be the same as [untransformedText].
152      *
153      * See the diagram on [TransformedTextFieldState] for a graphical representation of how this
154      * value relates to [untransformedText] and [visualText].
155      */
156     val outputText: TextFieldCharSequence
157         get() = outputTransformedText?.value?.text ?: untransformedText
158 
159     /**
160      * The text that should be laid out and drawn to the screen. If a [CodepointTransformation] is
161      * specified, this text has the transformation applied. If there's no transformation, this will
162      * be the same as [outputText].
163      *
164      * See the diagram on [TransformedTextFieldState] for a graphical representation of how this
165      * value relates to [untransformedText] and [outputText].
166      */
167     val visualText: TextFieldCharSequence
168         get() = codepointTransformedText?.value?.text ?: outputText
169 
170     /**
171      * Indicates which side of a wedge (text inserted by the [OutputTransformation]) the start and
172      * end of the selection should map to. This allows the user to move the cursor to both sides of
173      * the wedge even though both those indices map to the same index in the untransformed text.
174      */
175     var selectionWedgeAffinity by mutableStateOf(SelectionWedgeAffinity(WedgeAffinity.Start))
176 
177     /**
178      * [TransformedTextFieldState] is not recreated when only [InputTransformation] changes. This
179      * method simply updates the internal [InputTransformation] to be used by input methods like the
180      * IME, hardware keyboard, or gestures.
181      *
182      * [InputTransformation] property is not backed by snapshot state, so it can't be updated
183      * directly in composition. Make sure to call this method from outside the composition.
184      */
185     fun update(inputTransformation: InputTransformation?) {
186         this.inputTransformation = inputTransformation
187     }
188 
189     fun placeCursorBeforeCharAt(transformedOffset: Int) {
190         selectCharsIn(TextRange(transformedOffset))
191     }
192 
193     fun selectCharsIn(transformedRange: TextRange) {
194         val untransformedRange = mapFromTransformed(transformedRange)
195         selectUntransformedCharsIn(untransformedRange)
196     }
197 
198     fun selectUntransformedCharsIn(untransformedRange: TextRange) {
199         textFieldState.editAsUser(inputTransformation) {
200             setSelectionCoerced(untransformedRange.start, untransformedRange.end)
201         }
202     }
203 
204     fun highlightCharsIn(type: TextHighlightType, transformedRange: TextRange) {
205         val untransformedRange = mapFromTransformed(transformedRange)
206         textFieldState.editAsUser(inputTransformation) {
207             setHighlight(type, untransformedRange.start, untransformedRange.end)
208         }
209     }
210 
211     /** Replaces the entire content of the [textFieldState] with [newText]. */
212     fun replaceAll(newText: CharSequence) {
213         textFieldState.editAsUser(inputTransformation) {
214             delete(0, length)
215             append(newText.toString())
216             updateWedgeAffinity()
217         }
218     }
219 
220     fun selectAll() {
221         textFieldState.editAsUser(inputTransformation) { setSelectionCoerced(0, length) }
222     }
223 
224     fun deleteSelectedText() {
225         textFieldState.editAsUser(
226             inputTransformation,
227             undoBehavior = TextFieldEditUndoBehavior.NeverMerge
228         ) {
229             // `selection` is read from the buffer, so we don't need to transform it.
230             delete(selection.min, selection.max)
231             setSelectionCoerced(selection.min)
232             updateWedgeAffinity()
233         }
234     }
235 
236     /**
237      * Replaces the text in given [range] with [newText]. Like all other methods in this class,
238      * [range] is considered to be in transformed space.
239      */
240     fun replaceText(
241         newText: CharSequence,
242         range: TextRange,
243         undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible,
244         restartImeIfContentChanges: Boolean = true
245     ) {
246         textFieldState.editAsUser(
247             inputTransformation = inputTransformation,
248             undoBehavior = undoBehavior,
249             restartImeIfContentChanges = restartImeIfContentChanges
250         ) {
251             val selection = mapFromTransformed(range)
252             replace(selection.min, selection.max, newText)
253             val cursor = selection.min + newText.length
254             setSelectionCoerced(cursor)
255             updateWedgeAffinity()
256         }
257     }
258 
259     fun replaceSelectedText(
260         newText: CharSequence,
261         clearComposition: Boolean = false,
262         undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible,
263         restartImeIfContentChanges: Boolean = true
264     ) {
265         textFieldState.editAsUser(
266             inputTransformation = inputTransformation,
267             restartImeIfContentChanges = restartImeIfContentChanges,
268             undoBehavior = undoBehavior
269         ) {
270             if (clearComposition) {
271                 commitComposition()
272             }
273 
274             // `selection` is read from the buffer, so we don't need to transform it.
275             val selection = selection
276             replace(selection.min, selection.max, newText)
277             val cursor = selection.min + newText.length
278             setSelectionCoerced(cursor)
279             updateWedgeAffinity()
280         }
281     }
282 
283     fun collapseSelectionToMax() {
284         textFieldState.editAsUser(inputTransformation) {
285             // `selection` is read from the buffer, so we don't need to transform it.
286             setSelectionCoerced(selection.max)
287         }
288     }
289 
290     fun collapseSelectionToEnd() {
291         textFieldState.editAsUser(inputTransformation) {
292             // `selection` is read from the buffer, so we don't need to transform it.
293             setSelectionCoerced(selection.end)
294         }
295     }
296 
297     fun undo() {
298         textFieldState.undoState.undo()
299     }
300 
301     fun redo() {
302         textFieldState.undoState.redo()
303     }
304 
305     /**
306      * Runs [block] with a buffer that contains the source untransformed text. This is the text that
307      * will be fed into the [outputTransformation]. Any operations performed on this buffer MUST
308      * take care to explicitly convert between transformed and untransformed offsets and ranges.
309      * When possible, use the other methods on this class to manipulate selection to avoid having to
310      * do these conversions manually. Additionally any edit that ends up collapsing the selection
311      * resets the [selectionWedgeAffinity] back to [WedgeAffinity.Start].
312      *
313      * @see mapToTransformed
314      * @see mapFromTransformed
315      */
316     inline fun editUntransformedTextAsUser(
317         restartImeIfContentChanges: Boolean = true,
318         block: TextFieldBuffer.() -> Unit
319     ) {
320         textFieldState.editAsUser(
321             inputTransformation = inputTransformation,
322             restartImeIfContentChanges = restartImeIfContentChanges
323         ) {
324             block()
325             updateWedgeAffinity()
326         }
327     }
328 
329     /**
330      * If the text content changes after text is edited and the selection is collapsed into a
331      * cursor, wedge affinity needs to be updated.
332      */
333     private fun TextFieldBuffer.updateWedgeAffinity() {
334         if (changeTracker.changeCount > 0 && this@updateWedgeAffinity.selection.collapsed) {
335             selectionWedgeAffinity = SelectionWedgeAffinity(WedgeAffinity.Start)
336         }
337     }
338 
339     /**
340      * Maps an [offset] in the untransformed text to the corresponding offset or range in
341      * [visualText].
342      *
343      * An untransformed offset will map to non-collapsed range if the offset is in the middle of a
344      * surrogate pair in the untransformed text, in which case it will return the range of the
345      * codepoint that the surrogate maps to. Offsets on either side of a surrogate pair will return
346      * collapsed ranges.
347      *
348      * If there is no transformation, or the transformation does not change the text, a collapsed
349      * range of [offset] will be returned.
350      *
351      * @see mapFromTransformed
352      */
353     fun mapToTransformed(offset: Int): TextRange {
354         val presentMapping = outputTransformedText?.value?.offsetMapping
355         val visualMapping = codepointTransformedText?.value?.offsetMapping
356 
357         val intermediateRange = presentMapping?.mapFromSource(offset) ?: TextRange(offset)
358         return visualMapping?.let {
359             mapToTransformed(intermediateRange, it, selectionWedgeAffinity)
360         } ?: intermediateRange
361     }
362 
363     /**
364      * Maps a [range] in the untransformed text to the corresponding range in [visualText].
365      *
366      * If there is no transformation, or the transformation does not change the text, [range] will
367      * be returned.
368      *
369      * @see mapFromTransformed
370      */
371     fun mapToTransformed(range: TextRange): TextRange {
372         val presentMapping = outputTransformedText?.value?.offsetMapping
373         val visualMapping = codepointTransformedText?.value?.offsetMapping
374 
375         // Only apply the wedge affinity to the final range. If the first mapping returns a range,
376         // the first range should have both edges expanded by the second.
377         val intermediateRange = presentMapping?.let { mapToTransformed(range, it) } ?: range
378         return visualMapping?.let {
379             mapToTransformed(intermediateRange, it, selectionWedgeAffinity)
380         } ?: intermediateRange
381     }
382 
383     /**
384      * Maps an [offset] in [visualText] to the corresponding offset in the untransformed text.
385      *
386      * Multiple transformed offsets may map to the same untransformed offset. In particular, any
387      * offset in the middle of a surrogate pair will map to offset of the corresponding codepoint in
388      * the untransformed text.
389      *
390      * If there is no transformation, or the transformation does not change the text, [offset] will
391      * be returned.
392      *
393      * @see mapToTransformed
394      */
395     fun mapFromTransformed(offset: Int): TextRange {
396         val presentMapping = outputTransformedText?.value?.offsetMapping
397         val visualMapping = codepointTransformedText?.value?.offsetMapping
398 
399         val intermediateOffset = visualMapping?.mapFromDest(offset) ?: TextRange(offset)
400         return presentMapping?.let { mapFromTransformed(intermediateOffset, it) }
401             ?: intermediateOffset
402     }
403 
404     /**
405      * Maps a [range] in [visualText] to the corresponding range in the untransformed text.
406      *
407      * If there is no transformation, or the transformation does not change the text, [range] will
408      * be returned.
409      *
410      * @see mapToTransformed
411      */
412     fun mapFromTransformed(range: TextRange): TextRange {
413         val presentMapping = outputTransformedText?.value?.offsetMapping
414         val visualMapping = codepointTransformedText?.value?.offsetMapping
415 
416         val intermediateRange = visualMapping?.let { mapFromTransformed(range, it) } ?: range
417         return presentMapping?.let { mapFromTransformed(intermediateRange, it) }
418             ?: intermediateRange
419     }
420 
421     // TODO(b/296583846) Get rid of this.
422     /**
423      * Adds a [TextFieldState.NotifyImeListener] to the underlying [TextFieldState] and then
424      * suspends until cancelled, removing the listener before continuing.
425      *
426      * This listener is responsible for updating the IME about the latest changes to the underlying
427      * [TextFieldState]. Please note that the IME should be aware of the [outputText], rather than
428      * [untransformedText] since users mainly interact with the output representation.
429      *
430      * The real challenge comes from the fact that IME doesn't need updates if its commands are not
431      * interfered with. That's why [TextFieldState.NotifyImeListener] actually sends the latest
432      * synced value from IME, rather than the previous value inside the [TextFieldState] before the
433      * changes are applied. In the existence of [OutputTransformation], we have to transform these
434      * values once more before updating the IME.
435      */
436     suspend fun collectImeNotifications(
437         notifyImeListener: TextFieldState.NotifyImeListener
438     ): Nothing {
439         val transformedNotifyImeListener =
440             if (outputTransformation != null) {
441                 TextFieldState.NotifyImeListener { oldValue, _, restartIme ->
442                     notifyImeListener.onChange(
443                         oldValue =
444                             calculateTransformedText(
445                                     untransformedValue = oldValue,
446                                     outputTransformation = outputTransformation,
447                                     wedgeAffinity = selectionWedgeAffinity
448                                 )
449                                 ?.text ?: oldValue,
450                         newValue = visualText,
451                         restartIme = restartIme
452                     )
453                 }
454             } else {
455                 notifyImeListener
456             }
457         suspendCancellableCoroutine<Nothing> { continuation ->
458             textFieldState.addNotifyImeListener(transformedNotifyImeListener)
459             continuation.invokeOnCancellation {
460                 textFieldState.removeNotifyImeListener(transformedNotifyImeListener)
461             }
462         }
463     }
464 
465     override fun equals(other: Any?): Boolean {
466         if (this === other) return true
467         if (other !is TransformedTextFieldState) return false
468         if (textFieldState != other.textFieldState) return false
469         if (codepointTransformation != other.codepointTransformation) return false
470         return outputTransformation == other.outputTransformation
471     }
472 
473     override fun hashCode(): Int {
474         var result = textFieldState.hashCode()
475         result = 31 * result + (codepointTransformation?.hashCode() ?: 0)
476         result = 31 * result + (outputTransformation?.hashCode() ?: 0)
477         return result
478     }
479 
480     override fun toString(): String =
481         "TransformedTextFieldState(" +
482             "textFieldState=$textFieldState, " +
483             "outputTransformation=$outputTransformation, " +
484             "outputTransformedText=$outputTransformedText, " +
485             "codepointTransformation=$codepointTransformation, " +
486             "codepointTransformedText=$codepointTransformedText, " +
487             "outputText=\"$outputText\", " +
488             "visualText=\"$visualText\"" +
489             ")"
490 
491     private data class TransformedText(
492         val text: TextFieldCharSequence,
493         val offsetMapping: OffsetMappingCalculator,
494     )
495 
496     private companion object {
497 
498         /**
499          * Applies an [OutputTransformation] to a [TextFieldCharSequence], returning the transformed
500          * text content, the selection/cursor from the [untransformedValue] mapped to the offsets in
501          * the transformed text, and an [OffsetMappingCalculator] that can be used to map offsets in
502          * both directions between the transformed and untransformed text.
503          *
504          * This function is relatively expensive, since it creates a copy of [untransformedValue],
505          * so its result should be cached.
506          */
507         @kotlin.jvm.JvmStatic
508         private fun calculateTransformedText(
509             untransformedValue: TextFieldCharSequence,
510             outputTransformation: OutputTransformation,
511             wedgeAffinity: SelectionWedgeAffinity
512         ): TransformedText? {
513             val offsetMappingCalculator = OffsetMappingCalculator()
514             val buffer =
515                 TextFieldBuffer(
516                     initialValue = untransformedValue,
517                     offsetMappingCalculator = offsetMappingCalculator
518                 )
519 
520             // This is the call to external code.
521             with(outputTransformation) { buffer.transformOutput() }
522 
523             // Avoid allocations + mapping if there weren't actually any transformations.
524             if (buffer.changes.changeCount == 0) {
525                 return null
526             }
527 
528             val transformedTextWithSelection =
529                 buffer.toTextFieldCharSequence(
530                     // Pass the calculator explicitly since the one on transformedText won't be
531                     // updated yet.
532                     selection =
533                         mapToTransformed(
534                             range = untransformedValue.selection,
535                             mapping = offsetMappingCalculator,
536                             selectionWedgeAffinity = wedgeAffinity
537                         ),
538                     composition =
539                         untransformedValue.composition?.let {
540                             mapToTransformed(
541                                 range = it,
542                                 mapping = offsetMappingCalculator,
543                                 selectionWedgeAffinity = wedgeAffinity
544                             )
545                         }
546                 )
547             return TransformedText(transformedTextWithSelection, offsetMappingCalculator)
548         }
549 
550         /**
551          * Applies a [CodepointTransformation] to a [TextFieldCharSequence], returning the
552          * transformed text content, the selection/cursor from the [untransformedValue] mapped to
553          * the offsets in the transformed text, and an [OffsetMappingCalculator] that can be used to
554          * map offsets in both directions between the transformed and untransformed text.
555          *
556          * This function is relatively expensive, since it creates a copy of [untransformedValue],
557          * so its result should be cached.
558          */
559         @kotlin.jvm.JvmStatic
560         private fun calculateTransformedText(
561             untransformedValue: TextFieldCharSequence,
562             codepointTransformation: CodepointTransformation,
563             wedgeAffinity: SelectionWedgeAffinity
564         ): TransformedText? {
565             val offsetMappingCalculator = OffsetMappingCalculator()
566 
567             // This is the call to external code. Returns same instance if no codepoints change.
568             val transformedText =
569                 untransformedValue.toVisualText(codepointTransformation, offsetMappingCalculator)
570 
571             // Avoid allocations + mapping if there weren't actually any transformations.
572             if (transformedText === untransformedValue) {
573                 return null
574             }
575 
576             val transformedTextWithSelection =
577                 TextFieldCharSequence(
578                     text = transformedText,
579                     // Pass the calculator explicitly since the one on transformedText won't be
580                     // updated
581                     // yet.
582                     selection =
583                         mapToTransformed(
584                             untransformedValue.selection,
585                             offsetMappingCalculator,
586                             wedgeAffinity
587                         ),
588                     composition =
589                         untransformedValue.composition?.let {
590                             mapToTransformed(it, offsetMappingCalculator, wedgeAffinity)
591                         }
592                 )
593             return TransformedText(transformedTextWithSelection, offsetMappingCalculator)
594         }
595 
596         /**
597          * Maps [range] from untransformed to transformed indices.
598          *
599          * @param selectionWedgeAffinity The [SelectionWedgeAffinity] to use to collapse the
600          *   transformed range if necessary. If null, the range will be returned uncollapsed.
601          */
602         @kotlin.jvm.JvmStatic
603         private fun mapToTransformed(
604             range: TextRange,
605             mapping: OffsetMappingCalculator,
606             selectionWedgeAffinity: SelectionWedgeAffinity? = null
607         ): TextRange {
608             var transformedStart = mapping.mapFromSource(range.start)
609             // Avoid calculating mapping again if it's going to be the same value.
610             var transformedEnd =
611                 if (range.collapsed) transformedStart
612                 else {
613                     mapping.mapFromSource(range.end)
614                 }
615 
616             // Do not use separate affinities when the selection is collapsed into a cursor.
617             // This can show a selected region around a wedge when there is no selection in
618             // the untransformed space. We use startAffinity for cursors.
619             val startAffinity = selectionWedgeAffinity?.startAffinity
620             val endAffinity =
621                 if (range.collapsed) {
622                     startAffinity
623                 } else {
624                     selectionWedgeAffinity?.endAffinity
625                 }
626 
627             if (startAffinity != null && !transformedStart.collapsed) {
628                 transformedStart =
629                     when (startAffinity) {
630                         WedgeAffinity.Start -> TextRange(transformedStart.start)
631                         WedgeAffinity.End -> TextRange(transformedStart.end)
632                     }
633             }
634 
635             if (endAffinity != null && !transformedEnd.collapsed) {
636                 transformedEnd =
637                     when (endAffinity) {
638                         WedgeAffinity.Start -> TextRange(transformedEnd.start)
639                         WedgeAffinity.End -> TextRange(transformedEnd.end)
640                     }
641             }
642 
643             val transformedMin = minOf(transformedStart.min, transformedEnd.min)
644             val transformedMax = maxOf(transformedStart.max, transformedEnd.max)
645             val transformedRange =
646                 if (range.reversed) {
647                     TextRange(transformedMax, transformedMin)
648                 } else {
649                     TextRange(transformedMin, transformedMax)
650                 }
651 
652             return transformedRange
653         }
654 
655         @kotlin.jvm.JvmStatic
656         private fun mapFromTransformed(
657             range: TextRange,
658             mapping: OffsetMappingCalculator
659         ): TextRange {
660             val untransformedStart = mapping.mapFromDest(range.start)
661             // Avoid calculating mapping again if it's going to be the same value.
662             val untransformedEnd =
663                 if (range.collapsed) untransformedStart
664                 else {
665                     mapping.mapFromDest(range.end)
666                 }
667 
668             val untransformedMin = minOf(untransformedStart.min, untransformedEnd.min)
669             val untransformedMax = maxOf(untransformedStart.max, untransformedEnd.max)
670             return if (range.reversed) {
671                 TextRange(untransformedMax, untransformedMin)
672             } else {
673                 TextRange(untransformedMin, untransformedMax)
674             }
675         }
676     }
677 }
678 
679 /**
680  * Represents the [WedgeAffinity] for both sides of a selection.
681  *
682  * If the selection is collapsed into a cursor, only [startAffinity] is used.
683  */
684 internal data class SelectionWedgeAffinity(
685     val startAffinity: WedgeAffinity,
686     val endAffinity: WedgeAffinity,
687 ) {
688     constructor(affinity: WedgeAffinity) : this(affinity, affinity)
689 }
690 
691 /**
692  * Determines which side of a wedge a selection marker should be considered to be on when the marker
693  * is in a wedge. A "wedge" is a range of text that the cursor is not allowed inside. A wedge is
694  * created when an [OutputTransformation] either inserts or replaces a non-empty string.
695  */
696 internal enum class WedgeAffinity {
697     Start,
698     End
699 }
700 
701 internal enum class IndexTransformationType {
702     Untransformed,
703     Insertion,
704     Replacement,
705     Deletion
706 }
707 
708 /**
709  * Determines if the [transformedQueryIndex] is inside an insertion, replacement, deletion, or none
710  * of the above as specified by the transformations on this [TransformedTextFieldState].
711  *
712  * This function uses continuation-passing style to return multiple values without allocating.
713  *
714  * @param onResult Called with the determined [IndexTransformationType] and the ranges that
715  *   [transformedQueryIndex] maps to both in the [TransformedTextFieldState.untransformedText] and
716  *   when that range is mapped back into the [TransformedTextFieldState.visualText].
717  */
getIndexTransformationTypenull718 internal inline fun <R> TransformedTextFieldState.getIndexTransformationType(
719     transformedQueryIndex: Int,
720     onResult: (IndexTransformationType, untransformed: TextRange, retransformed: TextRange) -> R
721 ): R {
722     val untransformed = mapFromTransformed(transformedQueryIndex)
723     val retransformed = mapToTransformed(untransformed)
724     val type =
725         when {
726             untransformed.collapsed && retransformed.collapsed -> {
727                 // Simple case: no transformation in effect.
728                 Untransformed
729             }
730             !untransformed.collapsed && !retransformed.collapsed -> {
731                 // Replacement: An non-empty range in the source was replaced with a non-empty
732                 // string.
733                 Replacement
734             }
735             untransformed.collapsed && !retransformed.collapsed -> {
736                 // Insertion: An empty range in the source was replaced with a non-empty range.
737                 Insertion
738             }
739             else /* !untransformed.collapsed && retransformed.collapsed */ -> {
740                 // Deletion: A non-empty range in the source was replaced with an empty string.
741                 Deletion
742             }
743         }
744     return onResult(type, untransformed, retransformed)
745 }
746