SelectionMode.kt

/*
 * Copyright 2019 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.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect

/**
 * The enum class allows user to decide the selection mode.
 */
internal enum class SelectionMode {
    /**
     * When selection handles are dragged across composables, selection extends by row, for example,
     * when the end selection handle is dragged down, upper rows will be selected first, and the
     * lower rows.
     */
    Vertical {
        override fun compare(position: Offset, bounds: Rect): Int {
            if (bounds.contains(position)) return 0

            // When the position of the selection handle is on the top of the composable, and the
            // not on the right of the composable, it's considered as start.
            if (position.y < bounds.top) return -1

            // When the position of the selection handle is on the left of the composable, and not
            // below the bottom of composable, it's considered as start.
            if (position.x < bounds.left && position.y < bounds.bottom) return -1

            // In all other cases, the selection handle is considered as the end.
            return 1
        }
    },

    /**
     * When selection handles are dragged across composables, selection extends by column, for example,
     * when the end selection handle is dragged to the right, left columns will be selected first,
     * and the right rows.
     */
    Horizontal {
        override fun compare(position: Offset, bounds: Rect): Int {
            if (bounds.contains(position)) return 0

            // When the end of the selection is on the left of the composable, the composable is
            // outside of the selection range.
            if (position.x < bounds.left) return -1

            // When the end of the selection is on the top of the composable, and the not on the
            // right of the composable, the composable is outside of the selection range.
            if (position.y < bounds.top && position.x < bounds.right) return -1

            // In all other cases, the selection handle is considered as the end.
            return 1
        }
    };

    /**
     * A compare a selection handle with a  [Selectable] boundary. This defines whether an out of
     * boundary selection handle is treated as the start or the end of the Selectable. If the
     * [Selectable] is a text selectable, then the start is the index 0, and end corresponds to
     * the text length.
     *
     * @param position the position of the selection handle.
     * @param bounds the boundary of the [Selectable].
     * @return 0 if the selection handle [position] is within the [bounds]; a negative value if
     * the selection handle is considered as "start" of the [Selectable]; a positive value if the
     * selection handle is considered as the "end" of the [Selectable].
     */
    internal abstract fun compare(position: Offset, bounds: Rect): Int

    /**
     * Decides if Composable which has [bounds], should be accepted by the selection and
     * change its selected state for a selection that starts at [start] and ends at [end].
     *
     * @param bounds Composable bounds of the widget to be checked.
     * @param start The start coordinates of the selection, in SelectionContainer range.
     * @param end The end coordinates of the selection, in SelectionContainer range.
     */
    internal fun isSelected(
        bounds: Rect,
        start: Offset,
        end: Offset
    ): Boolean {
        // If either of the start or end is contained by bounds, the composable is selected.
        if (bounds.contains(start) || bounds.contains(end)) {
            return true
        }
        // Compare the location of start and end to the bound. If both are on the same side, return
        // false, otherwise return ture.
        val compareStart = compare(start, bounds)
        val compareEnd = compare(end, bounds)
        return (compareStart > 0) xor (compareEnd > 0)
    }
}