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

Go to download

Higher level abstractions of the Compose UI primitives. This library is design system agnostic, providing the high-level building blocks for both application and design-system developers

There is a newer version: 1.8.0-alpha01
Show newest version
/*
 * 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.text.input.internal.EditingBuffer
import androidx.compose.foundation.text.input.internal.undo.TextFieldEditUndoBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
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.TextRange
import androidx.compose.ui.text.coerceIn
import androidx.compose.ui.text.input.TextFieldValue

internal fun TextFieldState(initialValue: TextFieldValue): TextFieldState {
    return TextFieldState(
        initialText = initialValue.text,
        initialSelection = initialValue.selection
    )
}

/**
 * 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 editing 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: EditingBuffer = EditingBuffer(
        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 }
        check(!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(
            textFieldBuffer = newValue,
            newComposition = null, // new composition will be decided by the IME
            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 editing buffer.
     */
    internal inline fun editAsUser(
        inputTransformation: InputTransformation?,
        restartImeIfContentChanges: Boolean = true,
        undoBehavior: TextFieldEditUndoBehavior = TextFieldEditUndoBehavior.MergeIfPossible,
        block: EditingBuffer.() -> 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: EditingBuffer.() -> Unit) {
        mainBuffer.changeTracker.clearChanges()
        mainBuffer.block()

        val afterEditValue = TextFieldCharSequence(
            text = mainBuffer.toString(),
            selection = mainBuffer.selection,
            composition = mainBuffer.composition
        )

        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) {
                // 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
                    ),
                    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
        )

        // 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(
                textFieldBuffer = textFieldBuffer,
                // Composition should be decided by the IME after the content or selection has been
                // changed programmatically, outside the knowledge of IME.
                newComposition = null,
                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. However, we have two different buffers called
     * [EditingBuffer] and [TextFieldBuffer]. All developer facing APIs use [TextFieldBuffer] to
     * change the state but internal ones use [EditingBuffer]. This function consolidates both
     * forms of updates, then commits the result to [value].
     *
     * 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 [TextFieldBuffer] into [mainBuffer], then updates the [value].
     * This usually happens when the edit source is something programmatic like [edit] or
     * [InputTransformation]. IME commands are applied directly on [mainBuffer].
     *
     * @param textFieldBuffer Source buffer that will be used to sync the mainBuffer.
     * @param newComposition TextFieldBuffer does not allow changing composition. This parameter
     * is an opportunity to decide what the mainBuffer's new composition should be.
     * @param textChanged Whether the text content inside [textFieldBuffer] 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 [textFieldBuffer] is different than
     * [mainBuffer]'s selection.
     */
    @VisibleForTesting
    internal fun syncMainBufferToTemporaryBuffer(
        textFieldBuffer: TextFieldBuffer,
        newComposition: TextRange?,
        textChanged: Boolean,
        selectionChanged: Boolean
    ) {
        val bufferString = mainBuffer.toString()

        val bufferState = TextFieldCharSequence(
            bufferString,
            mainBuffer.selection,
            mainBuffer.composition
        )

        val compositionChanged = newComposition != mainBuffer.composition

        if (textChanged) {
            // reset the buffer in its entirety
            mainBuffer = EditingBuffer(
                text = textFieldBuffer.toString(),
                selection = textFieldBuffer.selection
            )
        } else if (selectionChanged) {
            mainBuffer.setSelection(textFieldBuffer.selection.start, textFieldBuffer.selection.end)
        }

        if (newComposition == null || newComposition.collapsed) {
            mainBuffer.commitComposition()
        } else {
            mainBuffer.setComposition(newComposition.min, newComposition.max)
        }

        if (textChanged || (!selectionChanged && compositionChanged)) {
            mainBuffer.commitComposition()
        }

        val finalValue = TextFieldCharSequence(
            text = if (textChanged) textFieldBuffer.toString() else bufferString,
            selection = mainBuffer.selection,
            composition = mainBuffer.composition
        )

        // 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 = bufferState,
            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.
 */
@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()
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy