All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.androidx.compose.foundation.text.input.TextFieldState.kt Maven / Gradle / Ivy

/*
 * Copyright 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

@file:OptIn(ExperimentalFoundationApi::class)

package androidx.compose.foundation.text.input

import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.internal.checkPrecondition
import androidx.compose.foundation.text.input.internal.undo.TextFieldEditUndoBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collection.MutableVector
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.coerceIn
import androidx.compose.ui.text.style.TextDecoration

/**
 * The editable text state of a text field, including both the [text] itself and position of the
 * cursor or selection.
 *
 * To change the text field contents programmatically, call [edit], [setTextAndSelectAll],
 * [setTextAndPlaceCursorAtEnd], or [clearText]. Individual parts of the state like [text],
 * [selection], or [composition] can be read from any snapshot restart scope like Composable
 * functions. To observe these members from outside a restart scope, use `snapshotFlow {
 * textFieldState.text }` or `snapshotFlow { textFieldState.selection }`.
 *
 * When instantiating this class from a composable, use [rememberTextFieldState] to automatically
 * save and restore the field state. For more advanced use cases, pass [TextFieldState.Saver] to
 * [rememberSaveable].
 *
 * @sample androidx.compose.foundation.samples.BasicTextFieldStateCompleteSample
 */
@Stable
class TextFieldState
internal constructor(
    initialText: String,
    initialSelection: TextRange,
    initialTextUndoManager: TextUndoManager
) {

    constructor(
        initialText: String = "",
        initialSelection: TextRange = TextRange(initialText.length)
    ) : this(initialText, initialSelection, TextUndoManager())

    /** Manages the history of edit operations that happen in this [TextFieldState]. */
    internal val textUndoManager: TextUndoManager = initialTextUndoManager

    /**
     * The buffer used for applying editor commands from IME. All edits coming from gestures or IME
     * commands must be reflected on this buffer eventually.
     */
    @VisibleForTesting
    internal var mainBuffer: TextFieldBuffer =
        TextFieldBuffer(
            initialValue =
                TextFieldCharSequence(
                    text = initialText,
                    selection = initialSelection.coerceIn(0, initialText.length)
                )
        )

    /**
     * [TextFieldState] does not synchronize calls to [edit] but requires main thread access. It
     * also has no way to disallow reentrant behavior (nested calls to [edit]) through the API.
     * Instead we keep track of whether an edit session is currently running. If [edit] is called
     * concurrently or reentered, it should throw an exception. The only exception is if
     * [TextFieldState] is being modified in two different snapshots. Hence, this value is backed by
     * a snapshot state.
     */
    private var isEditing: Boolean by mutableStateOf(false)

    /**
     * The current text, selection, and composing region. This value will automatically update when
     * the user enters text or otherwise changes the text field contents. To change it
     * programmatically, call [edit].
     *
     * This is backed by snapshot state, so reading this property in a restartable function (e.g. a
     * composable function) will cause the function to restart when the text field's value changes.
     *
     * @sample androidx.compose.foundation.samples.BasicTextFieldTextDerivedStateSample
     * @see edit
     */
    internal var value: TextFieldCharSequence by
        mutableStateOf(TextFieldCharSequence(initialText, initialSelection))
        /** Do not set directly. Always go through [updateValueAndNotifyListeners]. */
        private set

    /**
     * The current text content. This value will automatically update when the user enters text or
     * otherwise changes the text field contents. To change it programmatically, call [edit].
     *
     * To observe changes to this property outside a restartable function, use `snapshotFlow { text
     * }`.
     *
     * @sample androidx.compose.foundation.samples.BasicTextFieldTextValuesSample
     * @see edit
     * @see snapshotFlow
     */
    val text: CharSequence
        get() = value.text

    /**
     * The current selection range. If the selection is collapsed, it represents cursor location.
     * This value will automatically update when the user enters text or otherwise changes the text
     * field selection range. To change it programmatically, call [edit].
     *
     * To observe changes to this property outside a restartable function, use `snapshotFlow {
     * selection }`.
     *
     * @see edit
     * @see snapshotFlow
     * @see TextFieldCharSequence.selection
     */
    val selection: TextRange
        get() = value.selection

    /**
     * The current composing range dictated by the IME. If null, there is no composing region.
     *
     * To observe changes to this property outside a restartable function, use `snapshotFlow {
     * composition }`.
     *
     * @see edit
     * @see snapshotFlow
     * @see TextFieldCharSequence.composition
     */
    val composition: TextRange?
        get() = value.composition

    /**
     * Runs [block] with a mutable version of the current state. The block can make changes to the
     * text and cursor/selection. See the documentation on [TextFieldBuffer] for a more detailed
     * description of the available operations.
     *
     * Make sure that you do not make concurrent calls to this function or call it again inside
     * [block]'s scope. Doing either of these actions will result in triggering an
     * [IllegalStateException].
     *
     * @sample androidx.compose.foundation.samples.BasicTextFieldStateEditSample
     * @see setTextAndPlaceCursorAtEnd
     * @see setTextAndSelectAll
     */
    inline fun edit(block: TextFieldBuffer.() -> Unit) {
        val mutableValue = startEdit()
        try {
            mutableValue.block()
            commitEdit(mutableValue)
        } finally {
            finishEditing()
        }
    }

    override fun toString(): String =
        Snapshot.withoutReadObservation { "TextFieldState(selection=$selection, text=\"$text\")" }

    /**
     * Undo history controller for this TextFieldState.
     *
     * @sample androidx.compose.foundation.samples.BasicTextFieldUndoSample
     */
    // TextField does not implement UndoState because Undo related APIs should be able to remain
    // separately experimental than TextFieldState
    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
    @ExperimentalFoundationApi
    @get:ExperimentalFoundationApi
    val undoState: UndoState = UndoState(this)

    @Suppress("ShowingMemberInHiddenClass")
    @PublishedApi
    internal fun startEdit(): TextFieldBuffer {
        val isEditingFreeze = Snapshot.withoutReadObservation { isEditing }
        checkPrecondition(!isEditingFreeze) {
            "TextFieldState does not support concurrent or nested editing."
        }
        isEditing = true
        return TextFieldBuffer(value)
    }

    /**
     * If the text or selection in [newValue] was actually modified, updates this state's internal
     * values. If [newValue] was not modified at all, the state is not updated, and this will not
     * invalidate anyone who is observing this state.
     *
     * @param newValue [TextFieldBuffer] that contains the latest updates
     */
    @Suppress("ShowingMemberInHiddenClass")
    @PublishedApi
    internal fun commitEdit(newValue: TextFieldBuffer) {
        val textChanged = newValue.changes.changeCount > 0
        val selectionChanged = newValue.selection != mainBuffer.selection
        if (textChanged) {
            // clear the undo history after a programmatic edit if the text content has changed
            textUndoManager.clearHistory()
        }
        syncMainBufferToTemporaryBuffer(
            temporaryBuffer = newValue,
            textChanged = textChanged,
            selectionChanged = selectionChanged
        )
    }

    @Suppress("ShowingMemberInHiddenClass")
    @PublishedApi
    internal fun finishEditing() {
        isEditing = false
    }

    /**
     * An edit block that updates [TextFieldState] on behalf of user actions such as gestures, IME
     * commands, hardware keyboard events, clipboard actions, and more. These modifications must
     * also run through the given [inputTransformation] since they are user actions.
     *
     * Be careful that this method is not snapshot aware. It is only safe to call this from main
     * thread, or global snapshot. Also, this function is defined as inline for performance gains,
     * and it's not actually safe to early return from [block].
     *
     * Also all user edits should be recorded by [textUndoManager] since reverting to a previous
     * state requires all edit operations to be executed in reverse. However, some commands like
     * cut, and paste should be atomic operations that do not merge with previous or next operations
     * in the Undo stack. This can be controlled by [undoBehavior].
     *
     * @param inputTransformation [InputTransformation] to run after [block] is applied
     * @param restartImeIfContentChanges Whether IME should be restarted if the proposed changes end
     *   up editing the text content. Only pass false to this argument if the source of the changes
     *   is IME itself.
     * @param block The function that updates the current buffer.
     */
    internal inline fun editAsUser(
        inputTransformation: InputTransformation?,
        restartImeIfContentChanges: Boolean = true,
        undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible,
        block: TextFieldBuffer.() -> Unit
    ) {
        mainBuffer.changeTracker.clearChanges()
        mainBuffer.block()

        commitEditAsUser(
            inputTransformation = inputTransformation,
            restartImeIfContentChanges = restartImeIfContentChanges,
            undoBehavior = undoBehavior
        )
    }

    /**
     * Edits the contents of this [TextFieldState] without going through an [InputTransformation],
     * or recording the changes to the [textUndoManager]. IME would still be notified of any changes
     * committed by [block].
     *
     * This method of editing is not recommended for majority of use cases. It is originally added
     * to support applying of undo/redo actions without clearing the history. Also, it doesn't
     * allocate an additional buffer like [edit] method because changes are ignored and it's not a
     * public API.
     */
    internal inline fun editWithNoSideEffects(block: TextFieldBuffer.() -> Unit) {
        mainBuffer.changeTracker.clearChanges()
        mainBuffer.block()

        val afterEditValue = mainBuffer.toTextFieldCharSequence()

        updateValueAndNotifyListeners(
            oldValue = value,
            newValue = afterEditValue,
            restartImeIfContentChanges = true
        )
    }

    // Do not inline this function into editAsUser. Inline functions should be kept short.
    private fun commitEditAsUser(
        inputTransformation: InputTransformation?,
        restartImeIfContentChanges: Boolean = true,
        undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible,
    ) {
        val beforeEditValue = value

        // first immediately check whether there's any actual change of content or selection.
        // We only look at the operation count for content change because inputTransformation
        // should still run even if text doesn't change as a result of an input.
        // If there is no change at all, or only composition or highlight has changed, we can end
        // early.
        if (
            mainBuffer.changeTracker.changeCount == 0 &&
                beforeEditValue.selection == mainBuffer.selection
        ) {
            if (
                beforeEditValue.composition != mainBuffer.composition ||
                    beforeEditValue.highlight != mainBuffer.highlight ||
                    beforeEditValue.composingAnnotations != mainBuffer.composingAnnotations
            ) {
                // edit operation caused no change to text content or selection
                // No need to run an existing InputTransformation, or record an undo. Only update
                // the IME that composition has been accepted.
                updateValueAndNotifyListeners(
                    oldValue = value,
                    newValue =
                        TextFieldCharSequence(
                            text = mainBuffer.toString(),
                            selection = mainBuffer.selection,
                            composition = mainBuffer.composition,
                            highlight = mainBuffer.highlight,
                            composingAnnotations =
                                finalizeComposingAnnotations(
                                    composition = mainBuffer.composition,
                                    annotationList = mainBuffer.composingAnnotations
                                )
                        ),
                    restartImeIfContentChanges = restartImeIfContentChanges
                )
            }
            return
        }

        // There's a meaningful change to the buffer, let's run the full logic.
        // first take a _snapshot_ of current state of the mainBuffer after changes are applied.
        val afterEditValue =
            TextFieldCharSequence(
                text = mainBuffer.toString(),
                selection = mainBuffer.selection,
                composition = mainBuffer.composition,
                highlight = mainBuffer.highlight,
                composingAnnotations =
                    finalizeComposingAnnotations(
                        composition = mainBuffer.composition,
                        annotationList = mainBuffer.composingAnnotations
                    )
            )

        // if there's no filter; just record the undo, update the snapshot value, end.
        if (inputTransformation == null) {
            updateValueAndNotifyListeners(
                oldValue = beforeEditValue,
                newValue = afterEditValue,
                restartImeIfContentChanges = restartImeIfContentChanges
            )
            recordEditForUndo(
                previousValue = beforeEditValue,
                postValue = afterEditValue,
                changes = mainBuffer.changeTracker,
                undoBehavior = undoBehavior
            )
            return
        }

        // Prepare a TextFieldBuffer to run InputTransformation. TextFieldBuffer should be
        // initialized with the edits that are already applied on mainBuffer, hence the difference
        // between originalValue and initialValue.
        val textFieldBuffer =
            TextFieldBuffer(
                originalValue = beforeEditValue,
                initialValue = afterEditValue,
                initialChanges = mainBuffer.changeTracker
            )

        // apply the inputTransformation.
        with(inputTransformation) { textFieldBuffer.transformInput() }

        val textChangedByFilter = !textFieldBuffer.asCharSequence().contentEquals(afterEditValue)
        val selectionChangedByFilter = textFieldBuffer.selection != afterEditValue.selection
        if (textChangedByFilter || selectionChangedByFilter) {
            syncMainBufferToTemporaryBuffer(
                temporaryBuffer = textFieldBuffer,
                textChanged = textChangedByFilter,
                selectionChanged = selectionChangedByFilter
            )
        } else {
            updateValueAndNotifyListeners(
                oldValue = beforeEditValue,
                // If neither the text nor the selection changed by the filter, we want to preserve
                // the composition. Otherwise, the IME will reset it anyway.
                newValue =
                    textFieldBuffer.toTextFieldCharSequence(
                        composition = afterEditValue.composition
                    ),
                restartImeIfContentChanges = restartImeIfContentChanges
            )
        }
        // textFieldBuffer contains all the changes from both the user and the filter.
        recordEditForUndo(
            previousValue = beforeEditValue,
            postValue = value,
            changes = textFieldBuffer.changes,
            undoBehavior = undoBehavior
        )
    }

    /**
     * There are 3 types of edits that are defined on [TextFieldState];
     * 1. Programmatic changes that are created by [edit] function,
     * 2. User edits coming from the system that are initiated through IME, semantics, or gestures
     * 3. Applying Undo/Redo actions that should not trigger side effects.
     *
     * Eventually all changes, no matter the source, should be committed to [value]. Also, they have
     * to trigger the content change listeners.
     *
     * Finally notifies the listeners in [notifyImeListeners] that the contents of this
     * [TextFieldState] has changed.
     */
    private fun updateValueAndNotifyListeners(
        oldValue: TextFieldCharSequence,
        newValue: TextFieldCharSequence,
        restartImeIfContentChanges: Boolean
    ) {
        // value must be set before notifyImeListeners are called. Even though we are sending the
        // previous and current values, a system callback may request the latest state e.g. IME
        // restartInput call is handled before notifyImeListeners return.
        value = newValue
        finishEditing()

        notifyImeListeners.forEach { it.onChange(oldValue, newValue, restartImeIfContentChanges) }
    }

    /**
     * Records the difference between [previousValue] and [postValue], defined by [changes], into
     * [textUndoManager] according to the strategy defined by [undoBehavior].
     */
    private fun recordEditForUndo(
        previousValue: TextFieldCharSequence,
        postValue: TextFieldCharSequence,
        changes: TextFieldBuffer.ChangeList,
        undoBehavior: TextFieldEditUndoBehavior
    ) {
        when (undoBehavior) {
            TextFieldEditUndoBehavior.ClearHistory -> {
                textUndoManager.clearHistory()
            }
            TextFieldEditUndoBehavior.MergeIfPossible -> {
                textUndoManager.recordChanges(
                    pre = previousValue,
                    post = postValue,
                    changes = changes,
                    allowMerge = true
                )
            }
            TextFieldEditUndoBehavior.NeverMerge -> {
                textUndoManager.recordChanges(
                    pre = previousValue,
                    post = postValue,
                    changes = changes,
                    allowMerge = false
                )
            }
        }
    }

    internal fun addNotifyImeListener(notifyImeListener: NotifyImeListener) {
        notifyImeListeners.add(notifyImeListener)
    }

    internal fun removeNotifyImeListener(notifyImeListener: NotifyImeListener) {
        notifyImeListeners.remove(notifyImeListener)
    }

    /**
     * A listener that can be attached to a [TextFieldState] to listen for change events that may
     * interest IME.
     *
     * State in [TextFieldState] can change through various means but categorically there are two
     * sources; Developer([TextFieldState.edit]) and User([TextFieldState.editAsUser]). Only
     * non-InputTransformed IME sourced changes can skip updating the IME. Otherwise, all changes
     * must be sent to the IME to let it synchronize its state with the [TextFieldState]. Such a
     * communication channel is established by the IME registering a [NotifyImeListener] on a
     * [TextFieldState].
     */
    internal fun interface NotifyImeListener {

        /**
         * Called when the value in [TextFieldState] changes via any source. The
         * [restartImeIfContentChanges] flag determines whether a text change between [oldValue] and
         * [newValue] should restart the ongoing input connection. Selection changes never require a
         * restart.
         */
        fun onChange(
            oldValue: TextFieldCharSequence,
            newValue: TextFieldCharSequence,
            restartImeIfContentChanges: Boolean
        )
    }

    /**
     * Carries changes made to a [temporaryBuffer] into [mainBuffer], then updates the [value]. This
     * usually happens when the edit source is something programmatic like [edit] or
     * [InputTransformation]. Normally IME commands are applied directly on [mainBuffer].
     *
     * @param temporaryBuffer Source buffer that will be used to sync the mainBuffer.
     * @param textChanged Whether the text content inside [temporaryBuffer] is different than
     *   [mainBuffer]'s text content. Although this value can be calculated by this function, some
     *   callers already do the comparison before hand, so there's no need to recalculate it.
     * @param selectionChanged Whether the selection inside [temporaryBuffer] is different than
     *   [mainBuffer]'s selection.
     */
    @VisibleForTesting
    internal fun syncMainBufferToTemporaryBuffer(
        temporaryBuffer: TextFieldBuffer,
        textChanged: Boolean,
        selectionChanged: Boolean
    ) {
        val oldValue = mainBuffer.toTextFieldCharSequence()

        if (textChanged) {
            // reset the buffer in its entirety
            mainBuffer =
                TextFieldBuffer(
                    initialValue =
                        TextFieldCharSequence(
                            text = temporaryBuffer.toString(),
                            selection = temporaryBuffer.selection
                        ),
                )
        } else if (selectionChanged) {
            mainBuffer.selection =
                TextRange(temporaryBuffer.selection.start, temporaryBuffer.selection.end)
        }

        // Composition should be decided by the IME after the content or selection has been
        // changed programmatically, outside the knowledge of the IME.
        mainBuffer.commitComposition()

        val finalValue = mainBuffer.toTextFieldCharSequence()

        // We cannot use `value` as the old value here because intermediate IME changes are only
        // applied on mainBuffer (this only happens if syncMainBufferToTemporaryBuffer is triggered
        // after an InputTransformation). We must pass in the latest state just before finalValue is
        // calculated. This is the state IME knows about and is synced with.
        updateValueAndNotifyListeners(
            oldValue = oldValue,
            newValue = finalValue,
            restartImeIfContentChanges = true
        )
    }

    private val notifyImeListeners = mutableVectorOf()

    /**
     * Saves and restores a [TextFieldState] for [rememberSaveable].
     *
     * @see rememberTextFieldState
     */
    // Preserve nullability since this is public API.
    @Suppress("RedundantNullableReturnType")
    object Saver : androidx.compose.runtime.saveable.Saver {

        override fun SaverScope.save(value: TextFieldState): Any? {
            return listOf(
                value.text.toString(),
                value.selection.start,
                value.selection.end,
                with(TextUndoManager.Companion.Saver) { save(value.textUndoManager) }
            )
        }

        override fun restore(value: Any): TextFieldState? {
            val (text, selectionStart, selectionEnd, savedTextUndoManager) = value as List<*>
            return TextFieldState(
                initialText = text as String,
                initialSelection =
                    TextRange(start = selectionStart as Int, end = selectionEnd as Int),
                initialTextUndoManager =
                    with(TextUndoManager.Companion.Saver) { restore(savedTextUndoManager!!) }!!
            )
        }
    }
}

