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 @file:OptIn(ExperimentalFoundationApi::class)
18
19 package androidx.compose.foundation.text.input
20
21 import androidx.annotation.VisibleForTesting
22 import androidx.compose.foundation.ExperimentalFoundationApi
23 import androidx.compose.foundation.internal.checkPrecondition
24 import androidx.compose.foundation.text.input.internal.undo.TextFieldEditUndoBehavior
25 import androidx.compose.runtime.Composable
26 import androidx.compose.runtime.Stable
27 import androidx.compose.runtime.annotation.RememberInComposition
28 import androidx.compose.runtime.collection.MutableVector
29 import androidx.compose.runtime.collection.mutableVectorOf
30 import androidx.compose.runtime.getValue
31 import androidx.compose.runtime.mutableStateOf
32 import androidx.compose.runtime.saveable.SaverScope
33 import androidx.compose.runtime.saveable.rememberSaveable
34 import androidx.compose.runtime.setValue
35 import androidx.compose.runtime.snapshotFlow
36 import androidx.compose.runtime.snapshots.Snapshot
37 import androidx.compose.ui.text.AnnotatedString
38 import androidx.compose.ui.text.SpanStyle
39 import androidx.compose.ui.text.TextRange
40 import androidx.compose.ui.text.coerceIn
41 import androidx.compose.ui.text.style.TextDecoration
42
43 /**
44 * The editable text state of a text field, including both the [text] itself and position of the
45 * cursor or selection.
46 *
47 * To change the text field contents programmatically, call [edit], [setTextAndSelectAll],
48 * [setTextAndPlaceCursorAtEnd], or [clearText]. Individual parts of the state like [text],
49 * [selection], or [composition] can be read from any snapshot restart scope like Composable
50 * functions. To observe these members from outside a restart scope, use `snapshotFlow {
51 * textFieldState.text }` or `snapshotFlow { textFieldState.selection }`.
52 *
53 * When instantiating this class from a composable, use [rememberTextFieldState] to automatically
54 * save and restore the field state. For more advanced use cases, pass [TextFieldState.Saver] to
55 * [rememberSaveable].
56 *
57 * @sample androidx.compose.foundation.samples.BasicTextFieldStateCompleteSample
58 */
59 @Stable
60 class TextFieldState
61 internal constructor(
62 initialText: String,
63 initialSelection: TextRange,
64 initialTextUndoManager: TextUndoManager
65 ) {
66
67 @RememberInComposition
68 constructor(
69 initialText: String = "",
70 initialSelection: TextRange = TextRange(initialText.length)
71 ) : this(initialText, initialSelection, TextUndoManager())
72
73 /** Manages the history of edit operations that happen in this [TextFieldState]. */
74 internal val textUndoManager: TextUndoManager = initialTextUndoManager
75
76 /**
77 * The buffer used for applying editor commands from IME. All edits coming from gestures or IME
78 * commands must be reflected on this buffer eventually.
79 */
80 @VisibleForTesting
81 internal var mainBuffer: TextFieldBuffer =
82 TextFieldBuffer(
83 initialValue =
84 TextFieldCharSequence(
85 text = initialText,
86 selection = initialSelection.coerceIn(0, initialText.length)
87 )
88 )
89
90 /**
91 * [TextFieldState] does not synchronize calls to [edit] but requires main thread access. It
92 * also has no way to disallow reentrant behavior (nested calls to [edit]) through the API.
93 * Instead we keep track of whether an edit session is currently running. If [edit] is called
94 * concurrently or reentered, it should throw an exception. The only exception is if
95 * [TextFieldState] is being modified in two different snapshots. Hence, this value is backed by
96 * a snapshot state.
97 */
98 private var isEditing: Boolean by mutableStateOf(false)
99
100 /**
101 * The current text, selection, and composing region. This value will automatically update when
102 * the user enters text or otherwise changes the text field contents. To change it
103 * programmatically, call [edit].
104 *
105 * This is backed by snapshot state, so reading this property in a restartable function (e.g. a
106 * composable function) will cause the function to restart when the text field's value changes.
107 *
108 * @sample androidx.compose.foundation.samples.BasicTextFieldTextDerivedStateSample
109 * @see edit
110 */
111 internal var value: TextFieldCharSequence by
112 mutableStateOf(TextFieldCharSequence(initialText, initialSelection))
113 /** Do not set directly. Always go through [updateValueAndNotifyListeners]. */
114 private set
115
116 /**
117 * The current text content. This value will automatically update when the user enters text or
118 * otherwise changes the text field contents. To change it programmatically, call [edit].
119 *
120 * To observe changes to this property outside a restartable function, use `snapshotFlow { text
121 * }`.
122 *
123 * @sample androidx.compose.foundation.samples.BasicTextFieldTextValuesSample
124 * @see edit
125 * @see snapshotFlow
126 */
127 val text: CharSequence
128 get() = value.text
129
130 /**
131 * The current selection range. If the selection is collapsed, it represents cursor location.
132 * This value will automatically update when the user enters text or otherwise changes the text
133 * field selection range. To change it programmatically, call [edit].
134 *
135 * To observe changes to this property outside a restartable function, use `snapshotFlow {
136 * selection }`.
137 *
138 * @see edit
139 * @see snapshotFlow
140 * @see TextFieldCharSequence.selection
141 */
142 val selection: TextRange
143 get() = value.selection
144
145 /**
146 * The current composing range dictated by the IME. If null, there is no composing region.
147 *
148 * To observe changes to this property outside a restartable function, use `snapshotFlow {
149 * composition }`.
150 *
151 * @see edit
152 * @see snapshotFlow
153 * @see TextFieldCharSequence.composition
154 */
155 val composition: TextRange?
156 get() = value.composition
157
158 /**
159 * Runs [block] with a mutable version of the current state. The block can make changes to the
160 * text and cursor/selection. See the documentation on [TextFieldBuffer] for a more detailed
161 * description of the available operations.
162 *
163 * Make sure that you do not make concurrent calls to this function or call it again inside
164 * [block]'s scope. Doing either of these actions will result in triggering an
165 * [IllegalStateException].
166 *
167 * @sample androidx.compose.foundation.samples.BasicTextFieldStateEditSample
168 * @see setTextAndPlaceCursorAtEnd
169 * @see setTextAndSelectAll
170 */
editnull171 inline fun edit(block: TextFieldBuffer.() -> Unit) {
172 val mutableValue = startEdit()
173 try {
174 mutableValue.block()
175 commitEdit(mutableValue)
176 } finally {
177 finishEditing()
178 }
179 }
180
toStringnull181 override fun toString(): String =
182 Snapshot.withoutReadObservation { "TextFieldState(selection=$selection, text=\"$text\")" }
183
184 /**
185 * Undo history controller for this TextFieldState.
186 *
187 * @sample androidx.compose.foundation.samples.BasicTextFieldUndoSample
188 */
189 // TextField does not implement UndoState because Undo related APIs should be able to remain
190 // separately experimental than TextFieldState
191 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
192 @ExperimentalFoundationApi
193 @get:ExperimentalFoundationApi
194 val undoState: UndoState = UndoState(this)
195
196 @Suppress("ShowingMemberInHiddenClass")
197 @PublishedApi
startEditnull198 internal fun startEdit(): TextFieldBuffer {
199 val isEditingFreeze = Snapshot.withoutReadObservation { isEditing }
200 checkPrecondition(!isEditingFreeze) {
201 "TextFieldState does not support concurrent or nested editing."
202 }
203 isEditing = true
204 return TextFieldBuffer(value)
205 }
206
207 /**
208 * If the text or selection in [newValue] was actually modified, updates this state's internal
209 * values. If [newValue] was not modified at all, the state is not updated, and this will not
210 * invalidate anyone who is observing this state.
211 *
212 * @param newValue [TextFieldBuffer] that contains the latest updates
213 */
214 @Suppress("ShowingMemberInHiddenClass")
215 @PublishedApi
commitEditnull216 internal fun commitEdit(newValue: TextFieldBuffer) {
217 val textChanged = newValue.changes.changeCount > 0
218 val selectionChanged = newValue.selection != mainBuffer.selection
219 if (textChanged) {
220 // clear the undo history after a programmatic edit if the text content has changed
221 textUndoManager.clearHistory()
222 }
223 syncMainBufferToTemporaryBuffer(
224 temporaryBuffer = newValue,
225 textChanged = textChanged,
226 selectionChanged = selectionChanged
227 )
228 }
229
230 @Suppress("ShowingMemberInHiddenClass")
231 @PublishedApi
finishEditingnull232 internal fun finishEditing() {
233 isEditing = false
234 }
235
236 /**
237 * An edit block that updates [TextFieldState] on behalf of user actions such as gestures, IME
238 * commands, hardware keyboard events, clipboard actions, and more. These modifications must
239 * also run through the given [inputTransformation] since they are user actions.
240 *
241 * Be careful that this method is not snapshot aware. It is only safe to call this from main
242 * thread, or global snapshot. Also, this function is defined as inline for performance gains,
243 * and it's not actually safe to early return from [block].
244 *
245 * Also all user edits should be recorded by [textUndoManager] since reverting to a previous
246 * state requires all edit operations to be executed in reverse. However, some commands like
247 * cut, and paste should be atomic operations that do not merge with previous or next operations
248 * in the Undo stack. This can be controlled by [undoBehavior].
249 *
250 * @param inputTransformation [InputTransformation] to run after [block] is applied
251 * @param restartImeIfContentChanges Whether IME should be restarted if the proposed changes end
252 * up editing the text content. Only pass false to this argument if the source of the changes
253 * is IME itself.
254 * @param block The function that updates the current buffer.
255 */
editAsUsernull256 internal inline fun editAsUser(
257 inputTransformation: InputTransformation?,
258 restartImeIfContentChanges: Boolean = true,
259 undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible,
260 block: TextFieldBuffer.() -> Unit
261 ) {
262 mainBuffer.changeTracker.clearChanges()
263 mainBuffer.block()
264
265 commitEditAsUser(
266 inputTransformation = inputTransformation,
267 restartImeIfContentChanges = restartImeIfContentChanges,
268 undoBehavior = undoBehavior
269 )
270 }
271
272 /**
273 * Edits the contents of this [TextFieldState] without going through an [InputTransformation],
274 * or recording the changes to the [textUndoManager]. IME would still be notified of any changes
275 * committed by [block].
276 *
277 * This method of editing is not recommended for majority of use cases. It is originally added
278 * to support applying of undo/redo actions without clearing the history. Also, it doesn't
279 * allocate an additional buffer like [edit] method because changes are ignored and it's not a
280 * public API.
281 */
editWithNoSideEffectsnull282 internal inline fun editWithNoSideEffects(block: TextFieldBuffer.() -> Unit) {
283 mainBuffer.changeTracker.clearChanges()
284 mainBuffer.block()
285
286 val afterEditValue = mainBuffer.toTextFieldCharSequence()
287
288 updateValueAndNotifyListeners(
289 oldValue = value,
290 newValue = afterEditValue,
291 restartImeIfContentChanges = true
292 )
293 }
294
295 // Do not inline this function into editAsUser. Inline functions should be kept short.
commitEditAsUsernull296 private fun commitEditAsUser(
297 inputTransformation: InputTransformation?,
298 restartImeIfContentChanges: Boolean = true,
299 undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible,
300 ) {
301 val beforeEditValue = value
302
303 // first immediately check whether there's any actual change of content or selection.
304 // We only look at the operation count for content change because inputTransformation
305 // should still run even if text doesn't change as a result of an input.
306 // If there is no change at all, or only composition or highlight has changed, we can end
307 // early.
308 if (
309 mainBuffer.changeTracker.changeCount == 0 &&
310 beforeEditValue.selection == mainBuffer.selection
311 ) {
312 if (
313 beforeEditValue.composition != mainBuffer.composition ||
314 beforeEditValue.highlight != mainBuffer.highlight ||
315 beforeEditValue.composingAnnotations != mainBuffer.composingAnnotations
316 ) {
317 // edit operation caused no change to text content or selection
318 // No need to run an existing InputTransformation, or record an undo. Only update
319 // the IME that composition has been accepted.
320 updateValueAndNotifyListeners(
321 oldValue = value,
322 newValue =
323 TextFieldCharSequence(
324 text = mainBuffer.toString(),
325 selection = mainBuffer.selection,
326 composition = mainBuffer.composition,
327 highlight = mainBuffer.highlight,
328 composingAnnotations =
329 finalizeComposingAnnotations(
330 composition = mainBuffer.composition,
331 annotationList = mainBuffer.composingAnnotations
332 )
333 ),
334 restartImeIfContentChanges = restartImeIfContentChanges
335 )
336 }
337 return
338 }
339
340 // Eventually we may need to run a string equality check between old value and the new value
341 // This is an O(n) operation, meaning that it gets as expensive as the text length.
342 // Therefore we create this flag to remind ourselves whether the original changes may have
343 // caused a content difference. This value being false is a strong indicator that the
344 // content definitely hasn't changed. However this being true only introduces a possibility.
345 // The content change may have been an exact string replacement like "ab" => "ab".
346 val contentMayHaveChanged = mainBuffer.changeTracker.changeCount != 0
347
348 // There's a meaningful change to the buffer, either content or selection. We need to run
349 // the full logic including InputTransformation. But F=first take a _snapshot_ of current
350 // state of the mainBuffer after changes are applied.
351 val afterEditValue =
352 TextFieldCharSequence(
353 text = mainBuffer.toString(),
354 selection = mainBuffer.selection,
355 composition = mainBuffer.composition,
356 highlight = mainBuffer.highlight,
357 composingAnnotations =
358 finalizeComposingAnnotations(
359 composition = mainBuffer.composition,
360 annotationList = mainBuffer.composingAnnotations
361 )
362 )
363
364 // if there's no filter; just record the undo, update the snapshot value, end.
365 if (inputTransformation == null) {
366 updateValueAndNotifyListeners(
367 oldValue = beforeEditValue,
368 newValue = afterEditValue,
369 // updateValueAndNotifyListeners use restartImeIfContentChanges flag to possibly
370 // skip doing string equality check. Here we add our own flag to indicate the
371 // possibility of content changing. Since false value is a string indicator,
372 // this added logic works.
373 restartImeIfContentChanges = contentMayHaveChanged && restartImeIfContentChanges
374 )
375 recordEditForUndo(
376 previousValue = beforeEditValue,
377 postValue = afterEditValue,
378 changes = mainBuffer.changeTracker,
379 undoBehavior = undoBehavior
380 )
381 return
382 }
383
384 // Prepare a TextFieldBuffer to run InputTransformation. TextFieldBuffer should be
385 // initialized with the edits that are already applied on mainBuffer, hence the difference
386 // between originalValue and initialValue.
387 val textFieldBuffer =
388 TextFieldBuffer(
389 originalValue = beforeEditValue,
390 initialValue = afterEditValue,
391 initialChanges = mainBuffer.changeTracker
392 )
393
394 // apply the inputTransformation.
395 with(inputTransformation) { textFieldBuffer.transformInput() }
396
397 val textChangedByFilter = !textFieldBuffer.asCharSequence().contentEquals(afterEditValue)
398 val selectionChangedByFilter = textFieldBuffer.selection != afterEditValue.selection
399 if (textChangedByFilter || selectionChangedByFilter) {
400 syncMainBufferToTemporaryBuffer(
401 temporaryBuffer = textFieldBuffer,
402 textChanged = textChangedByFilter,
403 selectionChanged = selectionChangedByFilter
404 )
405 } else {
406 updateValueAndNotifyListeners(
407 oldValue = beforeEditValue,
408 // If neither the text nor the selection changed by the filter, we want to preserve
409 // the composition. Otherwise, the IME will reset it anyway.
410 newValue =
411 textFieldBuffer.toTextFieldCharSequence(
412 composition = afterEditValue.composition
413 ),
414 restartImeIfContentChanges = restartImeIfContentChanges
415 )
416 }
417 // textFieldBuffer contains all the changes from both the user and the filter.
418 recordEditForUndo(
419 previousValue = beforeEditValue,
420 postValue = value,
421 changes = textFieldBuffer.changes,
422 undoBehavior = undoBehavior
423 )
424 }
425
426 /**
427 * There are 3 types of edits that are defined on [TextFieldState];
428 * 1. Programmatic changes that are created by [edit] function,
429 * 2. User edits coming from the system that are initiated through IME, semantics, or gestures
430 * 3. Applying Undo/Redo actions that should not trigger side effects.
431 *
432 * Eventually all changes, no matter the source, should be committed to [value]. Also, they have
433 * to trigger the content change listeners.
434 *
435 * Finally notifies the listeners in [notifyImeListeners] that the contents of this
436 * [TextFieldState] has changed.
437 */
updateValueAndNotifyListenersnull438 private fun updateValueAndNotifyListeners(
439 oldValue: TextFieldCharSequence,
440 newValue: TextFieldCharSequence,
441 restartImeIfContentChanges: Boolean
442 ) {
443 // value must be set before notifyImeListeners are called. Even though we are sending the
444 // previous and current values, a system callback may request the latest state e.g. IME
445 // restartInput call is handled before notifyImeListeners return.
446 value = newValue
447 finishEditing()
448
449 notifyImeListeners.forEach {
450 it.onChange(
451 oldValue = oldValue,
452 newValue = newValue,
453 restartIme =
454 restartImeIfContentChanges &&
455 !oldValue.contentEquals(newValue)
456 // No need to restart the IME if there wasn't a composing region. This is
457 // useful to not unnecessarily restart digit only, or password fields.
458 &&
459 oldValue.composition != null
460 )
461 }
462 }
463
464 /**
465 * Records the difference between [previousValue] and [postValue], defined by [changes], into
466 * [textUndoManager] according to the strategy defined by [undoBehavior].
467 */
recordEditForUndonull468 private fun recordEditForUndo(
469 previousValue: TextFieldCharSequence,
470 postValue: TextFieldCharSequence,
471 changes: TextFieldBuffer.ChangeList,
472 undoBehavior: TextFieldEditUndoBehavior
473 ) {
474 when (undoBehavior) {
475 TextFieldEditUndoBehavior.ClearHistory -> {
476 textUndoManager.clearHistory()
477 }
478 TextFieldEditUndoBehavior.MergeIfPossible -> {
479 textUndoManager.recordChanges(
480 pre = previousValue,
481 post = postValue,
482 changes = changes,
483 allowMerge = true
484 )
485 }
486 TextFieldEditUndoBehavior.NeverMerge -> {
487 textUndoManager.recordChanges(
488 pre = previousValue,
489 post = postValue,
490 changes = changes,
491 allowMerge = false
492 )
493 }
494 }
495 }
496
addNotifyImeListenernull497 internal fun addNotifyImeListener(notifyImeListener: NotifyImeListener) {
498 notifyImeListeners.add(notifyImeListener)
499 }
500
removeNotifyImeListenernull501 internal fun removeNotifyImeListener(notifyImeListener: NotifyImeListener) {
502 notifyImeListeners.remove(notifyImeListener)
503 }
504
505 /**
506 * A listener that can be attached to a [TextFieldState] to listen for change events that may
507 * interest IME.
508 *
509 * State in [TextFieldState] can change through various means but categorically there are two
510 * sources; Developer([TextFieldState.edit]) and User([TextFieldState.editAsUser]). Only
511 * non-InputTransformed, IME sourced changes can skip updating the IME. Otherwise, all changes
512 * must be sent to the IME to let it synchronize its state with the [TextFieldState]. Such a
513 * communication channel is established by the text input session registering a
514 * [NotifyImeListener] on a [TextFieldState].
515 */
516 internal fun interface NotifyImeListener {
517
518 /**
519 * Called when the value in [TextFieldState] changes via any source. The [restartIme] flag
520 * determines whether the ongoing input connection should be restarted. Selection or
521 * Composition range changes never require a restart.
522 *
523 * @param oldValue The previous value of the [TextFieldState] before the latest changes are
524 * applied with one exception. If an [InputTransformation] is applied on the changes
525 * coming from the IME, we use the value after user changes are applied but before
526 * [InputTransformation]. This is essentially the last known state to the IME.
527 * @param newValue Current state of the [TextFieldState]. This is always equal to the
528 * [TextFieldState.value] at the time of calling this function.
529 * @param restartIme Whether to ignore other parameters and basically restart the input
530 * session with new configuration.
531 */
onChangenull532 fun onChange(
533 oldValue: TextFieldCharSequence,
534 newValue: TextFieldCharSequence,
535 restartIme: Boolean
536 )
537 }
538
539 /**
540 * Carries changes made to a [temporaryBuffer] into [mainBuffer], then updates the [value]. This
541 * usually happens when the edit source is something programmatic like [edit] or
542 * [InputTransformation]. Normally IME commands are applied directly on [mainBuffer].
543 *
544 * @param temporaryBuffer Source buffer that will be used to sync the mainBuffer.
545 * @param textChanged Whether the text content inside [temporaryBuffer] is different than
546 * [mainBuffer]'s text content. Although this value can be calculated by this function, some
547 * callers already do the comparison before hand, so there's no need to recalculate it.
548 * @param selectionChanged Whether the selection inside [temporaryBuffer] is different than
549 * [mainBuffer]'s selection.
550 */
551 @VisibleForTesting
552 internal fun syncMainBufferToTemporaryBuffer(
553 temporaryBuffer: TextFieldBuffer,
554 textChanged: Boolean,
555 selectionChanged: Boolean
556 ) {
557 val oldValue = mainBuffer.toTextFieldCharSequence()
558
559 if (textChanged) {
560 // reset the buffer in its entirety
561 mainBuffer =
562 TextFieldBuffer(
563 initialValue =
564 TextFieldCharSequence(
565 text = temporaryBuffer.toString(),
566 selection = temporaryBuffer.selection
567 ),
568 )
569 } else if (selectionChanged) {
570 mainBuffer.selection =
571 TextRange(temporaryBuffer.selection.start, temporaryBuffer.selection.end)
572 }
573
574 // Composition should be decided by the IME after the content or selection has been
575 // changed programmatically, outside the knowledge of the IME.
576 if (
577 textChanged || selectionChanged || oldValue.composition != temporaryBuffer.composition
578 ) {
579 mainBuffer.commitComposition()
580 }
581
582 val finalValue = mainBuffer.toTextFieldCharSequence()
583
584 // We cannot use `value` as the old value here because intermediate IME changes are only
585 // applied on mainBuffer (this only happens if syncMainBufferToTemporaryBuffer is triggered
586 // after an InputTransformation). We must pass in the latest state just before finalValue is
587 // calculated. This is the state IME knows about and is synced with.
588 updateValueAndNotifyListeners(
589 oldValue = oldValue,
590 newValue = finalValue,
591 restartImeIfContentChanges = true
592 )
593 }
594
595 private val notifyImeListeners = mutableVectorOf<NotifyImeListener>()
596
597 /**
598 * Saves and restores a [TextFieldState] for [rememberSaveable].
599 *
600 * @see rememberTextFieldState
601 */
602 // Preserve nullability since this is public API.
603 @Suppress("RedundantNullableReturnType")
604 object Saver : androidx.compose.runtime.saveable.Saver<TextFieldState, Any> {
605
savenull606 override fun SaverScope.save(value: TextFieldState): Any? {
607 return listOf(
608 value.text.toString(),
609 value.selection.start,
610 value.selection.end,
611 with(TextUndoManager.Companion.Saver) { save(value.textUndoManager) }
612 )
613 }
614
restorenull615 override fun restore(value: Any): TextFieldState? {
616 val (text, selectionStart, selectionEnd, savedTextUndoManager) = value as List<*>
617 return TextFieldState(
618 initialText = text as String,
619 initialSelection =
620 TextRange(start = selectionStart as Int, end = selectionEnd as Int),
621 initialTextUndoManager =
622 with(TextUndoManager.Companion.Saver) { restore(savedTextUndoManager!!) }!!
623 )
624 }
625 }
626 }
627
628 /**
629 * Create and remember a [TextFieldState]. The state is remembered using [rememberSaveable] and so
630 * will be saved and restored with the composition.
631 *
632 * If you need to store a [TextFieldState] in another object, use the [TextFieldState.Saver] object
633 * to manually save and restore the state.
634 *
635 * @param initialText The initial text state. If a different value is passed in a subsequent
636 * recomposition, the value of the state will _not_ be updated. To update the state after it's
637 * initialized, call methods on [TextFieldState].
638 * @param initialSelection The initial selection state. If a different value is passed in a
639 * subsequent recomposition, the value of the state will _not_ be updated. To update the state
640 * after it's initialized, call methods on [TextFieldState].
641 */
642 @Composable
rememberTextFieldStatenull643 fun rememberTextFieldState(
644 initialText: String = "",
645 initialSelection: TextRange = TextRange(initialText.length)
646 ): TextFieldState =
647 rememberSaveable(saver = TextFieldState.Saver) { TextFieldState(initialText, initialSelection) }
648
649 /**
650 * Sets the text in this [TextFieldState] to [text], replacing any text that was previously there,
651 * and places the cursor at the end of the new text.
652 *
653 * To perform more complicated edits on the text, call [TextFieldState.edit]. This function is
654 * equivalent to calling:
655 * ```
656 * edit {
657 * replace(0, length, text)
658 * placeCursorAtEnd()
659 * }
660 * ```
661 *
662 * @see setTextAndSelectAll
663 * @see clearText
664 * @see TextFieldBuffer.placeCursorAtEnd
665 */
setTextAndPlaceCursorAtEndnull666 fun TextFieldState.setTextAndPlaceCursorAtEnd(text: String) {
667 edit {
668 replace(0, length, text)
669 placeCursorAtEnd()
670 }
671 }
672
673 /**
674 * Sets the text in this [TextFieldState] to [text], replacing any text that was previously there,
675 * and selects all the text.
676 *
677 * To perform more complicated edits on the text, call [TextFieldState.edit]. This function is
678 * equivalent to calling:
679 * ```
680 * edit {
681 * replace(0, length, text)
682 * selectAll()
683 * }
684 * ```
685 *
686 * @see setTextAndPlaceCursorAtEnd
687 * @see clearText
688 * @see TextFieldBuffer.selectAll
689 */
setTextAndSelectAllnull690 fun TextFieldState.setTextAndSelectAll(text: String) {
691 edit {
692 replace(0, length, text)
693 selectAll()
694 }
695 }
696
697 /**
698 * Deletes all the text in the state.
699 *
700 * To perform more complicated edits on the text, call [TextFieldState.edit]. This function is
701 * equivalent to calling:
702 * ```
703 * edit {
704 * delete(0, length)
705 * placeCursorAtEnd()
706 * }
707 * ```
708 *
709 * @see setTextAndPlaceCursorAtEnd
710 * @see setTextAndSelectAll
711 */
clearTextnull712 fun TextFieldState.clearText() {
713 edit {
714 delete(0, length)
715 placeCursorAtEnd()
716 }
717 }
718
719 /**
720 * Final deciding property for which annotations would be rendered for the composing region. If the
721 * IME has not set any composing annotations and the composing region is not collapsed, we need to
722 * add the specific underline styling.
723 */
724 @Suppress("ListIterator")
finalizeComposingAnnotationsnull725 private fun finalizeComposingAnnotations(
726 composition: TextRange?,
727 annotationList: MutableVector<PlacedAnnotation>?
728 ): List<PlacedAnnotation> =
729 when {
730 annotationList != null && annotationList.isNotEmpty() -> {
731 // it is important to freeze the mutable list into an immutable list because
732 // mutable list is sustained inside the EditingBuffer and TextFieldCharSequence
733 // must be read-only.
734 annotationList.asMutableList().toList()
735 }
736 composition != null && !composition.collapsed -> {
737 listOf(
738 AnnotatedString.Range(
739 SpanStyle(textDecoration = TextDecoration.Underline),
740 start = composition.min,
741 end = composition.max
742 )
743 )
744 }
745 else -> emptyList()
746 }
747
748 /**
749 * Creates a temporary, mutable [TextFieldBuffer] representing the current state of this
750 * [TextFieldState].
751 *
752 * Use a [TextFieldBuffer] to:
753 * * Apply transformations for testing purposes
754 * * Preview how the TextField would render with a specific [OutputTransformation]
755 *
756 * This is similar to calling [TextFieldState.edit], but without committing the changes back to the
757 * [TextFieldState].
758 *
759 * **Important:** A [TextFieldBuffer] is intended for short-term use. Let the garbage collecter
760 * dispose of it when you're finished to avoid unnecessary memory usage.
761 *
762 * @sample androidx.compose.foundation.samples.TextFieldStateApplyOutputTransformation
763 */
toTextFieldBuffernull764 fun TextFieldState.toTextFieldBuffer(): TextFieldBuffer {
765 return TextFieldBuffer(value)
766 }
767