1 /*
2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.foundation.text.input
18
19 import androidx.compose.foundation.text.input.internal.toCharArray
20 import androidx.compose.ui.text.AnnotatedString
21 import androidx.compose.ui.text.TextRange
22 import androidx.compose.ui.text.coerceIn
23 import kotlin.jvm.JvmInline
24
25 internal typealias PlacedAnnotation = AnnotatedString.Range<AnnotatedString.Annotation>
26
27 /**
28 * An immutable snapshot of the contents of a [TextFieldState].
29 *
30 * This class is a [CharSequence] and directly represents the text being edited. It also stores the
31 * current [selection] of the field, which may either represent the cursor (if the selection is
32 * [collapsed][TextRange.collapsed]) or the selection range.
33 *
34 * This class also may contain the range being composed by the IME, if any, although this is not
35 * exposed.
36 *
37 * @param text If this TextFieldCharSequence is actually a copy of another, make sure to use the
38 * backing CharSequence object to stop unnecessary nesting and logic that depends on exact
39 * equality of CharSequence comparison that's using [CharSequence.equals].
40 * @see TextFieldBuffer
41 */
42 internal class TextFieldCharSequence(
43 text: CharSequence = "",
44 selection: TextRange = TextRange.Zero,
45 composition: TextRange? = null,
46 highlight: Pair<TextHighlightType, TextRange>? = null,
47 val composingAnnotations: List<PlacedAnnotation>? = null
48 ) : CharSequence {
49
50 override val length: Int
51 get() = text.length
52
53 val text: CharSequence = if (text is TextFieldCharSequence) text.text else text
54
55 /**
56 * The selection range. If the selection is collapsed, it represents cursor location. When
57 * selection range is out of bounds, it is constrained with the text length.
58 */
59 val selection: TextRange = selection.coerceIn(0, text.length)
60
61 /**
62 * Composition range created by IME. If null, there is no composition range.
63 *
64 * Input service composition is an instance of text produced by IME. An example visual for the
65 * composition is that the currently composed word is visually separated from others with
66 * underline, or text background. For description of composition please check
67 * [W3C IME Composition](https://www.w3.org/TR/ime-api/#ime-composition)
68 *
69 * Composition can only be set by the system.
70 */
71 val composition: TextRange? = composition?.coerceIn(0, text.length)
72
73 /**
74 * Range of text to be highlighted. This may be used to display handwriting gesture previews
75 * from the IME.
76 */
77 val highlight: Pair<TextHighlightType, TextRange>? =
78 highlight?.copy(second = highlight.second.coerceIn(0, text.length))
79
getnull80 override operator fun get(index: Int): Char = text[index]
81
82 override fun subSequence(startIndex: Int, endIndex: Int): CharSequence =
83 text.subSequence(startIndex, endIndex)
84
85 override fun toString(): String = text.toString()
86
87 fun contentEquals(other: CharSequence): Boolean = text.contentEquals(other)
88
89 /**
90 * Copies the contents of this sequence from [[sourceStartIndex], [sourceEndIndex]) into
91 * [destination] starting at [destinationOffset].
92 */
93 fun toCharArray(
94 destination: CharArray,
95 destinationOffset: Int,
96 sourceStartIndex: Int,
97 sourceEndIndex: Int
98 ) {
99 text.toCharArray(destination, destinationOffset, sourceStartIndex, sourceEndIndex)
100 }
101
102 /**
103 * Whether to show the cursor or selection and associated handles. When there is a handwriting
104 * gesture preview highlight, the cursor or selection should be hidden.
105 */
shouldShowSelectionnull106 fun shouldShowSelection(): Boolean = highlight == null
107
108 /**
109 * Returns true if [other] is a [TextFieldCharSequence] with the same contents, text, and
110 * composition. To compare just the text, call [contentEquals].
111 */
112 override fun equals(other: Any?): Boolean {
113 if (this === other) return true
114 if (other === null) return false
115 if (this::class != other::class) return false
116
117 other as TextFieldCharSequence
118
119 if (selection != other.selection) return false
120 if (composition != other.composition) return false
121 if (highlight != other.highlight) return false
122 if (composingAnnotations != other.composingAnnotations) return false
123 if (!contentEquals(other.text)) return false
124
125 return true
126 }
127
hashCodenull128 override fun hashCode(): Int {
129 var result = text.hashCode()
130 result = 31 * result + selection.hashCode()
131 result = 31 * result + (composition?.hashCode() ?: 0)
132 result = 31 * result + highlight.hashCode()
133 result = 31 * result + composingAnnotations.hashCode()
134 return result
135 }
136 }
137
138 /** A text range highlight type. The highlight styling depends on the type. */
139 @JvmInline
140 internal value class TextHighlightType private constructor(private val value: Int) {
141 companion object {
142 /**
143 * A highlight which previews the text range which would be selected by an ongoing stylus
144 * handwriting select gesture.
145 */
146 val HandwritingSelectPreview = TextHighlightType(0)
147
148 /**
149 * A highlight which previews the text range which would be deleted by an ongoing stylus
150 * handwriting delete gesture.
151 */
152 val HandwritingDeletePreview = TextHighlightType(1)
153 }
154 }
155
156 /**
157 * Returns the text before the selection.
158 *
159 * @param maxChars maximum number of characters (inclusive) before the minimum value in
160 * [TextFieldCharSequence.selection].
161 * @see TextRange.min
162 */
getTextBeforeSelectionnull163 internal fun TextFieldCharSequence.getTextBeforeSelection(maxChars: Int): CharSequence =
164 subSequence(kotlin.math.max(0, selection.min - maxChars), selection.min)
165
166 /**
167 * Returns the text after the selection.
168 *
169 * @param maxChars maximum number of characters (exclusive) after the maximum value in
170 * [TextFieldCharSequence.selection].
171 * @see TextRange.max
172 */
173 internal fun TextFieldCharSequence.getTextAfterSelection(maxChars: Int): CharSequence =
174 subSequence(selection.max, kotlin.math.min(selection.max + maxChars, length))
175
176 /** Returns the currently selected text. */
177 internal fun TextFieldCharSequence.getSelectedText(): CharSequence =
178 subSequence(selection.min, selection.max)
179