All Downloads are FREE. Search and download functionalities are using the official Maven repository.

skikoMain.androidx.compose.foundation.text.CupertionTextFieldPointerModifier.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.getTextFieldSelection
import androidx.compose.foundation.text.selection.isSelectionHandleInVisibleBound
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

@Composable
internal fun Modifier.cupertinoTextFieldPointer(
    manager: TextFieldSelectionManager,
    enabled: Boolean,
    interactionSource: MutableInteractionSource?,
    state: TextFieldState,
    focusRequester: FocusRequester,
    readOnly: Boolean,
    offsetMapping: OffsetMapping
): Modifier = if (enabled) {
    if (isInTouchMode) {
        val selectionModifier = getSelectionModifier(manager)
        val tapHandlerModifier = getTapHandlerModifier(
            interactionSource,
            state,
            focusRequester,
            readOnly,
            offsetMapping,
            manager
        )
        this
            .then(tapHandlerModifier)
            .then(selectionModifier)
            .pointerHoverIcon(textPointerIcon)
    } else {
        this
            .mouseDragGestureDetector(
                observer = manager.mouseSelectionObserver,
                enabled = enabled
            )
            .pointerHoverIcon(textPointerIcon)
    }
} else {
    this
}

@Composable
@OptIn(InternalFoundationTextApi::class)
private fun getTapHandlerModifier(
    interactionSource: MutableInteractionSource?,
    state: TextFieldState,
    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(
            onTap = { touchPointOffset ->
                tapTextFieldToFocus(
                    currentState,
                    currentFocusRequester,
                    !currentReadOnly
                )
                if (currentState.hasFocus) {
                    if (currentState.handleState != HandleState.Selection) {
                        currentState.layoutResult?.let { layoutResult ->
                            TextFieldDelegate.setCursorOffset(
                                touchPointOffset,
                                layoutResult,
                                currentState.processor,
                                currentOffsetMapping,
                                currentState.onValueChange
                            )
                            // Won't enter cursor state when text is empty.
                            if (currentState.textDelegate.text.isNotEmpty()) {
                                currentState.handleState = HandleState.Cursor
                            }
                        }
                    } else {
                        currentManager.deselect(touchPointOffset)
                    }
                }
            },
            onDoubleTap = {
                currentManager.doRepeatingTapSelection(it, SelectionAdjustment.Word)
            },
            onTripleTap = {
                currentManager.doRepeatingTapSelection(it, SelectionAdjustment.Paragraph)
            }
        )
    }
}

@Composable
private fun getSelectionModifier(manager: TextFieldSelectionManager): Modifier {
    val currentManager by rememberUpdatedState(manager)
    return Modifier.pointerInput(Unit) {
        detectDragGesturesAfterLongPress(
            onDragStart = {
                currentManager.touchSelectionObserver.onStart(
                    startPoint = it
                )
            },
            onDrag = { _, delta -> currentManager.touchSelectionObserver.onDrag(delta = delta) },
            onDragCancel = { currentManager.touchSelectionObserver.onCancel() },
            onDragEnd = { currentManager.touchSelectionObserver.onStop() }
        )
    }
}

private fun TextFieldSelectionManager.doRepeatingTapSelection(touchPointOffset: Offset, selectionAdjustment: SelectionAdjustment) {
    if (value.text.isEmpty()) return
    enterSelectionMode()
    state?.layoutResult?.let { layoutResult ->
        val offset = layoutResult.getOffsetForPosition(touchPointOffset)
        updateSelection(
            value = value,
            transformedStartOffset = offset,
            transformedEndOffset = offset,
            isStartHandle = false,
            adjustment = selectionAdjustment
        )
    }
}

/**
 * Copied from TextFieldSelectionManager.kt
 */
private fun TextFieldSelectionManager.updateSelection(
    value: TextFieldValue,
    transformedStartOffset: Int,
    transformedEndOffset: Int,
    isStartHandle: Boolean,
    adjustment: SelectionAdjustment
) {
    val transformedSelection = TextRange(
        offsetMapping.originalToTransformed(value.selection.start),
        offsetMapping.originalToTransformed(value.selection.end)
    )

    val newTransformedSelection = getTextFieldSelection(
        textLayoutResult = state?.layoutResult?.value,
        rawStartOffset = transformedStartOffset,
        rawEndOffset = transformedEndOffset,
        previousSelection = if (transformedSelection.collapsed) null else transformedSelection,
        isStartHandle = isStartHandle,
        adjustment = adjustment
    )

    val originalSelection = TextRange(
        start = offsetMapping.transformedToOriginal(newTransformedSelection.start),
        end = offsetMapping.transformedToOriginal(newTransformedSelection.end)
    )

    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