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