/**
 * Create and remember a [TextFieldState]. The state is remembered using [rememberSaveable] and so
 * will be saved and restored with the composition.
 *
 * If you need to store a [TextFieldState] in another object, use the [TextFieldState.Saver] object
 * to manually save and restore the state.
 *
 * @param initialText The initial text state. If a different value is passed in a subsequent
 *   recomposition, the value of the state will _not_ be updated. To update the state after it's
 *   initialized, call methods on [TextFieldState].
 * @param initialSelection The initial selection state. If a different value is passed in a
 *   subsequent recomposition, the value of the state will _not_ be updated. To update the state
 *   after it's initialized, call methods on [TextFieldState].
 */
@Composable
fun rememberTextFieldState(
    initialText: String = "",
    initialSelection: TextRange = TextRange(initialText.length)
): TextFieldState =
    rememberSaveable(saver = TextFieldState.Saver) { TextFieldState(initialText, initialSelection) }

/**
 * Sets the text in this [TextFieldState] to [text], replacing any text that was previously there,
 * and places the cursor at the end of the new text.
 *
 * To perform more complicated edits on the text, call [TextFieldState.edit]. This function is
 * equivalent to calling:
 * ```
 * edit {
 *   replace(0, length, text)
 *   placeCursorAtEnd()
 * }
 * ```
 *
 * @see setTextAndSelectAll
 * @see clearText
 * @see TextFieldBuffer.placeCursorAtEnd
 */
