/*
* Copyright 2019 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.
*/
package androidx.compose.ui.text.input
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.saveable.Saver
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.AnnotatedStringSaver
import androidx.compose.ui.text.Saver
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.constrain
import androidx.compose.ui.text.restore
import androidx.compose.ui.text.save
import kotlin.math.max
import kotlin.math.min
/**
* A class holding information about the editing state.
*
* The input service updates text selection, cursor, text and text composition. This class
* represents those values and it is possible to observe changes to those values in the text
* editing composables.
*
* This class stores a snapshot of the input state of the edit buffer and provide utility functions
* for answering IME requests such as getTextBeforeCursor, getSelectedText.
*
* Input service composition is an instance of text produced by IME. An example visual for the
* composition is that the currently composed word is visually separated from others with
* underline, or text background. For description of composition please check
* [W3C IME Composition](https://www.w3.org/TR/ime-api/#ime-composition).
*
* IME composition is defined by [composition] parameter and function. When a
* [TextFieldValue] with null [composition] is passed to a TextField, if there was an
* active [composition] on the text, the changes will be applied. Applying a composition will
* accept the changes that were still being composed by IME. Please use [copy]
* functions if you do not want to intentionally apply the ongoing IME composition.
*
* @param annotatedString the text to be rendered.
* @param selection the selection range. If the selection is collapsed, it represents cursor
* location. When selection range is out of bounds, it is constrained with the text length.
* @param composition the composition range, null means empty composition or apply if a
* composition exists on the text. Owned by IME, and if you have an instance of [TextFieldValue]
* please use [copy] functions if you do not want to intentionally change the value of this
* field.
*
*/
@Immutable
class TextFieldValue constructor(
val annotatedString: AnnotatedString,
selection: TextRange = TextRange.Zero,
composition: TextRange? = null
) {
/**
* @param text the text to be rendered.
* @param selection the selection range. If the selection is collapsed, it represents cursor
* location. When selection range is out of bounds, it is constrained with the text length.
* @param composition the composition range, null means empty composition or apply if a
* composition exists on the text. Owned by IME, and if you have an instance of [TextFieldValue]
* please use [copy] functions if you do not want to intentionally change the value of this
* field.
*/
constructor(
text: String = "",
selection: TextRange = TextRange.Zero,
composition: TextRange? = null
) : this(AnnotatedString(text), selection, composition)
val text: String get() = annotatedString.text
/**
* The selection range. If the selection is collapsed, it represents cursor
* location. When selection range is out of bounds, it is constrained with the text length.
*/
val selection: TextRange = selection.constrain(0, text.length)
/**
* Composition range created by IME. If null, there is no composition range.
*
* Input service composition is an instance of text produced by IME. An example visual for the
* composition is that the currently composed word is visually separated from others with
* underline, or text background. For description of composition please check
* [W3C IME Composition](https://www.w3.org/TR/ime-api/#ime-composition)
*
* Composition can be set on the by the system, however it is possible to apply an existing
* composition by setting the value to null. Applying a composition will accept the changes
* that were still being composed by IME.
*/
val composition: TextRange? = composition?.constrain(0, text.length)
/**
* Returns a copy of the TextFieldValue.
*/
fun copy(
annotatedString: AnnotatedString = this.annotatedString,
selection: TextRange = this.selection,
composition: TextRange? = this.composition
): TextFieldValue {
return TextFieldValue(annotatedString, selection, composition)
}
/**
* Returns a copy of the TextFieldValue.
*/
fun copy(
text: String,
selection: TextRange = this.selection,
composition: TextRange? = this.composition
): TextFieldValue {
return TextFieldValue(AnnotatedString(text), selection, composition)
}
// auto generated equals method
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TextFieldValue) return false
// compare selection and composition first for early return
// before comparing string.
return selection == other.selection &&
composition == other.composition &&
annotatedString == other.annotatedString
}
// auto generated hashCode method
override fun hashCode(): Int {
var result = annotatedString.hashCode()
result = 31 * result + selection.hashCode()
result = 31 * result + (composition?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "TextFieldValue(" +
"text='$annotatedString', " +
"selection=$selection, " +
"composition=$composition)"
}
companion object {
/**
* The default [Saver] implementation for [TextFieldValue].
*/
val Saver = Saver<TextFieldValue, Any>(
save = {
arrayListOf(
save(it.annotatedString, AnnotatedStringSaver, this),
save(it.selection, TextRange.Saver, this),
)
},
restore = {
@Suppress("UNCHECKED_CAST")
val list = it as List<Any>
TextFieldValue(
annotatedString = restore(list[0], AnnotatedStringSaver)!!,
selection = restore(list[1], TextRange.Saver)!!,
)
}
)
}
}
/**
* Returns the text before the selection.
*
* @param maxChars maximum number of characters (inclusive) before the minimum value in
* [TextFieldValue.selection].
*
* @see TextRange.min
*/
fun TextFieldValue.getTextBeforeSelection(maxChars: Int): AnnotatedString =
annotatedString.subSequence(max(0, selection.min - maxChars), selection.min)
/**
* Returns the text after the selection.
*
* @param maxChars maximum number of characters (exclusive) after the maximum value in
* [TextFieldValue.selection].
*
* @see TextRange.max
*/
fun TextFieldValue.getTextAfterSelection(maxChars: Int): AnnotatedString =
annotatedString.subSequence(selection.max, min(selection.max + maxChars, text.length))
/**
* Returns the currently selected text.
*/
fun TextFieldValue.getSelectedText(): AnnotatedString = annotatedString.subSequence(selection)