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