fun TextFieldState.setTextAndPlaceCursorAtEnd(text: String) {
    edit {
        replace(0, length, text)
        placeCursorAtEnd()
    }
}

/**
 * Sets the text in this [TextFieldState] to [text], replacing any text that was previously there,
 * and selects all the text.
 *
 * To perform more complicated edits on the text, call [TextFieldState.edit]. This function is
 * equivalent to calling:
 * ```
 * edit {
 *   replace(0, length, text)
 *   selectAll()
 * }
 * ```
 *
 * @see setTextAndPlaceCursorAtEnd
 * @see clearText
 * @see TextFieldBuffer.selectAll
 */
fun TextFieldState.setTextAndSelectAll(text: String) {
    edit {
        replace(0, length, text)
        selectAll()
    }
}

/**
 * Deletes all the text in the state.
 *
 * To perform more complicated edits on the text, call [TextFieldState.edit]. This function is
 * equivalent to calling:
 * ```
 * edit {
 *   delete(0, length)
 *   placeCursorAtEnd()
 * }
 * ```
 *
 * @see setTextAndPlaceCursorAtEnd
 * @see setTextAndSelectAll
 */
fun TextFieldState.clearText() {
    edit {
        delete(0, length)
        placeCursorAtEnd()
    }
}

/**
 * Final deciding property for which annotations would be rendered for the composing region. If the
 * IME has not set any composing annotations and the composing region is not collapsed, we need to
 * add the specific underline styling.
 */
@Suppress("ListIterator")
private fun finalizeComposingAnnotations(
    composition: TextRange?,
    annotationList: MutableVector?
): List =
    when {
        annotationList != null && annotationList.isNotEmpty() -> {
            // it is important to freeze the mutable list into an immutable list because
            // mutable list is sustained inside the EditingBuffer and TextFieldCharSequence
            // must be read-only.
            annotationList.asMutableList().toList()
        }
        composition != null && !composition.collapsed -> {
            listOf(
                AnnotatedString.Range(
                    SpanStyle(textDecoration = TextDecoration.Underline),
                    start = composition.min,
                    end = composition.max
                )
            )
        }
        else -> emptyList()
    }

/**
 * Creates a temporary, mutable [TextFieldBuffer] representing the current state of this
 * [TextFieldState].
 *
 * Use a [TextFieldBuffer] to:
 * * Apply transformations for testing purposes
 * * Preview how the TextField would render with a specific [OutputTransformation]
 *
 * This is similar to calling [TextFieldState.edit], but without committing the changes back to the
 * [TextFieldState].
 *
 * **Important:** A [TextFieldBuffer] is intended for short-term use. Let the garbage collecter
 * dispose of it when you're finished to avoid unnecessary memory usage.
 *
 * @sample androidx.compose.foundation.samples.TextFieldStateApplyOutputTransformation
 */
fun TextFieldState.toTextFieldBuffer(): TextFieldBuffer {
    return TextFieldBuffer(value)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy