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