1 /*
<lambda>null2  * Copyright 2024 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.foundation.text.input
18 
19 import androidx.compose.foundation.ExperimentalFoundationApi
20 import androidx.compose.foundation.internal.requirePrecondition
21 import androidx.compose.foundation.text.KeyboardOptions
22 import androidx.compose.runtime.Stable
23 import androidx.compose.ui.semantics.SemanticsPropertyReceiver
24 import androidx.compose.ui.semantics.maxTextLength
25 import androidx.compose.ui.text.input.KeyboardCapitalization
26 import androidx.compose.ui.text.intl.Locale
27 import androidx.compose.ui.text.substring
28 import androidx.compose.ui.text.toUpperCase
29 
30 /**
31  * A function that is ran after every change made to a [TextFieldState] by user input and can change
32  * or reject that input.
33  *
34  * Input transformations are ran after hardware and software keyboard events, when text is pasted or
35  * dropped into the field, or when an accessibility service changes the text.
36  *
37  * To chain filters together, call [then].
38  *
39  * Prebuilt filters are provided for common filter operations. See:
40  * - [InputTransformation].[maxLength]`()`
41  * - [InputTransformation].[allCaps]`()`
42  *
43  * @sample androidx.compose.foundation.samples.BasicTextFieldCustomInputTransformationSample
44  */
45 @Stable
46 fun interface InputTransformation {
47 
48     /**
49      * Optional [KeyboardOptions] that will be used as the default keyboard options for configuring
50      * the IME. The options passed directly to the text field composable will always override this.
51      */
52     val keyboardOptions: KeyboardOptions?
53         get() = null
54 
55     /**
56      * Optional semantics configuration that can update certain characteristics of the applied
57      * TextField, e.g. [SemanticsPropertyReceiver.maxTextLength].
58      */
59     fun SemanticsPropertyReceiver.applySemantics() = Unit
60 
61     /**
62      * The transform operation. For more information see the documentation on [InputTransformation].
63      *
64      * This function is scoped to [TextFieldBuffer], a buffer that can be changed in-place to alter
65      * or reject the changes or set the selection.
66      *
67      * To reject all changes in the scoped [TextFieldBuffer], call
68      * [revertAllChanges][TextFieldBuffer.revertAllChanges].
69      *
70      * When multiple [InputTransformation]s are linked together, the [transformInput] function of
71      * the first transformation is invoked before the second one. Once the changes are made to
72      * [TextFieldBuffer] by the initial [InputTransformation] in the chain, the same instance of
73      * [TextFieldBuffer] is forwarded to the subsequent transformation in the chain. Note that
74      * [TextFieldBuffer.originalValue] never changes while the buffer is passed along the chain.
75      * This sequence persists until the chain reaches its conclusion.
76      */
77     fun TextFieldBuffer.transformInput()
78 
79     companion object : InputTransformation {
80         override fun TextFieldBuffer.transformInput() {
81             // Noop.
82         }
83     }
84 }
85 
86 // region Pre-built transformations
87 
88 /**
89  * Creates a filter chain that will run [next] after this. Filters are applied sequentially, so any
90  * changes made by this filter will be visible to [next].
91  *
92  * The returned filter will use the [KeyboardOptions] from [next] if non-null, otherwise it will use
93  * the options from this transformation.
94  *
95  * @sample androidx.compose.foundation.samples.BasicTextFieldInputTransformationChainingSample
96  * @param next The [InputTransformation] that will be ran after this one.
97  */
98 @Stable
InputTransformationnull99 fun InputTransformation.then(next: InputTransformation): InputTransformation =
100     FilterChain(this, next)
101 
102 /**
103  * Creates an [InputTransformation] from a function that accepts both the current and proposed
104  * [TextFieldCharSequence] and returns the [TextFieldCharSequence] to use for the field.
105  *
106  * [transformation] can return either `current`, `proposed`, or a completely different value.
107  *
108  * The selection or cursor will be updated automatically. For more control of selection implement
109  * [InputTransformation] directly.
110  *
111  * @sample androidx.compose.foundation.samples.BasicTextFieldInputTransformationByValueChooseSample
112  * @sample androidx.compose.foundation.samples.BasicTextFieldInputTransformationByValueReplaceSample
113  */
114 @Stable
115 fun InputTransformation.byValue(
116     transformation: (current: CharSequence, proposed: CharSequence) -> CharSequence
117 ): InputTransformation = this.then(InputTransformationByValue(transformation))
118 
119 /**
120  * Returns a [InputTransformation] that forces all text to be uppercase.
121  *
122  * This transformation automatically configures the keyboard to capitalize all characters.
123  *
124  * @param locale The [Locale] in which to perform the case conversion.
125  */
126 @Stable
127 fun InputTransformation.allCaps(locale: Locale): InputTransformation =
128     this.then(AllCapsTransformation(locale))
129 
130 /**
131  * Returns [InputTransformation] that rejects input which causes the total length of the text field
132  * to be more than [maxLength] characters.
133  */
134 @Stable
135 fun InputTransformation.maxLength(maxLength: Int): InputTransformation =
136     this.then(MaxLengthFilter(maxLength))
137 
138 // endregion
139 // region Transformation implementations
140 
141 private class FilterChain(
142     private val first: InputTransformation,
143     private val second: InputTransformation,
144 ) : InputTransformation {
145 
146     override val keyboardOptions: KeyboardOptions?
147         get() =
148             second.keyboardOptions?.fillUnspecifiedValuesWith(first.keyboardOptions)
149                 ?: first.keyboardOptions
150 
151     override fun SemanticsPropertyReceiver.applySemantics() {
152         with(first) { applySemantics() }
153         with(second) { applySemantics() }
154     }
155 
156     override fun TextFieldBuffer.transformInput() {
157         with(first) { transformInput() }
158         with(second) { transformInput() }
159     }
160 
161     override fun toString(): String = "$first.then($second)"
162 
163     override fun equals(other: Any?): Boolean {
164         if (this === other) return true
165         if (other === null) return false
166         if (this::class != other::class) return false
167 
168         other as FilterChain
169 
170         if (first != other.first) return false
171         if (second != other.second) return false
172         if (keyboardOptions != other.keyboardOptions) return false
173 
174         return true
175     }
176 
177     override fun hashCode(): Int {
178         var result = first.hashCode()
179         result = 31 * result + second.hashCode()
180         result = 32 * result + keyboardOptions.hashCode()
181         return result
182     }
183 }
184 
185 private data class InputTransformationByValue(
186     val transformation: (current: CharSequence, proposed: CharSequence) -> CharSequence
187 ) : InputTransformation {
transformInputnull188     override fun TextFieldBuffer.transformInput() {
189         val proposed = toTextFieldCharSequence()
190         val accepted = transformation(originalValue, proposed)
191         when {
192             // These are reference comparisons – text comparison will be done by setTextIfChanged.
193             accepted === proposed -> return
194             accepted === originalValue -> revertAllChanges()
195             else -> {
196                 setTextIfChanged(accepted)
197             }
198         }
199     }
200 
toStringnull201     override fun toString(): String = "InputTransformation.byValue(transformation=$transformation)"
202 }
203 
204 // This is a very naive implementation for now, not intended to be production-ready.
205 @OptIn(ExperimentalFoundationApi::class)
206 private data class AllCapsTransformation(private val locale: Locale) : InputTransformation {
207     override val keyboardOptions =
208         KeyboardOptions(capitalization = KeyboardCapitalization.Characters)
209 
210     override fun TextFieldBuffer.transformInput() {
211         // only update inserted content
212         changes.forEachChange { range, _ ->
213             if (!range.collapsed) {
214                 replace(range.min, range.max, asCharSequence().substring(range).toUpperCase(locale))
215             }
216         }
217     }
218 
219     override fun toString(): String = "InputTransformation.allCaps(locale=$locale)"
220 }
221 
222 // This is a very naive implementation for now, not intended to be production-ready.
223 private data class MaxLengthFilter(private val maxLength: Int) : InputTransformation {
224 
225     init {
<lambda>null226         requirePrecondition(maxLength >= 0) { "maxLength must be at least zero" }
227     }
228 
applySemanticsnull229     override fun SemanticsPropertyReceiver.applySemantics() {
230         maxTextLength = maxLength
231     }
232 
transformInputnull233     override fun TextFieldBuffer.transformInput() {
234         if (length > maxLength) {
235             revertAllChanges()
236         }
237     }
238 
toStringnull239     override fun toString(): String {
240         return "InputTransformation.maxLength($maxLength)"
241     }
242 }
243