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