TextLayoutResultProxy.kt

/*
 * 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

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.text.TextLayoutResult

internal class TextLayoutResultProxy(val value: TextLayoutResult) {
    // TextLayoutResult methods
    fun getOffsetForPosition(position: Offset): Int {
        val shiftedOffset = shiftedOffset(position)
        return value.getOffsetForPosition(shiftedOffset)
    }

    fun getLineForVerticalPosition(vertical: Float): Int {
        val shiftedVertical = shiftedOffset(Offset(0f, vertical)).y
        return value.getLineForVerticalPosition(shiftedVertical)
    }

    fun getLineEnd(lineIndex: Int, visibleEnd: Boolean = false): Int =
        value.getLineEnd(lineIndex, visibleEnd)

    /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
     * in the view. Returns false when the position is in the empty space of left/right of text.
     */
    fun isPositionOnText(offset: Offset): Boolean {
        val shiftedOffset = shiftedOffset(offset)
        val line = value.getLineForVerticalPosition(shiftedOffset.y)
        return shiftedOffset.x >= value.getLineLeft(line) &&
            shiftedOffset.x <= value.getLineRight(line)
    }

    // Shift offset
    /** Measured bounds of the decoration box and inner text field. Together used to
     * calculate the relative touch offset. Because touches are applied on the decoration box, we
     * need to translate it to the inner text field coordinates.
     */
    var innerTextFieldCoordinates: LayoutCoordinates? = null
    var decorationBoxCoordinates: LayoutCoordinates? = null

    private fun shiftedOffset(offset: Offset): Offset {
        // If offset is outside visible bounds of the inner text field, use visible bounds edges
        val visibleInnerTextFieldRect = innerTextFieldCoordinates?.let { inner ->
            decorationBoxCoordinates?.localBoundingBoxOf(inner)
        } ?: Rect.Zero
        val coercedOffset = offset.coerceIn(visibleInnerTextFieldRect)

        // Translates touch to the inner text field coordinates
        return innerTextFieldCoordinates?.let { innerTextFieldCoordinates ->
            decorationBoxCoordinates?.let { decorationBoxCoordinates ->
                innerTextFieldCoordinates.localPositionOf(decorationBoxCoordinates, coercedOffset)
            }
        } ?: coercedOffset
    }
}

private fun Offset.coerceIn(rect: Rect): Offset {
    val xOffset = when {
        x < rect.left -> rect.left
        x > rect.right -> rect.right
        else -> x
    }
    val yOffset = when {
        y < rect.top -> rect.top
        y > rect.bottom -> rect.bottom
        else -> y
    }
    return Offset(xOffset, yOffset)
}