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