1 /*
<lambda>null2  * Copyright 2025 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.internal
18 
19 import androidx.compose.foundation.text.LegacyTextFieldState
20 import androidx.compose.foundation.text.TextFieldDelegate
21 import androidx.compose.foundation.text.selection.TextFieldSelectionManager
22 import androidx.compose.foundation.text.tapToFocus
23 import androidx.compose.ui.autofill.ContentDataType
24 import androidx.compose.ui.focus.FocusRequester
25 import androidx.compose.ui.node.DelegatingNode
26 import androidx.compose.ui.node.ModifierNodeElement
27 import androidx.compose.ui.node.SemanticsModifierNode
28 import androidx.compose.ui.node.invalidateSemantics
29 import androidx.compose.ui.node.requestAutofill
30 import androidx.compose.ui.platform.InspectorInfo
31 import androidx.compose.ui.semantics.SemanticsPropertyReceiver
32 import androidx.compose.ui.semantics.contentDataType
33 import androidx.compose.ui.semantics.copyText
34 import androidx.compose.ui.semantics.cutText
35 import androidx.compose.ui.semantics.disabled
36 import androidx.compose.ui.semantics.editableText
37 import androidx.compose.ui.semantics.getTextLayoutResult
38 import androidx.compose.ui.semantics.inputText
39 import androidx.compose.ui.semantics.insertTextAtCursor
40 import androidx.compose.ui.semantics.isEditable
41 import androidx.compose.ui.semantics.onAutofillText
42 import androidx.compose.ui.semantics.onClick
43 import androidx.compose.ui.semantics.onImeAction
44 import androidx.compose.ui.semantics.onLongClick
45 import androidx.compose.ui.semantics.password
46 import androidx.compose.ui.semantics.pasteText
47 import androidx.compose.ui.semantics.setSelection
48 import androidx.compose.ui.semantics.setText
49 import androidx.compose.ui.semantics.textSelectionRange
50 import androidx.compose.ui.text.TextRange
51 import androidx.compose.ui.text.input.CommitTextCommand
52 import androidx.compose.ui.text.input.DeleteAllCommand
53 import androidx.compose.ui.text.input.FinishComposingTextCommand
54 import androidx.compose.ui.text.input.ImeOptions
55 import androidx.compose.ui.text.input.OffsetMapping
56 import androidx.compose.ui.text.input.TextFieldValue
57 import androidx.compose.ui.text.input.TransformedText
58 
59 internal data class CoreTextFieldSemanticsModifier(
60     val transformedText: TransformedText,
61     val value: TextFieldValue,
62     val state: LegacyTextFieldState,
63     val readOnly: Boolean,
64     val enabled: Boolean,
65     val isPassword: Boolean,
66     val offsetMapping: OffsetMapping,
67     val manager: TextFieldSelectionManager,
68     val imeOptions: ImeOptions,
69     val focusRequester: FocusRequester
70 ) : ModifierNodeElement<CoreTextFieldSemanticsModifierNode>() {
71     override fun create(): CoreTextFieldSemanticsModifierNode =
72         CoreTextFieldSemanticsModifierNode(
73             transformedText = transformedText,
74             value = value,
75             state = state,
76             readOnly = readOnly,
77             enabled = enabled,
78             isPassword = isPassword,
79             offsetMapping = offsetMapping,
80             manager = manager,
81             imeOptions = imeOptions,
82             focusRequester = focusRequester
83         )
84 
85     override fun update(node: CoreTextFieldSemanticsModifierNode) {
86         node.updateNodeSemantics(
87             transformedText = transformedText,
88             value = value,
89             state = state,
90             readOnly = readOnly,
91             enabled = enabled,
92             isPassword = isPassword,
93             offsetMapping = offsetMapping,
94             manager = manager,
95             imeOptions = imeOptions,
96             focusRequester = focusRequester
97         )
98     }
99 
100     override fun InspectorInfo.inspectableProperties() {
101         // Show nothing in the inspector.
102     }
103 }
104 
105 internal class CoreTextFieldSemanticsModifierNode(
106     var transformedText: TransformedText,
107     var value: TextFieldValue,
108     var state: LegacyTextFieldState,
109     var readOnly: Boolean,
110     var enabled: Boolean,
111     var isPassword: Boolean,
112     var offsetMapping: OffsetMapping,
113     var manager: TextFieldSelectionManager,
114     var imeOptions: ImeOptions,
115     var focusRequester: FocusRequester
116 ) : DelegatingNode(), SemanticsModifierNode {
117     init {
<lambda>null118         manager.requestAutofillAction = { requestAutofill() }
119     }
120 
121     override val shouldMergeDescendantSemantics: Boolean
122         get() = true
123 
applySemanticsnull124     override fun SemanticsPropertyReceiver.applySemantics() {
125         this.inputText = value.annotatedString
126         this.editableText = transformedText.text
127         this.textSelectionRange = value.selection
128 
129         // The developer will set `contentType`. CTF populates the other autofill-related
130         // semantics. And since we're in a TextField, set the `contentDataType` to be "Text".
131         this.contentDataType = ContentDataType.Text
132         onAutofillText { text ->
133             state.justAutofilled = true
134             state.autofillHighlightOn = true
135             handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
136             true
137         }
138 
139         if (!enabled) this.disabled()
140         if (isPassword) this.password()
141         val editable = enabled && !readOnly
142         isEditable = editable
143         getTextLayoutResult {
144             if (state.layoutResult != null) {
145                 it.add(state.layoutResult!!.value)
146                 true
147             } else {
148                 false
149             }
150         }
151 
152         if (editable) {
153             setText { text ->
154                 handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
155                 true
156             }
157 
158             insertTextAtCursor { text ->
159                 if (readOnly || !enabled) return@insertTextAtCursor false
160 
161                 // If the action is performed while in an active text editing session, treat
162                 // this like an IME command and update the text by going through the buffer.
163                 // This keeps the buffer state consistent if other IME commands are performed
164                 // before the next recomposition, and is used for the testing code path.
165                 state.inputSession?.let { session ->
166                     TextFieldDelegate.onEditCommand(
167                         // Finish composing text first because when the field is focused the IME
168                         // might
169                         // set composition.
170                         ops = listOf(FinishComposingTextCommand(), CommitTextCommand(text, 1)),
171                         editProcessor = state.processor,
172                         state.onValueChange,
173                         session
174                     )
175                 }
176                     ?: run {
177                         val newText =
178                             value.text.replaceRange(
179                                 value.selection.start,
180                                 value.selection.end,
181                                 text
182                             )
183                         val newCursor = TextRange(value.selection.start + text.length)
184                         state.onValueChange(TextFieldValue(newText, newCursor))
185                     }
186                 true
187             }
188         }
189 
190         setSelection { selectionStart, selectionEnd, relativeToOriginalText ->
191             // in traversal mode we get selection from the `textSelectionRange` semantics which
192             // is selection in original text. In non-traversal mode selection comes from the
193             // Talkback and indices are relative to the transformed text
194             val start =
195                 if (relativeToOriginalText) {
196                     selectionStart
197                 } else {
198                     offsetMapping.transformedToOriginal(selectionStart)
199                 }
200             val end =
201                 if (relativeToOriginalText) {
202                     selectionEnd
203                 } else {
204                     offsetMapping.transformedToOriginal(selectionEnd)
205                 }
206 
207             if (!enabled) {
208                 false
209             } else if (start == value.selection.start && end == value.selection.end) {
210                 false
211             } else if (
212                 minOf(start, end) >= 0 && maxOf(start, end) <= value.annotatedString.length
213             ) {
214                 // Do not show toolbar if it's a traversal mode (with the volume keys), or
215                 // if the cursor just moved to beginning or end.
216                 if (relativeToOriginalText || start == end) {
217                     manager.exitSelectionMode()
218                 } else {
219                     manager.enterSelectionMode()
220                 }
221                 state.onValueChange(TextFieldValue(value.annotatedString, TextRange(start, end)))
222                 true
223             } else {
224                 manager.exitSelectionMode()
225                 false
226             }
227         }
228         onImeAction(imeOptions.imeAction) {
229             // This will perform the appropriate default action if no handler has been
230             // specified, so
231             // as far as the platform is concerned, we always handle the action and never want
232             // to
233             // defer to the default _platform_ implementation.
234             state.onImeActionPerformed(imeOptions.imeAction)
235             true
236         }
237         onClick {
238             // according to the documentation, we still need to provide proper semantics actions
239             // even if the state is 'disabled'
240             tapToFocus(state, focusRequester, !readOnly)
241             true
242         }
243         onLongClick {
244             manager.enterSelectionMode()
245             true
246         }
247         if (!value.selection.collapsed && !isPassword) {
248             copyText {
249                 manager.copy()
250                 true
251             }
252             if (enabled && !readOnly) {
253                 cutText {
254                     manager.cut()
255                     true
256                 }
257             }
258         }
259         if (enabled && !readOnly) {
260             pasteText {
261                 manager.paste()
262                 true
263             }
264         }
265     }
266 
updateNodeSemanticsnull267     fun updateNodeSemantics(
268         transformedText: TransformedText,
269         value: TextFieldValue,
270         state: LegacyTextFieldState,
271         readOnly: Boolean,
272         enabled: Boolean,
273         isPassword: Boolean,
274         offsetMapping: OffsetMapping,
275         manager: TextFieldSelectionManager,
276         imeOptions: ImeOptions,
277         focusRequester: FocusRequester
278     ) {
279         // Find the diff: current previous and new values before updating current.
280         val previousEditable = this.enabled && !this.readOnly
281         val previousEnabled = this.enabled
282         val previousIsPassword = this.isPassword
283         val previousImeOptions = this.imeOptions
284         val previousManager = this.manager
285         val editable = enabled && !readOnly
286 
287         // Apply the diff.
288         this.transformedText = transformedText
289         this.value = value
290         this.state = state
291         this.readOnly = readOnly
292         this.enabled = enabled
293         this.offsetMapping = offsetMapping
294         this.manager = manager
295         this.imeOptions = imeOptions
296         this.focusRequester = focusRequester
297 
298         if (
299             enabled != previousEnabled ||
300                 editable != previousEditable ||
301                 imeOptions != previousImeOptions ||
302                 isPassword != previousIsPassword ||
303                 !value.selection.collapsed
304         ) {
305             invalidateSemantics()
306         }
307 
308         if (manager != previousManager) {
309             manager.requestAutofillAction = { requestAutofill() }
310         }
311     }
312 
313     /**
314      * In an active input session, semantics updates are handled just as user updates coming from
315      * the IME. Otherwise the updates are directly applied on the current state.
316      */
handleTextUpdateFromSemanticsnull317     private fun handleTextUpdateFromSemantics(
318         state: LegacyTextFieldState,
319         text: String,
320         readOnly: Boolean,
321         enabled: Boolean
322     ) {
323         if (readOnly || !enabled) return
324 
325         // If the action is performed while in an active text editing session, treat this
326         // like an IME command and update the text by going through the buffer.
327         state.inputSession?.let { session ->
328             TextFieldDelegate.onEditCommand(
329                 ops = listOf(DeleteAllCommand(), CommitTextCommand(text, 1)),
330                 editProcessor = state.processor,
331                 state.onValueChange,
332                 session
333             )
334         } ?: run { state.onValueChange(TextFieldValue(text, TextRange(text.length))) }
335     }
336 }
337