Merge "Rename NavRecord to NavEntry" into androidx-main
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
index 78ac040..7f8b8f7 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.text.input
+import android.R
import android.os.Build
import android.text.InputType
import android.text.SpannableStringBuilder
@@ -40,7 +41,10 @@
import androidx.compose.foundation.text.TEST_FONT_FAMILY
import androidx.compose.foundation.text.computeSizeForDefaultText
import androidx.compose.foundation.text.input.TextFieldBuffer.ChangeList
+import androidx.compose.foundation.text.input.internal.TextLayoutState
+import androidx.compose.foundation.text.input.internal.TransformedTextFieldState
import androidx.compose.foundation.text.input.internal.selection.FakeClipboard
+import androidx.compose.foundation.text.input.internal.selection.TextFieldSelectionState
import androidx.compose.foundation.text.input.internal.setComposingRegion
import androidx.compose.foundation.text.selection.fetchTextLayoutResult
import androidx.compose.foundation.verticalScroll
@@ -127,6 +131,9 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
@OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
@LargeTest
@@ -1130,9 +1137,7 @@
requestFocus(Tag)
- inputMethodInterceptor.withInputConnection {
- performContextMenuAction(android.R.id.selectAll)
- }
+ inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.selectAll) }
rule.runOnIdle {
assertThat(state.selection).isEqualTo(TextRange(0, 5))
@@ -1152,7 +1157,7 @@
requestFocus(Tag)
- inputMethodInterceptor.withInputConnection { performContextMenuAction(android.R.id.cut) }
+ inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.cut) }
rule.waitForIdle()
assertThat(clipboard.getClipEntry()?.readText()).isEqualTo("He")
@@ -1171,7 +1176,7 @@
requestFocus(Tag)
- inputMethodInterceptor.withInputConnection { performContextMenuAction(android.R.id.copy) }
+ inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.copy) }
rule.waitForIdle()
assertThat(clipboard.getClipEntry()?.readText()).isEqualTo("He")
@@ -1189,7 +1194,7 @@
requestFocus(Tag)
- inputMethodInterceptor.withInputConnection { performContextMenuAction(android.R.id.paste) }
+ inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.paste) }
rule.runOnIdle {
assertThat(state.text.toString()).isEqualTo("Worldo")
@@ -1299,6 +1304,34 @@
}
@Test
+ fun textField_state_invokesAutofill() {
+ val mockLambda: () -> Unit = mock()
+ var density by mutableStateOf(Density(1f))
+
+ val manager =
+ TextFieldSelectionState(
+ // other parameters not necessary to test autofill invocation
+ textFieldState =
+ TransformedTextFieldState(
+ textFieldState = TextFieldState(),
+ inputTransformation = null,
+ codepointTransformation = null,
+ outputTransformation = null
+ ),
+ textLayoutState = TextLayoutState(),
+ density = density,
+ enabled = true,
+ readOnly = false,
+ isFocused = false,
+ isPassword = false
+ )
+ .apply { requestAutofillAction = mockLambda }
+
+ manager.autofill()
+ verify(mockLambda, times(1)).invoke()
+ }
+
+ @Test
fun changingInputTransformation_doesNotRestartInput() {
var inputTransformation by mutableStateOf(InputTransformation.maxLength(10))
inputMethodInterceptor.setTextFieldTestContent {
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
index fb28556..9d8e33e 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
@@ -23,7 +23,6 @@
import androidx.compose.foundation.text.LegacyTextFieldState
import androidx.compose.foundation.text.TextDelegate
import androidx.compose.foundation.text.TextLayoutResultProxy
-import androidx.compose.ui.autofill.AutofillManager
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -98,7 +97,6 @@
private val hapticFeedback = mock<HapticFeedback>()
private val focusRequester = mock<FocusRequester>()
private val multiParagraph = mock<MultiParagraph>()
- private val autofillManager = mock<AutofillManager>()
@Before
fun setup() {
@@ -110,7 +108,6 @@
manager.textToolbar = textToolbar
manager.hapticFeedBack = hapticFeedback
manager.focusRequester = focusRequester
- manager.autofillManager = autofillManager
manager.coroutineScope = null
whenever(layoutResult.layoutInput)
@@ -370,10 +367,12 @@
@Test
fun autofill_selection_collapse() {
manager.value = TextFieldValue(text = text, selection = TextRange(4, 4))
+ val mockLambda: () -> Unit = mock()
+ val manager = TextFieldSelectionManager().apply { requestAutofillAction = mockLambda }
manager.autofill()
- verify(autofillManager, times(1)).requestAutofillForActiveElement()
+ verify(mockLambda, times(1)).invoke()
assertThat(state.handleState).isEqualTo(HandleState.None)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
index 925ecfd..7e63192 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
@@ -68,7 +68,6 @@
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalAutofillManager
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
@@ -301,7 +300,6 @@
val currentHapticFeedback = LocalHapticFeedback.current
val currentClipboard = LocalClipboard.current
val currentTextToolbar = LocalTextToolbar.current
- val autofillManager = LocalAutofillManager.current
val textToolbarHandler =
remember(coroutineScope, currentTextToolbar) {
@@ -360,7 +358,6 @@
enabled = enabled,
readOnly = readOnly,
isPassword = isPassword,
- autofillManager = autofillManager,
showTextToolbar = textToolbarHandler
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 14f8402..494f392 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -27,6 +27,7 @@
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.text.handwriting.stylusHandwriting
+import androidx.compose.foundation.text.input.internal.CoreTextFieldSemanticsModifier
import androidx.compose.foundation.text.input.internal.createLegacyPlatformTextInputServiceAdapter
import androidx.compose.foundation.text.input.internal.legacyTextInputAdapter
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
@@ -58,7 +59,6 @@
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.ContentDataType
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusRequester
@@ -82,7 +82,6 @@
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalAutofillManager
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
@@ -92,33 +91,13 @@
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.platform.SoftwareKeyboardController
-import androidx.compose.ui.semantics.contentDataType
-import androidx.compose.ui.semantics.copyText
-import androidx.compose.ui.semantics.cutText
-import androidx.compose.ui.semantics.disabled
-import androidx.compose.ui.semantics.editableText
-import androidx.compose.ui.semantics.getTextLayoutResult
-import androidx.compose.ui.semantics.insertTextAtCursor
-import androidx.compose.ui.semantics.isEditable
-import androidx.compose.ui.semantics.onAutofillText
-import androidx.compose.ui.semantics.onClick
-import androidx.compose.ui.semantics.onImeAction
-import androidx.compose.ui.semantics.onLongClick
-import androidx.compose.ui.semantics.password
-import androidx.compose.ui.semantics.pasteText
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.setSelection
-import androidx.compose.ui.semantics.setText
-import androidx.compose.ui.semantics.textSelectionRange
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.input.CommitTextCommand
-import androidx.compose.ui.text.input.DeleteAllCommand
import androidx.compose.ui.text.input.EditProcessor
-import androidx.compose.ui.text.input.FinishComposingTextCommand
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeOptions
import androidx.compose.ui.text.input.KeyboardType
@@ -316,7 +295,6 @@
manager.coroutineScope = coroutineScope
manager.textToolbar = LocalTextToolbar.current
manager.hapticFeedBack = LocalHapticFeedback.current
- manager.autofillManager = LocalAutofillManager.current
manager.focusRequester = focusRequester
manager.editable = !readOnly
manager.enabled = enabled
@@ -474,151 +452,18 @@
val isPassword = visualTransformation is PasswordVisualTransformation
val semanticsModifier =
- Modifier.semantics(true) {
- // focused semantics are handled by Modifier.focusable()
- this.editableText = transformedText.text
- this.textSelectionRange = value.selection
-
- // The developer will set `contentType`. CTF populates the other autofill-related
- // semantics. And since we're in a TextField, set the `contentDataType` to be "Text".
- this.contentDataType = ContentDataType.Text
- onAutofillText { text ->
- state.justAutofilled = true
- state.autofillHighlightOn = true
- handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
- true
- }
-
- if (!enabled) this.disabled()
- if (isPassword) this.password()
- val editable = enabled && !readOnly
- isEditable = editable
- getTextLayoutResult {
- if (state.layoutResult != null) {
- it.add(state.layoutResult!!.value)
- true
- } else {
- false
- }
- }
- if (editable) {
- setText { text ->
- handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
- true
- }
-
- insertTextAtCursor { text ->
- if (readOnly || !enabled) return@insertTextAtCursor false
-
- // If the action is performed while in an active text editing session, treat
- // this like an IME command and update the text by going through the buffer.
- // This keeps the buffer state consistent if other IME commands are performed
- // before the next recomposition, and is used for the testing code path.
- state.inputSession?.let { session ->
- TextFieldDelegate.onEditCommand(
- // Finish composing text first because when the field is focused the IME
- // might
- // set composition.
- ops = listOf(FinishComposingTextCommand(), CommitTextCommand(text, 1)),
- editProcessor = state.processor,
- state.onValueChange,
- session
- )
- }
- ?: run {
- val newText =
- value.text.replaceRange(
- value.selection.start,
- value.selection.end,
- text
- )
- val newCursor = TextRange(value.selection.start + text.length)
- state.onValueChange(TextFieldValue(newText, newCursor))
- }
- true
- }
- }
-
- setSelection { selectionStart, selectionEnd, relativeToOriginalText ->
- // in traversal mode we get selection from the `textSelectionRange` semantics which
- // is
- // selection in original text. In non-traversal mode selection comes from the
- // Talkback
- // and indices are relative to the transformed text
- val start =
- if (relativeToOriginalText) {
- selectionStart
- } else {
- offsetMapping.transformedToOriginal(selectionStart)
- }
- val end =
- if (relativeToOriginalText) {
- selectionEnd
- } else {
- offsetMapping.transformedToOriginal(selectionEnd)
- }
-
- if (!enabled) {
- false
- } else if (start == value.selection.start && end == value.selection.end) {
- false
- } else if (
- minOf(start, end) >= 0 && maxOf(start, end) <= value.annotatedString.length
- ) {
- // Do not show toolbar if it's a traversal mode (with the volume keys), or
- // if the cursor just moved to beginning or end.
- if (relativeToOriginalText || start == end) {
- manager.exitSelectionMode()
- } else {
- manager.enterSelectionMode()
- }
- state.onValueChange(
- TextFieldValue(value.annotatedString, TextRange(start, end))
- )
- true
- } else {
- manager.exitSelectionMode()
- false
- }
- }
- onImeAction(imeOptions.imeAction) {
- // This will perform the appropriate default action if no handler has been
- // specified, so
- // as far as the platform is concerned, we always handle the action and never want
- // to
- // defer to the default _platform_ implementation.
- state.onImeActionPerformed(imeOptions.imeAction)
- true
- }
- onClick {
- // according to the documentation, we still need to provide proper semantics actions
- // even if the state is 'disabled'
- tapToFocus(state, focusRequester, !readOnly)
- true
- }
- onLongClick {
- manager.enterSelectionMode()
- true
- }
- if (!value.selection.collapsed && !isPassword) {
- copyText {
- manager.copy()
- true
- }
- if (enabled && !readOnly) {
- cutText {
- manager.cut()
- true
- }
- }
- }
- if (enabled && !readOnly) {
- pasteText {
- manager.paste()
- true
- }
- }
- }
+ CoreTextFieldSemanticsModifier(
+ transformedText,
+ value,
+ state,
+ readOnly,
+ enabled,
+ isPassword,
+ offsetMapping,
+ manager,
+ imeOptions,
+ focusRequester
+ )
val showCursor = enabled && !readOnly && windowInfo.isWindowFocused && !state.hasHighlight()
val cursorModifier = Modifier.cursor(state, value, offsetMapping, cursorBrush, showCursor)
@@ -884,30 +729,6 @@
}
}
-/**
- * In an active input session, semantics updates are handled just as user updates coming from the
- * IME. Otherwise the updates are directly applied on the current state.
- */
-private fun handleTextUpdateFromSemantics(
- state: LegacyTextFieldState,
- text: String,
- readOnly: Boolean,
- enabled: Boolean
-) {
- if (readOnly || !enabled) return
-
- // If the action is performed while in an active text editing session, treat this
- // like an IME command and update the text by going through the buffer.
- state.inputSession?.let { session ->
- TextFieldDelegate.onEditCommand(
- ops = listOf(DeleteAllCommand(), CommitTextCommand(text, 1)),
- editProcessor = state.processor,
- state.onValueChange,
- session
- )
- } ?: run { state.onValueChange(TextFieldValue(text, TextRange(text.length))) }
-}
-
internal class LegacyTextFieldState(
var textDelegate: TextDelegate,
val recomposeScope: RecomposeScope,
@@ -1108,7 +929,7 @@
}
/** Request focus on tap. If already focused, makes sure the keyboard is requested. */
-private fun tapToFocus(
+internal fun tapToFocus(
state: LegacyTextFieldState,
focusRequester: FocusRequester,
allowKeyboard: Boolean
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/CoreTextFieldSemanticsModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/CoreTextFieldSemanticsModifier.kt
new file mode 100644
index 0000000..a5fe111
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/CoreTextFieldSemanticsModifier.kt
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.input.internal
+
+import androidx.compose.foundation.text.LegacyTextFieldState
+import androidx.compose.foundation.text.TextFieldDelegate
+import androidx.compose.foundation.text.selection.TextFieldSelectionManager
+import androidx.compose.foundation.text.tapToFocus
+import androidx.compose.ui.autofill.ContentDataType
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.node.invalidateSemantics
+import androidx.compose.ui.node.requestAutofill
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.contentDataType
+import androidx.compose.ui.semantics.copyText
+import androidx.compose.ui.semantics.cutText
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.editableText
+import androidx.compose.ui.semantics.getTextLayoutResult
+import androidx.compose.ui.semantics.insertTextAtCursor
+import androidx.compose.ui.semantics.isEditable
+import androidx.compose.ui.semantics.onAutofillText
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.onImeAction
+import androidx.compose.ui.semantics.onLongClick
+import androidx.compose.ui.semantics.password
+import androidx.compose.ui.semantics.pasteText
+import androidx.compose.ui.semantics.setSelection
+import androidx.compose.ui.semantics.setText
+import androidx.compose.ui.semantics.textSelectionRange
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.CommitTextCommand
+import androidx.compose.ui.text.input.DeleteAllCommand
+import androidx.compose.ui.text.input.FinishComposingTextCommand
+import androidx.compose.ui.text.input.ImeOptions
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.TransformedText
+
+internal data class CoreTextFieldSemanticsModifier(
+ val transformedText: TransformedText,
+ val value: TextFieldValue,
+ val state: LegacyTextFieldState,
+ val readOnly: Boolean,
+ val enabled: Boolean,
+ val isPassword: Boolean,
+ val offsetMapping: OffsetMapping,
+ val manager: TextFieldSelectionManager,
+ val imeOptions: ImeOptions,
+ val focusRequester: FocusRequester
+) : ModifierNodeElement<CoreTextFieldSemanticsModifierNode>() {
+ override fun create(): CoreTextFieldSemanticsModifierNode =
+ CoreTextFieldSemanticsModifierNode(
+ transformedText = transformedText,
+ value = value,
+ state = state,
+ readOnly = readOnly,
+ enabled = enabled,
+ isPassword = isPassword,
+ offsetMapping = offsetMapping,
+ manager = manager,
+ imeOptions = imeOptions,
+ focusRequester = focusRequester
+ )
+
+ override fun update(node: CoreTextFieldSemanticsModifierNode) {
+ node.updateNodeSemantics(
+ transformedText = transformedText,
+ value = value,
+ state = state,
+ readOnly = readOnly,
+ enabled = enabled,
+ isPassword = isPassword,
+ offsetMapping = offsetMapping,
+ manager = manager,
+ imeOptions = imeOptions,
+ focusRequester = focusRequester
+ )
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ // Show nothing in the inspector.
+ }
+}
+
+internal class CoreTextFieldSemanticsModifierNode(
+ var transformedText: TransformedText,
+ var value: TextFieldValue,
+ var state: LegacyTextFieldState,
+ var readOnly: Boolean,
+ var enabled: Boolean,
+ var isPassword: Boolean,
+ var offsetMapping: OffsetMapping,
+ var manager: TextFieldSelectionManager,
+ var imeOptions: ImeOptions,
+ var focusRequester: FocusRequester
+) : DelegatingNode(), SemanticsModifierNode {
+ init {
+ manager.requestAutofillAction = { requestAutofill() }
+ }
+
+ override val shouldMergeDescendantSemantics: Boolean
+ get() = true
+
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ this.editableText = transformedText.text
+ this.textSelectionRange = value.selection
+
+ // The developer will set `contentType`. CTF populates the other autofill-related
+ // semantics. And since we're in a TextField, set the `contentDataType` to be "Text".
+ this.contentDataType = ContentDataType.Text
+ onAutofillText { text ->
+ state.justAutofilled = true
+ state.autofillHighlightOn = true
+ handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
+ true
+ }
+
+ if (!enabled) this.disabled()
+ if (isPassword) this.password()
+ val editable = enabled && !readOnly
+ isEditable = editable
+ getTextLayoutResult {
+ if (state.layoutResult != null) {
+ it.add(state.layoutResult!!.value)
+ true
+ } else {
+ false
+ }
+ }
+
+ if (editable) {
+ setText { text ->
+ handleTextUpdateFromSemantics(state, text.text, readOnly, enabled)
+ true
+ }
+
+ insertTextAtCursor { text ->
+ if (readOnly || !enabled) return@insertTextAtCursor false
+
+ // If the action is performed while in an active text editing session, treat
+ // this like an IME command and update the text by going through the buffer.
+ // This keeps the buffer state consistent if other IME commands are performed
+ // before the next recomposition, and is used for the testing code path.
+ state.inputSession?.let { session ->
+ TextFieldDelegate.onEditCommand(
+ // Finish composing text first because when the field is focused the IME
+ // might
+ // set composition.
+ ops = listOf(FinishComposingTextCommand(), CommitTextCommand(text, 1)),
+ editProcessor = state.processor,
+ state.onValueChange,
+ session
+ )
+ }
+ ?: run {
+ val newText =
+ value.text.replaceRange(
+ value.selection.start,
+ value.selection.end,
+ text
+ )
+ val newCursor = TextRange(value.selection.start + text.length)
+ state.onValueChange(TextFieldValue(newText, newCursor))
+ }
+ true
+ }
+ }
+
+ setSelection { selectionStart, selectionEnd, relativeToOriginalText ->
+ // in traversal mode we get selection from the `textSelectionRange` semantics which
+ // is selection in original text. In non-traversal mode selection comes from the
+ // Talkback and indices are relative to the transformed text
+ val start =
+ if (relativeToOriginalText) {
+ selectionStart
+ } else {
+ offsetMapping.transformedToOriginal(selectionStart)
+ }
+ val end =
+ if (relativeToOriginalText) {
+ selectionEnd
+ } else {
+ offsetMapping.transformedToOriginal(selectionEnd)
+ }
+
+ if (!enabled) {
+ false
+ } else if (start == value.selection.start && end == value.selection.end) {
+ false
+ } else if (
+ minOf(start, end) >= 0 && maxOf(start, end) <= value.annotatedString.length
+ ) {
+ // Do not show toolbar if it's a traversal mode (with the volume keys), or
+ // if the cursor just moved to beginning or end.
+ if (relativeToOriginalText || start == end) {
+ manager.exitSelectionMode()
+ } else {
+ manager.enterSelectionMode()
+ }
+ state.onValueChange(TextFieldValue(value.annotatedString, TextRange(start, end)))
+ true
+ } else {
+ manager.exitSelectionMode()
+ false
+ }
+ }
+ onImeAction(imeOptions.imeAction) {
+ // This will perform the appropriate default action if no handler has been
+ // specified, so
+ // as far as the platform is concerned, we always handle the action and never want
+ // to
+ // defer to the default _platform_ implementation.
+ state.onImeActionPerformed(imeOptions.imeAction)
+ true
+ }
+ onClick {
+ // according to the documentation, we still need to provide proper semantics actions
+ // even if the state is 'disabled'
+ tapToFocus(state, focusRequester, !readOnly)
+ true
+ }
+ onLongClick {
+ manager.enterSelectionMode()
+ true
+ }
+ if (!value.selection.collapsed && !isPassword) {
+ copyText {
+ manager.copy()
+ true
+ }
+ if (enabled && !readOnly) {
+ cutText {
+ manager.cut()
+ true
+ }
+ }
+ }
+ if (enabled && !readOnly) {
+ pasteText {
+ manager.paste()
+ true
+ }
+ }
+ }
+
+ fun updateNodeSemantics(
+ transformedText: TransformedText,
+ value: TextFieldValue,
+ state: LegacyTextFieldState,
+ readOnly: Boolean,
+ enabled: Boolean,
+ isPassword: Boolean,
+ offsetMapping: OffsetMapping,
+ manager: TextFieldSelectionManager,
+ imeOptions: ImeOptions,
+ focusRequester: FocusRequester
+ ) {
+ // Find the diff: current previous and new values before updating current.
+ val previousEditable = this.enabled && !this.readOnly
+ val previousEnabled = this.enabled
+ val previousIsPassword = this.isPassword
+ val previousImeOptions = this.imeOptions
+ val previousManager = this.manager
+ val editable = enabled && !readOnly
+
+ // Apply the diff.
+ this.transformedText = transformedText
+ this.value = value
+ this.state = state
+ this.readOnly = readOnly
+ this.enabled = enabled
+ this.offsetMapping = offsetMapping
+ this.manager = manager
+ this.imeOptions = imeOptions
+ this.focusRequester = focusRequester
+
+ if (
+ enabled != previousEnabled ||
+ editable != previousEditable ||
+ imeOptions != previousImeOptions ||
+ isPassword != previousIsPassword ||
+ !value.selection.collapsed
+ ) {
+ invalidateSemantics()
+ }
+
+ if (manager != previousManager) {
+ manager.requestAutofillAction = { requestAutofill() }
+ }
+ }
+
+ /**
+ * In an active input session, semantics updates are handled just as user updates coming from
+ * the IME. Otherwise the updates are directly applied on the current state.
+ */
+ private fun handleTextUpdateFromSemantics(
+ state: LegacyTextFieldState,
+ text: String,
+ readOnly: Boolean,
+ enabled: Boolean
+ ) {
+ if (readOnly || !enabled) return
+
+ // If the action is performed while in an active text editing session, treat this
+ // like an IME command and update the text by going through the buffer.
+ state.inputSession?.let { session ->
+ TextFieldDelegate.onEditCommand(
+ ops = listOf(DeleteAllCommand(), CommitTextCommand(text, 1)),
+ editProcessor = state.processor,
+ state.onValueChange,
+ session
+ )
+ } ?: run { state.onValueChange(TextFieldValue(text, TextRange(text.length))) }
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
index c9eb589..d7810f9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
@@ -66,6 +66,7 @@
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.node.invalidateSemantics
import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.node.requestAutofill
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@@ -202,8 +203,9 @@
ObserverModifierNode,
LayoutAwareModifierNode {
- private val editable
- get() = enabled && !readOnly
+ init {
+ textFieldSelectionState.requestAutofillAction = { requestAutofill() }
+ }
private val pointerInputNode =
delegate(
@@ -430,9 +432,7 @@
stylusHandwritingTrigger: MutableSharedFlow<Unit>?
) {
// Find the diff: current previous and new values before updating current.
- val previousEditable = this.editable
- val editable = enabled && !readOnly
-
+ val previousEditable = this.enabled && !this.readOnly
val previousEnabled = this.enabled
val previousTextFieldState = this.textFieldState
val previousKeyboardOptions = this.keyboardOptions
@@ -440,6 +440,7 @@
val previousInteractionSource = this.interactionSource
val previousIsPassword = this.isPassword
val previousStylusHandwritingTrigger = this.stylusHandwritingTrigger
+ val editable = enabled && !readOnly
// Apply the diff.
this.textFieldState = textFieldState
@@ -487,6 +488,7 @@
textFieldSelectionState.receiveContentConfiguration =
receiveContentConfigurationProvider
}
+ textFieldSelectionState.requestAutofillAction = { requestAutofill() }
}
if (interactionSource != previousInteractionSource) {
@@ -507,7 +509,8 @@
if (!enabled) disabled()
if (isPassword) password()
- isEditable = [email protected]
+ val editable = enabled && !readOnly
+ isEditable = editable
// The developer will set `contentType`. TF populates the other autofill-related
// semantics. And since we're in a TextField, set the `contentDataType` to be "Text".
@@ -630,6 +633,7 @@
isElementFocused = focusState.isFocused
onFocusChange()
+ val editable = enabled && !readOnly
if (focusState.isFocused) {
// Deselect when losing focus even if readonly.
if (editable) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
index 5baa1a8..0a6c258 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
@@ -66,7 +66,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.autofill.AutofillManager
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.isSpecified
@@ -115,9 +114,6 @@
var isFocused: Boolean,
private var isPassword: Boolean,
) {
- /** [AutofillManager] to perform Autofill. */
- private var autofillManager: AutofillManager? = null
-
/** [HapticFeedback] handle to perform haptic feedback. */
private var hapticFeedBack: HapticFeedback? = null
@@ -130,6 +126,9 @@
/** Whether user is interacting with the UI in touch mode. */
var isInTouchMode: Boolean by mutableStateOf(true)
+ /** The action to invoke when autofill is requested in text toolbar. */
+ var requestAutofillAction: (() -> Unit)? = null
+
/**
* Reduced [ReceiveContentConfiguration] from the attached modifier node hierarchy. This value
* is set by [TextFieldDecoratorModifierNode].
@@ -353,8 +352,7 @@
density: Density,
enabled: Boolean,
readOnly: Boolean,
- isPassword: Boolean,
- autofillManager: AutofillManager?
+ isPassword: Boolean
) {
if (!enabled) {
hideTextToolbar()
@@ -366,7 +364,6 @@
this.enabled = enabled
this.readOnly = readOnly
this.isPassword = isPassword
- this.autofillManager = autofillManager
}
/** Implements the complete set of gestures supported by the cursor handle. */
@@ -443,7 +440,6 @@
clipboard = null
hapticFeedBack = null
- autofillManager = null
}
/**
@@ -1395,7 +1391,7 @@
* Inserts credentials (if there exist any that match this field type) into the text field.
*/
fun autofill() {
- autofillManager?.requestAutofillForActiveElement()
+ requestAutofillAction?.invoke()
}
fun deselect() {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
index 674b87a..6f75364 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
@@ -38,7 +38,6 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.autofill.AutofillManager
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -100,8 +99,8 @@
*/
internal var visualTransformation: VisualTransformation = VisualTransformation.None
- /** [AutofillManager] to perform clipboard features. */
- internal var autofillManager: AutofillManager? = null
+ /** The action to invoke when autofill is requested in text toolbar. */
+ internal var requestAutofillAction: (() -> Unit)? = null
/** [Clipboard] to perform clipboard features. */
internal var clipboard: Clipboard? = null
@@ -705,7 +704,7 @@
}
internal fun autofill() {
- autofillManager?.requestAutofillForActiveElement()
+ requestAutofillAction?.invoke()
}
internal fun getHandlePosition(isStartHandle: Boolean): Offset {
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 5a884ad..2b88ca8 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -228,7 +228,6 @@
public abstract class AutofillManager {
method public abstract void cancel();
method public abstract void commit();
- method public abstract void requestAutofillForActiveElement();
}
public final class AutofillNode {
@@ -2860,6 +2859,7 @@
public final class DelegatableNodeKt {
method public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
+ method public static void requestAutofill(androidx.compose.ui.node.DelegatableNode);
method public static androidx.compose.ui.unit.Density requireDensity(androidx.compose.ui.node.DelegatableNode);
method public static androidx.compose.ui.graphics.GraphicsContext requireGraphicsContext(androidx.compose.ui.node.DelegatableNode);
method public static androidx.compose.ui.layout.LayoutCoordinates requireLayoutCoordinates(androidx.compose.ui.node.DelegatableNode);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 2c0d712..8b114f0 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -228,7 +228,6 @@
public abstract class AutofillManager {
method public abstract void cancel();
method public abstract void commit();
- method public abstract void requestAutofillForActiveElement();
}
public final class AutofillNode {
@@ -2914,6 +2913,7 @@
public final class DelegatableNodeKt {
method public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
+ method public static void requestAutofill(androidx.compose.ui.node.DelegatableNode);
method public static androidx.compose.ui.unit.Density requireDensity(androidx.compose.ui.node.DelegatableNode);
method public static androidx.compose.ui.graphics.GraphicsContext requireGraphicsContext(androidx.compose.ui.node.DelegatableNode);
method public static androidx.compose.ui.layout.LayoutCoordinates requireLayoutCoordinates(androidx.compose.ui.node.DelegatableNode);
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
index d6ce34a..25dcd50 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/autofill/AndroidAutofillManagerTest.kt
@@ -42,11 +42,14 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusManager
-import androidx.compose.ui.focus.focusProperties
+import androidx.compose.ui.node.SemanticsModifierNode
+import androidx.compose.ui.node.elementOf
+import androidx.compose.ui.node.requestAutofill
import androidx.compose.ui.platform.LocalAutofillManager
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.contentDataType
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.contentType
@@ -713,68 +716,6 @@
@Test
@SmallTest
- @SdkSuppress(minSdkVersion = 26)
- fun autofillManager_requestAutofillAfterFocus() {
- val am: PlatformAutofillManager = mock()
- val contextMenuTag = "menu_tag"
- var autofillManager: AutofillManager?
-
- rule.setContent {
- autofillManager = LocalAutofillManager.current
- (autofillManager as AndroidAutofillManager).platformAutofillManager = am
- Box(
- modifier =
- Modifier.semantics {
- testTag = contextMenuTag
- onAutofillText { true }
- }
- .focusProperties { canFocus = true }
- .clickable { autofillManager?.requestAutofillForActiveElement() }
- .size(height, width)
- )
- }
-
- // `requestAutofill` is always called after an element is focused
- rule.onNodeWithTag(contextMenuTag).requestFocus()
- rule.runOnIdle { verify(am).notifyViewEntered(any(), any(), any()) }
-
- // then `requestAutofill` is called on that same previously focused element
- rule.onNodeWithTag(contextMenuTag).performClick()
- rule.runOnIdle { verify(am).requestAutofill(any(), any(), any()) }
- }
-
- @Test
- @SmallTest
- @SdkSuppress(minSdkVersion = 26)
- fun autofillManager_notAutofillable_doesNotrequestAutofillAfterFocus() {
- val am: PlatformAutofillManager = mock()
- val contextMenuTag = "menu_tag"
- var autofillManager: AutofillManager?
-
- rule.setContent {
- autofillManager = LocalAutofillManager.current
- (autofillManager as AndroidAutofillManager).platformAutofillManager = am
- Box(
- modifier =
- Modifier.semantics { testTag = contextMenuTag }
- .focusProperties { canFocus = true }
- .clickable { autofillManager?.requestAutofillForActiveElement() }
- .size(height, width)
- )
- }
- clearInvocations(am)
-
- // `requestAutofill` is always called after an element is focused
- rule.onNodeWithTag(contextMenuTag).requestFocus()
- rule.runOnIdle { verifyZeroInteractions(am) }
-
- // then `requestAutofill` is called on that same previously focused element
- rule.onNodeWithTag(contextMenuTag).performClick()
- rule.runOnIdle { verifyNoMoreInteractions(am) }
- }
-
- @Test
- @SmallTest
fun autofillManager_lazyColumnScroll_callsCommit() {
lateinit var state: LazyListState
lateinit var coroutineScope: CoroutineScope
@@ -866,4 +807,35 @@
// A column disappearing will call commit
rule.runOnIdle { verify(am).commit() }
}
+
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 26)
+ fun autofillManager_requestAutofill() {
+ val am: PlatformAutofillManager = mock()
+ val semanticsModifier = TestSemanticsModifier { testTag = "TestTag" }
+ var autofillManager: AutofillManager?
+
+ rule.setContent {
+ autofillManager = LocalAutofillManager.current
+ (autofillManager as AndroidAutofillManager).platformAutofillManager = am
+ Box(Modifier.elementOf(semanticsModifier))
+ }
+
+ // Act
+ rule.runOnIdle { semanticsModifier.requestAutofill() }
+
+ // Assert
+ rule.runOnIdle { verify(am).requestAutofill(any(), any(), any()) }
+ }
+
+ private class TestSemanticsModifier(
+ private val onApplySemantics: SemanticsPropertyReceiver.() -> Unit
+ ) : SemanticsModifierNode, Modifier.Node() {
+
+ override fun SemanticsPropertyReceiver.applySemantics() {
+ contentType = ContentType.Username
+ onApplySemantics.invoke(this)
+ }
+ }
}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index 84314e3e..1d90f0b 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -3510,6 +3510,10 @@
override fun requestFocus(): Boolean = false
+ override fun requestAutofill(node: LayoutNode) {
+ TODO("Not yet implemented")
+ }
+
override fun measureAndLayout(sendPointerUpdate: Boolean) {}
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 3f9150a..ffb3fec 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -2846,6 +2846,10 @@
override fun requestFocus(): Boolean = false
+ override fun requestAutofill(node: LayoutNode) {
+ TODO("Not yet implemented")
+ }
+
override val rootForTest: RootForTest
get() = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
index 76aa7ce..4322a1c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
@@ -281,6 +281,10 @@
override fun requestFocus() = TODO("Not yet implemented")
+ override fun requestAutofill(node: LayoutNode) {
+ TODO("Not yet implemented")
+ }
+
override fun onSemanticsChange() {}
override fun getFocusDirection(keyEvent: KeyEvent) = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
index a36713d..5d5e649 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
@@ -521,6 +521,10 @@
override fun requestFocus(): Boolean = false
+ override fun requestAutofill(node: LayoutNode) {
+ TODO("Not yet implemented")
+ }
+
override fun measureAndLayout(sendPointerUpdate: Boolean) {}
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
index 69f31cc..4d49938 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
@@ -77,19 +77,6 @@
platformAutofillManager.cancel()
}
- // This will be used to request autofill when
- // `AutofillManager.requestAutofillForActiveElement()` is called (e.g. from the text toolbar).
- private var previouslyFocusedId = -1
-
- override fun requestAutofillForActiveElement() {
- if (previouslyFocusedId <= 0) return
-
- rectManager.rects.withRect(previouslyFocusedId) { left, top, right, bottom ->
- reusableRect.set(left, top, right, bottom)
- platformAutofillManager.requestAutofill(view, previouslyFocusedId, reusableRect)
- }
- }
-
override fun onFocusChanged(
previous: FocusTargetModifierNode?,
current: FocusTargetModifierNode?
@@ -105,7 +92,6 @@
rectManager.rects.withRect(semanticsId) { l, t, r, b ->
platformAutofillManager.notifyViewEntered(view, semanticsId, Rect(l, t, r, b))
}
- previouslyFocusedId = semanticsId
}
}
}
@@ -138,7 +124,6 @@
val previousFocus = prevConfig?.getOrNull(SemanticsProperties.Focused)
val currFocus = config?.getOrNull(SemanticsProperties.Focused)
if (previousFocus != true && currFocus == true && config.isAutofillable()) {
- previouslyFocusedId = semanticsId
rectManager.rects.withRect(semanticsId) { l, t, r, b ->
platformAutofillManager.notifyViewEntered(view, semanticsId, Rect(l, t, r, b))
}
@@ -236,6 +221,13 @@
private var currentlyDisplayedIDs = MutableIntSet()
private var pendingChangesToDisplayedIds = false
+ internal fun requestAutofill(semanticsInfo: SemanticsInfo) {
+ rectManager.rects.withRect(semanticsInfo.semanticsId) { left, top, right, bottom ->
+ reusableRect.set(left, top, right, bottom)
+ platformAutofillManager.requestAutofill(view, semanticsInfo.semanticsId, reusableRect)
+ }
+ }
+
internal fun onPostAttach(semanticsInfo: SemanticsInfo) {
if (semanticsInfo.semanticsConfiguration?.isRelatedToAutoCommit() == true) {
currentlyDisplayedIDs.add(semanticsInfo.semanticsId)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index c059f5d..7da948c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -1247,6 +1247,13 @@
}
}
+ override fun requestAutofill(node: LayoutNode) {
+ @OptIn(ExperimentalComposeUiApi::class)
+ if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
+ _autofillManager?.requestAutofill(node)
+ }
+ }
+
fun requestClearInvalidObservations() {
observationClearRequested = true
}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index f13b1f1..b88e67e 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -2448,6 +2448,10 @@
override fun requestFocus(): Boolean = false
+ override fun requestAutofill(node: LayoutNode) {
+ TODO("Not yet implemented")
+ }
+
override fun measureAndLayout(sendPointerUpdate: Boolean) {}
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {}
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
index 4fdd4ea..de1b8d6 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
@@ -446,6 +446,10 @@
override fun requestFocus() = TODO("Not yet implemented")
+ override fun requestAutofill(node: LayoutNode) {
+ TODO("Not yet implemented")
+ }
+
override fun measureAndLayout(sendPointerUpdate: Boolean) = TODO("Not yet implemented")
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt
index 2a6163b..f5ebdc7 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/autofill/AutofillManager.kt
@@ -42,14 +42,4 @@
* without processing any information entered in the autofillable field.
*/
abstract fun cancel()
-
- /**
- * Request autofill for previously focused element.
- *
- * This may have no effect, and it is not required that any autofill service will be notified.
- *
- * Any component that can be autofilled may call this when it is active to request an autofill
- * services response.
- */
- abstract fun requestAutofillForActiveElement()
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index 08505aa..8b45c3f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -332,6 +332,13 @@
checkPreconditionNotNull(requireLayoutNode().owner) { "This node does not have an owner." }
/**
+ * Requests autofill for the LayoutNode that this [DelegatableNode] is attached to. If the node does
+ * not have any autofill semantic properties set, then the request still may be sent to the Autofill
+ * service, but no response is expected.
+ */
+fun DelegatableNode.requestAutofill() = requireLayoutNode().requestAutofill()
+
+/**
* Returns the current [Density] of the LayoutNode that this [DelegatableNode] is attached to. If
* the node is not attached, this function will throw an [IllegalStateException].
*/
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 5f60779..d751280 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -402,6 +402,14 @@
private var isSemanticsInvalidated = false
+ internal fun requestAutofill() {
+ // Ignore calls while semantics are being applied (b/378114177).
+ if (isCurrentlyCalculatingSemanticsConfiguration) return
+
+ val owner = requireOwner()
+ owner.requestAutofill(this)
+ }
+
internal fun invalidateSemantics() {
// Ignore calls to invalidate Semantics while semantics are being applied (b/378114177).
if (isCurrentlyCalculatingSemanticsConfiguration) return
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index a75bf87..ee7342a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -245,6 +245,9 @@
*/
fun requestFocus(): Boolean
+ /** Ask the system to request autofill values to this owner. */
+ fun requestAutofill(node: LayoutNode)
+
/**
* Iterates through all LayoutNodes that have requested layout and measures and lays them out.
* If [sendPointerUpdate] is `true` then a simulated PointerEvent may be sent to update pointer