
skikoMain.androidx.compose.foundation.text.CupertinoTextFieldPointerModifier.skiko.kt Maven / Gradle / Ivy
/*
* Copyright 2023 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.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.detectRepeatingTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.text.selection.SelectionAdjustment
import androidx.compose.foundation.text.selection.TextFieldSelectionManager
import androidx.compose.foundation.text.selection.getTextFieldSelectionLayout
import androidx.compose.foundation.text.selection.isSelectionHandleInVisibleBound
import androidx.compose.foundation.text.selection.selectionGestureInput
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
@Composable
internal fun Modifier.cupertinoTextFieldPointer(
manager: TextFieldSelectionManager,
enabled: Boolean,
interactionSource: MutableInteractionSource?,
state: LegacyTextFieldState,
focusRequester: FocusRequester,
readOnly: Boolean,
offsetMapping: OffsetMapping
): Modifier = if (enabled) {
// TODO switch to ".updateSelectionTouchMode { state.isInTouchMode = it }" as in defaultTextFieldPointer
if (isInTouchMode) {
val longPressHandlerModifier = getLongPressHandlerModifier(state, offsetMapping, manager)
val tapHandlerModifier = getTapHandlerModifier(
interactionSource,
state,
focusRequester,
readOnly,
offsetMapping,
manager
)
this
.then(tapHandlerModifier)
.then(longPressHandlerModifier)
.pointerHoverIcon(textPointerIcon)
} else {
this
.selectionGestureInput(
mouseSelectionObserver = manager.mouseSelectionObserver,
textDragObserver = manager.touchSelectionObserver,
)
.pointerHoverIcon(textPointerIcon)
}
} else {
this
}
@Composable
@OptIn(InternalFoundationTextApi::class)
private fun getTapHandlerModifier(
interactionSource: MutableInteractionSource?,
state: LegacyTextFieldState,
focusRequester: FocusRequester,
readOnly: Boolean,
offsetMapping: OffsetMapping,
manager: TextFieldSelectionManager
): Modifier {
val currentState by rememberUpdatedState(state)
val currentFocusRequester by rememberUpdatedState(focusRequester)
val currentReadOnly by rememberUpdatedState(readOnly)
val currentOffsetMapping by rememberUpdatedState(offsetMapping)
val currentManager by rememberUpdatedState(manager)
/*
We need to move tap recognizer here from selection modifier (as it is in common) because:
1) we need to handle triple tap
2) without rewriting, we have onDoubleTap call and onTap call, and onDoubleTap will execute
before onTap.
*/
return Modifier.pointerInput(interactionSource) {
detectRepeatingTapGestures(
onTapRelease = { touchPointOffset ->
if (currentState.hasFocus) {
// To show keyboard if it was hidden. Even in selection mode (like native)
requestFocusAndShowKeyboardIfNeeded(
currentState,
currentFocusRequester,
!currentReadOnly
)
if (currentState.handleState != HandleState.Selection) {
currentState.layoutResult?.let { layoutResult ->
// TODO: Research native behavior with any text transformations (which adds symbols like with using NSNumberFormatter)
if (currentManager.visualTransformation != VisualTransformation.None) {
TextFieldDelegate.setCursorOffset(
touchPointOffset,
layoutResult,
currentState.processor,
currentOffsetMapping,
currentState.onValueChange
)
} else {
TextFieldDelegate.cupertinoSetCursorOffsetFocused(
position = touchPointOffset,
textLayoutResult = layoutResult,
editProcessor = currentState.processor,
offsetMapping = currentOffsetMapping,
showContextMenu = { show ->
// it shouldn't be selection, but this is a way to call a context menu in BasicTextField
if (show) { currentManager.enterSelectionMode() } else { currentManager.exitSelectionMode() }
},
onValueChange = currentState.onValueChange
)
}
}
} else {
currentManager.deselect(touchPointOffset)
}
} else {
requestFocusAndShowKeyboardIfNeeded(
currentState,
currentFocusRequester,
!currentReadOnly
)
currentState.layoutResult?.let { layoutResult ->
TextFieldDelegate.setCursorOffset(
touchPointOffset,
layoutResult,
currentState.processor,
currentOffsetMapping,
currentState.onValueChange
)
}
}
if (currentState.textDelegate.text.isNotEmpty()) {
currentState.handleState = HandleState.Cursor
}
},
onDoubleTapPress = {
currentManager.doRepeatingTapSelection(it, SelectionAdjustment.Word)
},
onTripleTapPress = {
currentManager.doRepeatingTapSelection(it, SelectionAdjustment.Paragraph)
}
)
}
}
/**
* Returns a modifier which allows to precisely move the caret in the text by drag gesture after long press
*
* @param state The state of the text field.
* @param offsetMapping The offset mapping of the text field.
* @return A modifier that handles long press and drag gestures.
*/
@Composable
private fun getLongPressHandlerModifier(
state: LegacyTextFieldState,
offsetMapping: OffsetMapping,
manager: TextFieldSelectionManager,
): Modifier {
val currentState by rememberUpdatedState(state)
val currentOffsetMapping by rememberUpdatedState(offsetMapping)
val currentManager by rememberUpdatedState(manager)
return Modifier.pointerInput(Unit) {
val longTapActionsObserver =
object : TextDragObserver {
var dragTotalDistance = Offset.Zero
var dragBeginOffset = Offset.Zero
override fun onStart(startPoint: Offset) {
currentManager.draggingHandle = Handle.SelectionEnd
currentManager.currentDragPosition = startPoint
currentState.layoutResult?.let { layoutResult ->
TextFieldDelegate.setCursorOffset(
startPoint,
layoutResult,
currentState.processor,
currentOffsetMapping,
currentState.onValueChange
)
dragBeginOffset = startPoint
}
dragTotalDistance = Offset.Zero
}
override fun onDrag(delta: Offset) {
dragTotalDistance += delta
currentState.layoutResult?.let { layoutResult ->
val currentDragPosition = dragBeginOffset + dragTotalDistance
currentManager.currentDragPosition = currentDragPosition
TextFieldDelegate.setCursorOffset(
currentDragPosition,
layoutResult,
currentState.processor,
currentOffsetMapping,
currentState.onValueChange
)
}
}
// Unnecessary here
override fun onDown(point: Offset) {}
override fun onUp() {}
override fun onStop() {
currentManager.draggingHandle = null
currentManager.currentDragPosition = null
}
override fun onCancel() {
currentManager.draggingHandle = null
currentManager.currentDragPosition = null
}
}
detectDragGesturesAfterLongPress(
onDragStart = { longTapActionsObserver.onStart(it) },
onDrag = { _, delta -> longTapActionsObserver.onDrag(delta = delta) },
onDragCancel = { longTapActionsObserver.onCancel() },
onDragEnd = { longTapActionsObserver.onStop() }
)
}
}
private fun TextFieldSelectionManager.doRepeatingTapSelection(
touchPointOffset: Offset,
selectionAdjustment: SelectionAdjustment
) {
if (value.text.isEmpty()) return
enterSelectionMode()
state?.layoutResult?.let { layoutResult ->
updateSelection(
value = value,
currentPosition = touchPointOffset,
isStartOfSelection = true,
isStartHandle = false,
adjustment = selectionAdjustment
)
}
}
/**
* Copied from TextFieldSelectionManager.kt
*/
private fun TextFieldSelectionManager.updateSelection(
value: TextFieldValue,
currentPosition: Offset,
isStartOfSelection: Boolean,
isStartHandle: Boolean,
adjustment: SelectionAdjustment
) {
val layoutResult = state?.layoutResult ?: return
val previousTransformedSelection = TextRange(
offsetMapping.originalToTransformed(value.selection.start),
offsetMapping.originalToTransformed(value.selection.end)
)
val currentOffset = layoutResult.getOffsetForPosition(
position = currentPosition,
coerceInVisibleBounds = false
)
val rawStartHandleOffset = if (isStartHandle || isStartOfSelection) currentOffset else
previousTransformedSelection.start
val rawEndHandleOffset = if (!isStartHandle || isStartOfSelection) currentOffset else
previousTransformedSelection.end
val previousSelectionLayout = previousSelectionLayout // for smart cast
val rawPreviousHandleOffset = if (
isStartOfSelection ||
previousSelectionLayout == null ||
previousRawDragOffset == -1
) {
-1
} else {
previousRawDragOffset
}
val selectionLayout = getTextFieldSelectionLayout(
layoutResult = layoutResult.value,
rawStartHandleOffset = rawStartHandleOffset,
rawEndHandleOffset = rawEndHandleOffset,
rawPreviousHandleOffset = rawPreviousHandleOffset,
previousSelectionRange = previousTransformedSelection,
isStartOfSelection = isStartOfSelection,
isStartHandle = isStartHandle,
)
if (!selectionLayout.shouldRecomputeSelection(previousSelectionLayout)) {
return
}
this.previousSelectionLayout = selectionLayout
previousRawDragOffset = currentOffset
val newTransformedSelection = adjustment.adjust(selectionLayout)
val originalSelection = TextRange(
start = offsetMapping.transformedToOriginal(newTransformedSelection.start.offset),
end = offsetMapping.transformedToOriginal(newTransformedSelection.end.offset)
)
if (originalSelection == value.selection) return
hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
val newValue = createTextFieldValue(
annotatedString = value.annotatedString,
selection = originalSelection
)
onValueChange(newValue)
// showSelectionHandleStart/End might be set to false when scrolled out of the view.
// When the selection is updated, they must also be updated so that handles will be shown
// or hidden correctly.
state?.showSelectionHandleStart = isSelectionHandleInVisibleBound(true)
state?.showSelectionHandleEnd = isSelectionHandleInVisibleBound(false)
}
/**
* Copied from TextFieldSelectionManager.kt
*/
private fun createTextFieldValue(
annotatedString: AnnotatedString,
selection: TextRange
): TextFieldValue {
return TextFieldValue(
annotatedString = annotatedString,
selection = selection
)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy