1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.text.input
18 
19 import androidx.compose.runtime.Immutable
20 import androidx.compose.runtime.saveable.Saver
21 import androidx.compose.ui.text.AnnotatedString
22 import androidx.compose.ui.text.AnnotatedStringSaver
23 import androidx.compose.ui.text.Saver
24 import androidx.compose.ui.text.TextRange
25 import androidx.compose.ui.text.coerceIn
26 import androidx.compose.ui.text.restore
27 import androidx.compose.ui.text.save
28 import kotlin.math.max
29 import kotlin.math.min
30 
31 /**
32  * A class holding information about the editing state.
33  *
34  * The input service updates text selection, cursor, text and text composition. This class
35  * represents those values and it is possible to observe changes to those values in the text editing
36  * composables.
37  *
38  * This class stores a snapshot of the input state of the edit buffer and provide utility functions
39  * for answering IME requests such as getTextBeforeCursor, getSelectedText.
40  *
41  * Input service composition is an instance of text produced by IME. An example visual for the
42  * composition is that the currently composed word is visually separated from others with underline,
43  * or text background. For description of composition please check
44  * [W3C IME Composition](https://www.w3.org/TR/ime-api/#ime-composition).
45  *
46  * IME composition is defined by [composition] parameter and function. When a [TextFieldValue] with
47  * null [composition] is passed to a TextField, if there was an active [composition] on the text,
48  * the changes will be applied. Applying a composition will accept the changes that were still being
49  * composed by IME. Please use [copy] functions if you do not want to intentionally apply the
50  * ongoing IME composition.
51  *
52  * @param annotatedString the text to be rendered.
53  * @param selection the selection range. If the selection is collapsed, it represents cursor
54  *   location. When selection range is out of bounds, it is constrained with the text length.
55  * @param composition the composition range, null means empty composition or apply if a composition
56  *   exists on the text. Owned by IME, and if you have an instance of [TextFieldValue] please use
57  *   [copy] functions if you do not want to intentionally change the value of this field.
58  */
59 @Immutable
60 class TextFieldValue
61 constructor(
62     val annotatedString: AnnotatedString,
63     selection: TextRange = TextRange.Zero,
64     composition: TextRange? = null
65 ) {
66     /**
67      * @param text the text to be rendered.
68      * @param selection the selection range. If the selection is collapsed, it represents cursor
69      *   location. When selection range is out of bounds, it is constrained with the text length.
70      * @param composition the composition range, null means empty composition or apply if a
71      *   composition exists on the text. Owned by IME, and if you have an instance of
72      *   [TextFieldValue] please use [copy] functions if you do not want to intentionally change the
73      *   value of this field.
74      */
75     constructor(
76         text: String = "",
77         selection: TextRange = TextRange.Zero,
78         composition: TextRange? = null
79     ) : this(AnnotatedString(text), selection, composition)
80 
81     val text: String
82         get() = annotatedString.text
83 
84     /**
85      * The selection range. If the selection is collapsed, it represents cursor location. When
86      * selection range is out of bounds, it is constrained with the text length.
87      */
88     val selection: TextRange = selection.coerceIn(0, text.length)
89 
90     /**
91      * Composition range created by IME. If null, there is no composition range.
92      *
93      * Input service composition is an instance of text produced by IME. An example visual for the
94      * composition is that the currently composed word is visually separated from others with
95      * underline, or text background. For description of composition please check
96      * [W3C IME Composition](https://www.w3.org/TR/ime-api/#ime-composition)
97      *
98      * Composition can be set on the by the system, however it is possible to apply an existing
99      * composition by setting the value to null. Applying a composition will accept the changes that
100      * were still being composed by IME.
101      */
102     val composition: TextRange? = composition?.coerceIn(0, text.length)
103 
104     /** Returns a copy of the TextFieldValue. */
copynull105     fun copy(
106         annotatedString: AnnotatedString = this.annotatedString,
107         selection: TextRange = this.selection,
108         composition: TextRange? = this.composition
109     ): TextFieldValue {
110         return TextFieldValue(annotatedString, selection, composition)
111     }
112 
113     /** Returns a copy of the TextFieldValue. */
copynull114     fun copy(
115         text: String,
116         selection: TextRange = this.selection,
117         composition: TextRange? = this.composition
118     ): TextFieldValue {
119         return TextFieldValue(AnnotatedString(text), selection, composition)
120     }
121 
122     // auto generated equals method
equalsnull123     override fun equals(other: Any?): Boolean {
124         if (this === other) return true
125         if (other !is TextFieldValue) return false
126 
127         // compare selection and composition first for early return
128         //  before comparing string.
129         return selection == other.selection &&
130             composition == other.composition &&
131             annotatedString == other.annotatedString
132     }
133 
134     // auto generated hashCode method
hashCodenull135     override fun hashCode(): Int {
136         var result = annotatedString.hashCode()
137         result = 31 * result + selection.hashCode()
138         result = 31 * result + (composition?.hashCode() ?: 0)
139         return result
140     }
141 
toStringnull142     override fun toString(): String {
143         return "TextFieldValue(" +
144             "text='$annotatedString', " +
145             "selection=$selection, " +
146             "composition=$composition)"
147     }
148 
149     companion object {
150         /** The default [Saver] implementation for [TextFieldValue]. */
151         val Saver =
152             Saver<TextFieldValue, Any>(
<lambda>null153                 save = {
154                     arrayListOf(
155                         save(it.annotatedString, AnnotatedStringSaver, this),
156                         save(it.selection, TextRange.Saver, this),
157                     )
158                 },
<lambda>null159                 restore = {
160                     @Suppress("UNCHECKED_CAST") val list = it as List<Any>
161                     TextFieldValue(
162                         annotatedString = restore(list[0], AnnotatedStringSaver)!!,
163                         selection = restore(list[1], TextRange.Saver)!!,
164                     )
165                 }
166             )
167     }
168 }
169 
170 /**
171  * Returns the text before the selection.
172  *
173  * @param maxChars maximum number of characters (inclusive) before the minimum value in
174  *   [TextFieldValue.selection].
175  * @see TextRange.min
176  */
TextFieldValuenull177 fun TextFieldValue.getTextBeforeSelection(maxChars: Int): AnnotatedString =
178     annotatedString.subSequence(max(0, selection.min - maxChars), selection.min)
179 
180 /**
181  * Returns the text after the selection.
182  *
183  * @param maxChars maximum number of characters (exclusive) after the maximum value in
184  *   [TextFieldValue.selection].
185  * @see TextRange.max
186  */
187 fun TextFieldValue.getTextAfterSelection(maxChars: Int): AnnotatedString =
188     annotatedString.subSequence(selection.max, min(selection.max + maxChars, text.length))
189 
190 /** Returns the currently selected text. */
191 fun TextFieldValue.getSelectedText(): AnnotatedString = annotatedString.subSequence(selection)
192