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

commonMain.androidx.compose.foundation.text.selection.SelectionHandles.kt Maven / Gradle / Ivy

/*
 * Copyright 2021 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.text.Handle
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.takeOrElse
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.text.style.ResolvedTextDirection
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.compose.ui.window.PopupPositionProvider

internal val HandleWidth = 25.dp
internal val HandleHeight = 25.dp

/**
 * [SelectionHandleInfo]s for the nodes representing selection handles. These nodes are in popup
 * windows, and will respond to drag gestures.
 */
internal val SelectionHandleInfoKey =
    SemanticsPropertyKey("SelectionHandleInfo")

/**
 * Information about a single selection handle popup.
 *
 * @param handle Which selection [Handle] this is about.
 * @param position The position that the handle is anchored to relative to the selectable content.
 * This position is not necessarily the position of the popup itself, it's the position that the
 * handle "points" to (so e.g. top-middle for [Handle.Cursor]).
 * @param anchor How the selection handle is anchored to its position
 * @param visible Whether the icon of the handle is actually shown
 */
internal data class SelectionHandleInfo(
    val handle: Handle,
    val position: Offset,
    val anchor: SelectionHandleAnchor,
    val visible: Boolean,
)

/**
 * How the selection handle is anchored to its position
 *
 * In a regular text selection, selection start is anchored to left.
 * Only cursor handle is always anchored at the middle.
 * In a regular text selection, selection end is anchored to right.
 */
internal enum class SelectionHandleAnchor {
    Left,
    Middle,
    Right
}

@Composable
internal expect fun SelectionHandle(
    offsetProvider: OffsetProvider,
    isStartHandle: Boolean,
    direction: ResolvedTextDirection,
    handlesCrossed: Boolean,
    minTouchTargetSize: DpSize = DpSize.Unspecified,
    lineHeight: Float,
    modifier: Modifier,
)

/**
 * Avoids boxing of [Offset] which is an inline value class.
 */
internal fun interface OffsetProvider {
    fun provide(): Offset
}

/**
 * Adjust coordinates for given text offset.
 *
 * Currently [android.text.Layout.getLineBottom] returns y coordinates of the next
 * line's top offset, which is not included in current line's hit area. To be able to
 * hit current line, move up this y coordinates by 1 pixel.
 */
internal fun getAdjustedCoordinates(position: Offset): Offset {
    return Offset(position.x, position.y - 1f)
}

/**
 * This [PopupPositionProvider] for a selection handle. It will position the selection handle
 * to the result of [positionProvider] in its anchor layout.
 */
internal class HandlePositionProvider(
    private val handleReferencePoint: Alignment,
    private val positionProvider: OffsetProvider,
) : PopupPositionProvider {

    /**
     * When Handle disappears, it starts reporting its position as [Offset.Unspecified]. Normally,
     * Popup is dismissed immediately when its position becomes unspecified, but for one frame a
     * position update might be requested by soon-to-be-destroyed Popup. In this case, report the
     * last known position as there are no more updates. If the first ever position is provided as
     * unspecified, start with [Offset.Zero] default.
     */
    private var prevPosition: Offset = Offset.Zero

    override fun calculatePosition(
        anchorBounds: IntRect,
        windowSize: IntSize,
        layoutDirection: LayoutDirection,
        popupContentSize: IntSize
    ): IntOffset {
        val position = positionProvider.provide().takeOrElse { prevPosition }
        prevPosition = position

        val adjustment = handleReferencePoint.align(popupContentSize, IntSize.Zero, layoutDirection)
        return anchorBounds.topLeft + position.round() + adjustment
    }
}

/**
 * Computes whether the handle's appearance should be left-pointing or right-pointing.
 */
internal fun isLeftSelectionHandle(
    isStartHandle: Boolean,
    direction: ResolvedTextDirection,
    handlesCrossed: Boolean
): Boolean {
    return if (isStartHandle) {
        isHandleLtrDirection(direction, handlesCrossed)
    } else {
        !isHandleLtrDirection(direction, handlesCrossed)
    }
}

/**
 * This method is to check if the selection handles should use the natural Ltr pointing
 * direction.
 * If the context is Ltr and the handles are not crossed, or if the context is Rtl and the handles
 * are crossed, return true.
 *
 * In Ltr context, the start handle should point to the left, and the end handle should point to
 * the right. However, in Rtl context or when handles are crossed, the start handle should point to
 * the right, and the end handle should point to left.
 */
/*@VisibleForTesting*/
internal fun isHandleLtrDirection(
    direction: ResolvedTextDirection,
    areHandlesCrossed: Boolean
): Boolean {
    return direction == ResolvedTextDirection.Ltr && !areHandlesCrossed ||
        direction == ResolvedTextDirection.Rtl && areHandlesCrossed
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy