blob: 66c2d4fc0d22e66703e746c147f1e5afbff78890 [file] [log] [blame]
/*
* 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.text2.input
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.text2.input.internal.ChangeTracker
import androidx.compose.ui.text.TextRange
/**
* A [TextFieldBuffer] that also provides both read and write access to the current selection
* used by [TextEditFilter.filter].
*/
@ExperimentalFoundationApi
class TextFieldBufferWithSelection internal constructor(
value: TextFieldCharSequence,
/** The value reverted to when [revertAllChanges] is called. */
private val sourceValue: TextFieldCharSequence = TextFieldCharSequence(),
initialChanges: ChangeTracker? = null
) : TextFieldBuffer(value, initialChanges) {
/**
* True if the selection range has non-zero length. If this is false, then the selection
* represents the cursor.
*/
val hasSelection: Boolean
get() = !selectionInChars.collapsed
/**
* The selected range of characters.
*
* @see selectionInCodepoints
*/
var selectionInChars: TextRange = value.selectionInChars
private set
/**
* The selected range of codepoints.
*
* @see selectionInChars
*/
val selectionInCodepoints: TextRange
get() = charsToCodepoints(selectionInChars)
/**
* Places the cursor before the codepoint at the given index.
*
* If [index] is inside an invalid run, the cursor will be placed at the nearest earlier index.
*
* To place the cursor at the beginning of the field, pass index 0. To place the cursor at the
* end of the field, after the last character, pass index
* [TextFieldBuffer.codepointLength] or call [placeCursorAtEnd].
*
* @param index Codepoint index to place cursor before, should be in range 0 to
* [TextFieldBuffer.codepointLength], inclusive.
*
* @see placeCursorBeforeCharAt
* @see placeCursorAfterCodepointAt
*/
fun placeCursorBeforeCodepointAt(index: Int) {
requireValidIndex(index, inCodepoints = true)
val charIndex = codepointIndexToCharIndex(index)
selectionInChars = TextRange(charIndex)
}
/**
* Places the cursor before the character at the given index.
*
* If [index] is inside a surrogate pair or other invalid run, the cursor will be placed at the
* nearest earlier index.
*
* To place the cursor at the beginning of the field, pass index 0. To place the cursor at the
* end of the field, after the last character, pass index [TextFieldBuffer.length] or call
* [placeCursorAtEnd].
*
* @param index Character index to place cursor before, should be in range 0 to
* [TextFieldBuffer.length], inclusive.
*
* @see placeCursorBeforeCodepointAt
* @see placeCursorAfterCharAt
*/
fun placeCursorBeforeCharAt(index: Int) {
requireValidIndex(index, inCodepoints = false)
selectionInChars = TextRange(index)
}
/**
* Places the cursor after the codepoint at the given index.
*
* If [index] is inside an invalid run, the cursor will be placed at the nearest later index.
*
* To place the cursor at the end of the field, after the last character, pass index
* [MutableTextFieldValue.codepointLength] or call [placeCursorAtEnd].
*
* @param index Codepoint index to place cursor after, should be in range 0 to
* [MutableTextFieldValue.codepointLength], inclusive.
*
* @see placeCursorAfterCharAt
* @see placeCursorBeforeCodepointAt
*/
fun placeCursorAfterCodepointAt(index: Int) {
requireValidIndex(index, inCodepoints = true)
val charIndex = codepointIndexToCharIndex((index + 1).coerceAtMost(codepointLength))
selectionInChars = TextRange(charIndex)
}
/**
* Places the cursor after the character at the given index.
*
* If [index] is inside a surrogate pair or other invalid run, the cursor will be placed at the
* nearest later index.
*
* To place the cursor at the end of the field, after the last character, pass index
* [MutableTextFieldValue.length] or call [placeCursorAtEnd].
*
* @param index Character index to place cursor after, should be in range 0 to
* [MutableTextFieldValue.length], inclusive.
*
* @see placeCursorAfterCodepointAt
* @see placeCursorBeforeCharAt
*/
fun placeCursorAfterCharAt(index: Int) {
requireValidIndex(index, inCodepoints = false)
selectionInChars = TextRange((index + 1).coerceAtMost(length))
}
/**
* Places the cursor at the end of the text.
*/
fun placeCursorAtEnd() {
selectionInChars = TextRange(length)
}
/**
* Returns a [TextEditResult] that places the cursor after the last change made to this
* [TextFieldBuffer].
*
* @see placeCursorAtEnd
* @see placeCursorBeforeFirstChange
*/
fun placeCursorAfterLastChange() {
if (changes.changeCount > 0) {
placeCursorBeforeCharAt(changes.getRange(changes.changeCount).max)
}
}
/**
* Returns a [TextEditResult] that places the cursor before the first change made to this
* [TextFieldBuffer].
*
* @see placeCursorAfterLastChange
*/
fun placeCursorBeforeFirstChange() {
if (changes.changeCount > 0) {
placeCursorBeforeCharAt(changes.getRange(0).min)
}
}
/**
* Places the selection around the given [range] in codepoints.
*
* If the start or end of [range] fall inside invalid runs, the values will be adjusted to the
* nearest earlier and later codepoints, respectively.
*
* To place the start of the selection at the beginning of the field, pass index 0. To place the
* end of the selection at the end of the field, after the last codepoint, pass index
* [TextFieldBuffer.codepointLength]. Passing a zero-length range is the same as calling
* [placeCursorBeforeCodepointAt].
*
* @param range Codepoint range of the selection, should be in range 0 to
* [TextFieldBuffer.codepointLength], inclusive.
*
* @see selectCharsIn
*/
fun selectCodepointsIn(range: TextRange) {
requireValidRange(range, inCodepoints = true)
selectionInChars = codepointsToChars(range)
}
/**
* Places the selection around the given [range] in characters.
*
* If the start or end of [range] fall inside surrogate pairs or other invalid runs, the values will
* be adjusted to the nearest earlier and later characters, respectively.
*
* To place the start of the selection at the beginning of the field, pass index 0. To place the end
* of the selection at the end of the field, after the last character, pass index
* [TextFieldBuffer.length]. Passing a zero-length range is the same as calling
* [placeCursorBeforeCharAt].
*
* @param range Codepoint range of the selection, should be in range 0 to
* [TextFieldBuffer.length], inclusive.
*
* @see selectCharsIn
*/
fun selectCharsIn(range: TextRange) {
requireValidRange(range, inCodepoints = false)
selectionInChars = range
}
/**
* Places the selection around all the text.
*/
fun selectAll() {
selectionInChars = TextRange(0, length)
}
/**
* Returns a [TextEditResult] that places the selection before the first change and after the
* last change.
*
* @see selectAll
*/
fun selectAllChanges() {
if (changes.changeCount > 0) {
selectCharsIn(
TextRange(
changes.getRange(0).min,
changes.getRange(changes.changeCount).max
)
)
}
}
/**
* Revert all changes made to this value since it was created. After calling this method, this
* object will be in the same state it was when it was initially created, and [changes] will be
* empty.
*/
fun revertAllChanges() {
replace(0, length, sourceValue.toString())
selectionInChars = sourceValue.selectionInChars
clearChangeList()
}
override fun onTextWillChange(rangeToBeReplaced: TextRange, newLength: Int) {
super.onTextWillChange(rangeToBeReplaced, newLength)
// Adjust selection.
val start = rangeToBeReplaced.min
val end = rangeToBeReplaced.max
var selStart = selectionInChars.min
var selEnd = selectionInChars.max
if (selEnd < start) {
// The entire selection is before the insertion point – we don't have to adjust the
// mark at all, so skip the math.
return
}
if (selStart <= start && end <= selEnd) {
// The insertion is entirely inside the selection, move the end only.
val diff = newLength - (end - start)
// Preserve "cursorness".
if (selStart == selEnd) {
selStart += diff
}
selEnd += diff
} else if (selStart > start && selEnd < end) {
// Selection is entirely inside replacement, move it to the end.
selStart = start + newLength
selEnd = start + newLength
} else if (selStart >= end) {
// The entire selection is after the insertion, so shift everything forward.
val diff = newLength - (end - start)
selStart += diff
selEnd += diff
} else if (start < selStart) {
// Insertion is around start of selection, truncate start of selection.
selStart = start + newLength
selEnd += newLength - (end - start)
} else {
// Insertion is around end of selection, truncate end of selection.
selEnd = start
}
selectionInChars = TextRange(selStart, selEnd)
}
internal fun toTextFieldCharSequence(composition: TextRange? = null): TextFieldCharSequence =
toTextFieldCharSequence(selection = selectionInChars, composition = composition)
}