diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/TextFieldPointerModifier.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/TextFieldPointerModifier.android.kt new file mode 100644 index 0000000000000..76ef964f73e08 --- /dev/null +++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/TextFieldPointerModifier.android.kt @@ -0,0 +1,44 @@ +/* + * 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 + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.text.selection.TextFieldSelectionManager +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.text.input.OffsetMapping + +@Composable +internal actual fun Modifier.textFieldPointer( + manager: TextFieldSelectionManager, + enabled: Boolean, + interactionSource: MutableInteractionSource?, + state: LegacyTextFieldState, + focusRequester: FocusRequester, + readOnly: Boolean, + offsetMapping: OffsetMapping, +): Modifier = + defaultTextFieldPointer( + manager, + enabled, + interactionSource, + state, + focusRequester, + readOnly, + offsetMapping, + ) diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.android.kt new file mode 100644 index 0000000000000..bc678024c1020 --- /dev/null +++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.android.kt @@ -0,0 +1,30 @@ +/* + * 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.selection + +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Color + +/** Default color used is the blue from the Compose logo, b/172679845 for context */ +private val DefaultSelectionColor = Color(0xFF4286F4) + +@Stable +internal actual val DefaultTextSelectionColors = + TextSelectionColors( + handleColor = DefaultSelectionColor, + backgroundColor = DefaultSelectionColor.copy(alpha = 0.4f), + ) 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 282cc08fa50ce..2f596f721eb57 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 @@ -42,11 +42,9 @@ import androidx.compose.foundation.text.selection.SimpleLayout import androidx.compose.foundation.text.selection.TextFieldSelectionHandle import androidx.compose.foundation.text.selection.TextFieldSelectionManager import androidx.compose.foundation.text.selection.addBasicTextFieldTextContextMenuComponents -import androidx.compose.foundation.text.selection.awaitSelectionGestures import androidx.compose.foundation.text.selection.isSelectionHandleInVisibleBound import androidx.compose.foundation.text.selection.rememberPlatformSelectionBehaviors import androidx.compose.foundation.text.selection.textFieldMagnifier -import androidx.compose.foundation.text.selection.updateSelectionTouchMode import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DontMemoize @@ -75,8 +73,6 @@ import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.compose.ui.input.pointer.PointerIcon -import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.FirstBaseline import androidx.compose.ui.layout.IntrinsicMeasurable @@ -387,36 +383,15 @@ internal fun CoreTextField( } val pointerModifier = - Modifier.updateSelectionTouchMode { state.isInTouchMode = it } - .tapPressTextFieldModifier(interactionSource, enabled) { offset -> - tapToFocus(state, focusRequester, !readOnly) - if (state.hasFocus && enabled) { - if (state.handleState != HandleState.Selection) { - state.layoutResult?.let { layoutResult -> - TextFieldDelegate.setCursorOffset( - offset, - layoutResult, - state.processor, - offsetMapping, - state.onValueChange, - ) - // Won't enter cursor state when text is empty. - if (state.textDelegate.text.isNotEmpty()) { - state.handleState = HandleState.Cursor - } - } - } else { - manager.deselect(offset) - } - } - } - .pointerInput(manager.mouseSelectionObserver, manager.touchSelectionObserver) { - awaitSelectionGestures( - manager.mouseSelectionObserver, - manager.touchSelectionObserver, - ) - } - .pointerHoverIcon(PointerIcon.Text) + Modifier.textFieldPointer( + manager, + enabled, + interactionSource, + state, + focusRequester, + readOnly, + offsetMapping, + ) val drawModifier = Modifier.drawBehind { @@ -965,7 +940,7 @@ internal class LegacyTextFieldState( } /** Request focus on tap. If already focused, makes sure the keyboard is requested. */ -internal fun tapToFocus( +internal fun requestFocusAndShowKeyboardIfNeeded( state: LegacyTextFieldState, focusRequester: FocusRequester, allowKeyboard: Boolean, diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPointerModifier.common.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPointerModifier.common.kt new file mode 100644 index 0000000000000..22b40861707b5 --- /dev/null +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPointerModifier.common.kt @@ -0,0 +1,84 @@ +/* + * 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 + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.text.selection.TextFieldSelectionManager +import androidx.compose.foundation.text.selection.awaitSelectionGestures +import androidx.compose.foundation.text.selection.updateSelectionTouchMode +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.input.OffsetMapping + +/* + * Marked @Composable to preserve state across recompositions and deliver updated parameters + * to the pointer handling logic without restarting in-progress gestures. + * Platform-specific actuals adapt gestures to native platform behavior, which differs from Compose behavior. + * A non-composable signature would require recreating pointer input when parameters change, + * which cancels active gestures. + */ +@Composable +internal expect fun Modifier.textFieldPointer( + manager: TextFieldSelectionManager, + enabled: Boolean, + interactionSource: MutableInteractionSource?, + state: LegacyTextFieldState, + focusRequester: FocusRequester, + readOnly: Boolean, + offsetMapping: OffsetMapping, +): Modifier + +internal fun Modifier.defaultTextFieldPointer( + manager: TextFieldSelectionManager, + enabled: Boolean, + interactionSource: MutableInteractionSource?, + state: LegacyTextFieldState, + focusRequester: FocusRequester, + readOnly: Boolean, + offsetMapping: OffsetMapping, +) = + this.updateSelectionTouchMode { state.isInTouchMode = it } + .tapPressTextFieldModifier(interactionSource, enabled) { offset -> + requestFocusAndShowKeyboardIfNeeded(state, focusRequester, !readOnly) + if (state.hasFocus && enabled) { + if (state.handleState != HandleState.Selection) { + state.layoutResult?.let { layoutResult -> + TextFieldDelegate.setCursorOffset( + offset, + layoutResult, + state.processor, + offsetMapping, + state.onValueChange, + ) + // Won't enter cursor state when text is empty. + if (state.textDelegate.text.isNotEmpty()) { + state.handleState = HandleState.Cursor + } + } + } else { + manager.deselect(offset) + } + } + } + .pointerInput(manager.mouseSelectionObserver, manager.touchSelectionObserver) { + awaitSelectionGestures(manager.mouseSelectionObserver, manager.touchSelectionObserver) + } + .pointerHoverIcon(PointerIcon.Text) 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 index a504788a9910e..a9ace83dd389b 100644 --- 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 @@ -18,8 +18,8 @@ package androidx.compose.foundation.text.input.internal import androidx.compose.foundation.text.LegacyTextFieldState import androidx.compose.foundation.text.TextFieldDelegate +import androidx.compose.foundation.text.requestFocusAndShowKeyboardIfNeeded import androidx.compose.foundation.text.selection.TextFieldSelectionManager -import androidx.compose.foundation.text.tapToFocus import androidx.compose.ui.autofill.ContentDataType import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.autofill.FillableData @@ -262,7 +262,7 @@ internal class CoreTextFieldSemanticsModifierNode( onClick { // according to the documentation, we still need to provide proper semantics actions // even if the state is 'disabled' - tapToFocus(state, focusRequester, !readOnly) + requestFocusAndShowKeyboardIfNeeded(state, focusRequester, !readOnly) true } onLongClick { diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextSelectionColors.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextSelectionColors.kt index b8e136622590d..4a7b7751cb2b8 100644 --- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextSelectionColors.kt +++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextSelectionColors.kt @@ -63,12 +63,4 @@ class TextSelectionColors(val handleColor: Color, val backgroundColor: Color) { */ val LocalTextSelectionColors = compositionLocalOf { DefaultTextSelectionColors } -/** Default color used is the blue from the Compose logo, b/172679845 for context */ -private val DefaultSelectionColor = Color(0xFF4286F4) - -@Stable -private val DefaultTextSelectionColors = - TextSelectionColors( - handleColor = DefaultSelectionColor, - backgroundColor = DefaultSelectionColor.copy(alpha = 0.4f), - ) +@Stable internal expect val DefaultTextSelectionColors: TextSelectionColors diff --git a/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/TextFieldPointerModifier.commonStubs.kt b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/TextFieldPointerModifier.commonStubs.kt new file mode 100644 index 0000000000000..95def57b9eb30 --- /dev/null +++ b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/TextFieldPointerModifier.commonStubs.kt @@ -0,0 +1,36 @@ +/* + * 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 + +import androidx.compose.foundation.implementedInJetBrainsFork +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.text.selection.TextFieldSelectionManager +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.text.input.OffsetMapping + +@Composable +internal actual fun Modifier.textFieldPointer( + manager: TextFieldSelectionManager, + enabled: Boolean, + interactionSource: MutableInteractionSource?, + state: LegacyTextFieldState, + focusRequester: FocusRequester, + readOnly: Boolean, + offsetMapping: OffsetMapping, +): Modifier = implementedInJetBrainsFork() diff --git a/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.commonStubs.kt b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.commonStubs.kt new file mode 100644 index 0000000000000..41615b26cfef4 --- /dev/null +++ b/compose/foundation/foundation/src/commonStubsMain/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.commonStubs.kt @@ -0,0 +1,23 @@ +/* + * 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.selection + +import androidx.compose.foundation.implementedInJetBrainsFork +import androidx.compose.runtime.Stable + +@Stable +internal actual val DefaultTextSelectionColors: TextSelectionColors = implementedInJetBrainsFork()