blob: 9b6c9abcf92445feeb275b5c04936d5d8d3bf62b [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.
*/
@file:OptIn(ExperimentalFoundationApi::class)
package androidx.compose.foundation.text2.input
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.ui.text.TextRange
/**
* A value to be returned from [TextFieldState.edit] that specifies where the place the cursor or
* selection.
*
* Predefined results include:
* - [TextFieldBuffer.placeCursorAtEnd]
* - [TextFieldBuffer.placeCursorBeforeFirstChange]
* - [TextFieldBuffer.placeCursorAfterLastChange]
* - [TextFieldBuffer.placeCursorBeforeCharAt]
* - [TextFieldBuffer.placeCursorAfterCharAt]
* - [TextFieldBuffer.placeCursorBeforeCodepointAt]
* - [TextFieldBuffer.placeCursorAfterCodepointAt]
* - [TextFieldBuffer.selectAll]
* - [TextFieldBuffer.selectAllChanges]
* - [TextFieldBuffer.selectCharsIn]
* - [TextFieldBuffer.selectCodepointsIn]
*/
@ExperimentalFoundationApi
sealed class TextEditResult {
/**
* Returns the [TextRange] to set as the new selection. If the selection is collapsed, it will
* set the cursor instead.
*
* @return The new range of the selection, in character offsets.
*/
internal abstract fun calculateSelection(
oldValue: TextFieldCharSequence,
newValue: TextFieldBuffer
): TextRange
}
/**
* Returns a [TextEditResult] that places the cursor before the codepoint at the given index.
*
* _Note: This method has no side effects – just calling it will not perform the action, its return
* value must be returned from [TextFieldState.edit]._
*
* 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].
*
* @param index Codepoint index to place cursor before, should be in range 0 to
* [TextFieldBuffer.codepointLength], inclusive.
*
* @see placeCursorBeforeCharAt
* @see placeCursorAfterCodepointAt
* @see TextFieldState.edit
*/
@ExperimentalFoundationApi
fun TextFieldBuffer.placeCursorBeforeCodepointAt(index: Int): TextEditResult =
PlaceCursorResult(this, index, after = false, inCodepoints = true)
/**
* Returns a [TextEditResult] that places the cursor after the codepoint at the given index.
*
* _Note: This method has no side effects – just calling it will not perform the action, its return
* value must be returned from [TextFieldState.edit]._
*
* If [index] is inside an invalid run, the cursor will be placed at the nearest earlier index.
*
* 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 after, should be in range 0 to
* [TextFieldBuffer.codepointLength], inclusive.
*
* @see placeCursorBeforeCharAt
* @see placeCursorAfterCodepointAt
* @see TextFieldState.edit
*/
@ExperimentalFoundationApi
fun TextFieldBuffer.placeCursorAfterCodepointAt(index: Int): TextEditResult =
PlaceCursorResult(this, index, after = true, inCodepoints = true)
/**
* Returns a [TextEditResult] that places the cursor before the character at the given index.
*
* _Note: This method has no side effects – just calling it will not perform the action, its return
* value must be returned from [TextFieldState.edit]._
*
* 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
* @see TextFieldState.edit
*/
@ExperimentalFoundationApi
fun TextFieldBuffer.placeCursorBeforeCharAt(index: Int): TextEditResult =
PlaceCursorResult(this, index, after = false, inCodepoints = false)
/**
* Returns a [TextEditResult] that places the cursor after the character at the given index.
*
* _Note: This method has no side effects – just calling it will not perform the action, its return
* value must be returned from [TextFieldState.edit]._
*
* 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 end of the field, after the last character, pass index
* [TextFieldBuffer.length] or call [placeCursorAtEnd].
*
* @param index Character index to place cursor after, should be in range 0 to
* [TextFieldBuffer.length], inclusive.
*
* @see placeCursorAfterCodepointAt
* @see placeCursorBeforeCharAt
* @see TextFieldState.edit
*/
@ExperimentalFoundationApi
fun TextFieldBuffer.placeCursorAfterCharAt(index: Int): TextEditResult =
PlaceCursorResult(this, index, after = true, inCodepoints = false)
/**
* Returns a [TextEditResult] that places the cursor at the end of the text.
*
* _Note: This method has no side effects – just calling it will not perform the action, its return
* value must be returned from [TextFieldState.edit]._
*
* @see placeCursorAfterLastChange
* @see TextFieldState.edit
*/
@Suppress("UnusedReceiverParameter")
@ExperimentalFoundationApi
fun TextFieldBuffer.placeCursorAtEnd(): TextEditResult = PlaceCursorAtEndResult
/**
* Returns a [TextEditResult] that places the cursor after the last change made to this
* [TextFieldBuffer].
*
* _Note: This method has no side effects – just calling it will not perform the action, its return
* value must be returned from [TextFieldState.edit]._
*
* @see placeCursorAtEnd
* @see placeCursorBeforeFirstChange
* @see TextFieldState.edit
*/
@Suppress("UnusedReceiverParameter")
@ExperimentalFoundationApi
fun TextFieldBuffer.placeCursorAfterLastChange(): TextEditResult =
PlaceCursorAfterLastChangeResult
/**
* Returns a [TextEditResult] that places the cursor before the first change made to this
* [TextFieldBuffer].
*
* _Note: This method has no side effects – just calling it will not perform the action, its return
* value must be returned from [TextFieldState.edit]._
*
* @see placeCursorAfterLastChange
* @see TextFieldState.edit
*/
@Suppress("UnusedReceiverParameter")
@ExperimentalFoundationApi
fun TextFieldBuffer.placeCursorBeforeFirstChange(): TextEditResult =
PlaceCursorBeforeFirstChangeResult
/**
* Returns a [TextEditResult] that places the selection around the given [range] in codepoints.
*
* _Note: This method has no side effects – just calling it will not perform the action, its return
* value must be returned from [TextFieldState.edit]._
*
* 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
* @see TextFieldState.edit
*/
@ExperimentalFoundationApi
fun TextFieldBuffer.selectCodepointsIn(range: TextRange): TextEditResult =
SelectRangeResult(this, range, inCodepoints = true)
/**
* Returns a [TextEditResult] that places the selection around the given [range] in characters.
*
* _Note: This method has no side effects – just calling it will not perform the action, its return
* value must be returned from [TextFieldState.edit]._
*
* 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
* @see TextFieldState.edit
*/
@ExperimentalFoundationApi
fun TextFieldBuffer.selectCharsIn(range: TextRange): TextEditResult =
SelectRangeResult(this, range, inCodepoints = false)
/**
* Returns a [TextEditResult] that places the selection before the first change and after the
* last change.
*
* _Note: This method has no side effects – just calling it will not perform the action, its return
* value must be returned from [TextFieldState.edit]._
*
* @see selectAll
* @see TextFieldState.edit
*/
@Suppress("UnusedReceiverParameter")
@ExperimentalFoundationApi
fun TextFieldBuffer.selectAllChanges(): TextEditResult = SelectAllChangesResult
/**
* Returns a [TextEditResult] that places the selection around all the text.
*
* _Note: This method has no side effects – just calling it will not perform the action, its return
* value must be returned from [TextFieldState.edit]._
*
* @see selectAllChanges
* @see TextFieldState.edit
*/
@Suppress("UnusedReceiverParameter")
@ExperimentalFoundationApi
fun TextFieldBuffer.selectAll(): TextEditResult = SelectAllResult
private object PlaceCursorAtEndResult : TextEditResult() {
override fun calculateSelection(
oldValue: TextFieldCharSequence,
newValue: TextFieldBuffer
): TextRange = TextRange(newValue.length)
override fun toString(): String = "placeCursorAtEnd()"
}
private object PlaceCursorAfterLastChangeResult : TextEditResult() {
override fun calculateSelection(
oldValue: TextFieldCharSequence,
newValue: TextFieldBuffer
): TextRange {
return if (newValue.changes.changeCount == 0) {
oldValue.selectionInChars
} else {
TextRange(newValue.changes.getRange(newValue.changes.changeCount).max)
}
}
override fun toString(): String = "placeCursorAfterLastChange()"
}
private object PlaceCursorBeforeFirstChangeResult : TextEditResult() {
override fun calculateSelection(
oldValue: TextFieldCharSequence,
newValue: TextFieldBuffer
): TextRange {
return if (newValue.changes.changeCount == 0) {
oldValue.selectionInChars
} else {
TextRange(newValue.changes.getRange(0).min)
}
}
override fun toString(): String = "placeCursorBeforeFirstChange()"
}
private object SelectAllChangesResult : TextEditResult() {
override fun calculateSelection(
oldValue: TextFieldCharSequence,
newValue: TextFieldBuffer
): TextRange {
val changes = newValue.changes
return if (changes.changeCount == 0) {
oldValue.selectionInChars
} else {
TextRange(
changes.getRange(0).min,
changes.getRange(changes.changeCount).max
)
}
}
override fun toString(): String = "selectAllChanges()"
}
private object SelectAllResult : TextEditResult() {
override fun calculateSelection(
oldValue: TextFieldCharSequence,
newValue: TextFieldBuffer
): TextRange = TextRange(0, newValue.length)
override fun toString(): String = "selectAll()"
}
private class PlaceCursorResult(
value: TextFieldBuffer,
private val rawIndex: Int,
private val after: Boolean,
private val inCodepoints: Boolean
) : TextEditResult() {
init {
value.requireValidIndex(rawIndex, inCodepoints)
}
override fun calculateSelection(
oldValue: TextFieldCharSequence,
newValue: TextFieldBuffer
): TextRange {
var index = if (after) rawIndex + 1 else rawIndex
index = index.coerceAtMost(if (inCodepoints) newValue.codepointLength else newValue.length)
index = if (inCodepoints) newValue.codepointIndexToCharIndex(index) else index
return TextRange(index)
}
override fun toString(): String = buildString {
append("placeCursor")
append(if (after) "After" else "Before")
append(if (inCodepoints) "Codepoint" else "Char")
append("At(index=$rawIndex)")
}
}
private class SelectRangeResult(
value: TextFieldBuffer,
private val rawRange: TextRange,
private val inCodepoints: Boolean
) : TextEditResult() {
init {
value.requireValidRange(rawRange, inCodepoints)
}
override fun calculateSelection(
oldValue: TextFieldCharSequence,
newValue: TextFieldBuffer
): TextRange = if (inCodepoints) newValue.codepointsToChars(rawRange) else rawRange
override fun toString(): String = buildString {
append("select")
append(if (inCodepoints) "Codepoints" else "Chars")
append("In(range=$rawRange)")
}
}