[go: nahoru, domu]

blob: fc44c7494258c36c877fc5ce6f5393391a37eb15 [file] [log] [blame]
/*
* 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.ui.input
import androidx.ui.text.InternalTextApi
import androidx.ui.text.TextRange
/**
* The editing buffer
*
* This class manages the all editing relate states, editing buffers, selection, styles, etc.
*/
@OptIn(InternalTextApi::class)
class EditingBuffer(
/**
* The initial text of this editing buffer
*/
initialText: String,
/**
* The initial selection range of this buffer.
* If you provide collapsed selection, it is treated as the cursor position. The cursor and
* selection cannot exists at the same time.
* The selection must points the valid index of the initialText, otherwise
* IndexOutOfBoundsException will be thrown.
*/
initialSelection: TextRange // The initial selection range
) {
internal companion object {
const val NOWHERE = -1
}
private val gapBuffer = PartialGapBuffer(initialText)
/**
* The inclusive selection start offset
*/
internal var selectionStart = initialSelection.min
private set
/**
* The exclusive selection end offset
*/
internal var selectionEnd = initialSelection.max
private set
/**
* The inclusive composition start offset
*
* If there is no composing text, returns -1
*/
internal var compositionStart = NOWHERE
private set
/**
* The exclusive composition end offset
*
* If there is no composing text, returns -1
*/
internal var compositionEnd = NOWHERE
private set
/**
* Helper function that returns true if the editing buffer has composition text
*/
internal fun hasComposition(): Boolean = compositionStart != NOWHERE
/**
* Helper accessor for cursor offset
*/
internal var cursor: Int
/**
* Return the cursor offset.
*
* Since selection and cursor cannot exist at the same time, return -1 if there is a
* selection.
*/
get() = if (selectionStart == selectionEnd) selectionEnd else -1
/**
* Set the cursor offset.
*
* Since selection and cursor cannot exist at the same time, cancel selection if there is.
*/
set(cursor) = setSelection(cursor, cursor)
/**
* [] operator for the character at the index.
*/
internal operator fun get(index: Int): Char = gapBuffer[index]
/**
* Returns the length of the buffer.
*/
internal val length: Int get() = gapBuffer.length
init {
val start = initialSelection.min
val end = initialSelection.max
if (start < 0 || start > initialText.length) {
throw IndexOutOfBoundsException(
"start ($start) offset is outside of text region ${initialText.length}")
}
if (end < 0 || end > initialText.length) {
throw IndexOutOfBoundsException(
"end ($end) offset is outside of text region ${initialText.length}")
}
if (start > end) {
throw IllegalArgumentException("Do not set reversed range: $start > $end")
}
}
/**
* Replace the text and move the cursor to the end of inserted text.
*
* This function cancels selection if there.
*
* @throws IndexOutOfBoundsException if start or end offset is outside of current buffer
* @throws IllegalArgumentException if start is larger than end. (reversed range)
*/
internal fun replace(start: Int, end: Int, text: String) {
if (start < 0 || start > gapBuffer.length) {
throw IndexOutOfBoundsException(
"start ($start) offset is outside of text region ${gapBuffer.length}")
}
if (end < 0 || end > gapBuffer.length) {
throw IndexOutOfBoundsException(
"end ($end) offset is outside of text region ${gapBuffer.length}")
}
if (start > end) {
throw IllegalArgumentException("Do not set reversed range: $start > $end")
}
gapBuffer.replace(start, end, text)
// On Android, all text modification APIs also provides explicit cursor location. On the
// hand, desktop application usually doesn't. So, here tentatively move the cursor to the
// end offset of the editing area for desktop like application. In case of Android,
// implementation will call setSelection immediately after replace function to update this
// tentative cursor location.
selectionStart = start + text.length
selectionEnd = start + text.length
// Similarly, if text modification happens, cancel ongoing composition. If caller want to
// change the composition text, it is caller responsibility to call setComposition again
// to set composition range after replace function.
compositionStart = NOWHERE
compositionEnd = NOWHERE
}
/**
* Remove the given range of text.
*
* Different from replace method, this doesn't move cursor location to the end of modified text.
* Instead, preserve the selection with adjusting the deleted text.
*/
internal fun delete(start: Int, end: Int) {
val deleteRange = TextRange(start, end)
if (deleteRange.intersects(TextRange(selectionStart, selectionEnd))) {
// Currently only target for deleteSurroundingText/deleteSurroundingTextInCodePoints.
TODO("support deletion within selection range.")
}
gapBuffer.replace(start, end, "")
if (end <= selectionStart) {
selectionStart -= deleteRange.length
selectionEnd -= deleteRange.length
}
if (!hasComposition()) {
return
}
val compositionRange = TextRange(compositionStart, compositionEnd)
// Following figure shows the deletion range and composition range.
// |---| represents a range to be deleted.
// |===| represents a composition range.
if (deleteRange.intersects(compositionRange)) {
if (deleteRange.contains(compositionRange)) {
// Input:
// Buffer : ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Delete : |-------------|
// Composition: |======|
//
// Result:
// Buffer : ABCDETUVWXYZ
// Composition:
compositionStart = NOWHERE
compositionEnd = NOWHERE
} else if (compositionRange.contains(deleteRange)) {
// Input:
// Buffer : ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Delete : |------|
// Composition: |==========|
//
// Result:
// Buffer : ABCDEFGHIQRSTUVWXYZ
// Composition: |===|
compositionEnd -= deleteRange.length
} else if (deleteRange.contains(compositionStart)) {
// Input:
// Buffer : ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Delete : |---------|
// Composition: |========|
//
// Result:
// Buffer : ABCDEFPQRSTUVWXYZ
// Composition: |=====|
compositionStart = deleteRange.min
compositionEnd -= deleteRange.length
} else { // deleteRange contains compositionEnd
// Input:
// Buffer : ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Delete : |---------|
// Composition: |=======|
//
// Result:
// Buffer : ABCDEFGHSTUVWXYZ
// Composition: |====|
compositionEnd = deleteRange.min
}
} else {
if (compositionStart <= deleteRange.min) {
// Input:
// Buffer : ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Delete : |-------|
// Composition: |=======|
//
// Result:
// Buffer : ABCDEFGHIJKLTUVWXYZ
// Composition: |=======|
// do nothing
} else {
// Input:
// Buffer : ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Delete : |-------|
// Composition: |=======|
//
// Result:
// Buffer : AJKLMNOPQRSTUVWXYZ
// Composition: |=======|
compositionStart -= deleteRange.length
compositionEnd -= deleteRange.length
}
}
}
/**
* Mark the specified area of the text as selected text.
*
* You can set cursor by specifying the same value to `start` and `end`.
* The reversed range is not allowed.
* @param start the inclusive start offset of the selection
* @param end the exclusive end offset of the selection
*
* @throws IndexOutOfBoundsException if start or end offset is outside of current buffer.
* @throws IllegalArgumentException if start is larger than end. (reversed range)
*/
internal fun setSelection(start: Int, end: Int) {
if (start < 0 || start > gapBuffer.length) {
throw IndexOutOfBoundsException(
"start ($start) offset is outside of text region ${gapBuffer.length}")
}
if (end < 0 || end> gapBuffer.length) {
throw IndexOutOfBoundsException(
"end ($end) offset is outside of text region ${gapBuffer.length}")
}
if (start > end) {
throw IllegalArgumentException("Do not set reversed range: $start > $end")
}
selectionStart = start
selectionEnd = end
}
/**
* Mark the specified area of the text as composition text.
*
* The empty range or reversed range is not allowed.
* Use clearComposition in case of clearing composition.
*
* @param start the inclusive start offset of the composition
* @param end the exclusive end offset of the composition
*
* @throws IndexOutOfBoundsException if start or end offset is ouside of current buffer
* @throws IllegalArgumentException if start is larger than or equal to end. (reversed or
* collapsed range)
*/
internal fun setComposition(start: Int, end: Int) {
if (start < 0 || start > gapBuffer.length) {
throw IndexOutOfBoundsException(
"start ($start) offset is outside of text region ${gapBuffer.length}")
}
if (end < 0 || end> gapBuffer.length) {
throw IndexOutOfBoundsException(
"end ($end) offset is outside of text region ${gapBuffer.length}")
}
if (start >= end) {
throw IllegalArgumentException("Do not set reversed or empty range: $start > $end")
}
compositionStart = start
compositionEnd = end
}
/**
* Removes the ongoing composition text and reset the composition range.
*/
internal fun cancelComposition() {
replace(compositionStart, compositionEnd, "")
compositionStart = NOWHERE
compositionEnd = NOWHERE
}
/**
* Commits the ongoing composition text and reset the composition range.
*/
internal fun commitComposition() {
compositionStart = NOWHERE
compositionEnd = NOWHERE
}
override fun toString(): String = gapBuffer.toString()
}