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.ui.text.android.selection
18
19 import android.graphics.Paint
20 import android.os.Build
21 import android.text.TextPaint
22 import androidx.annotation.RequiresApi
23 import androidx.compose.ui.text.android.selection.SegmentFinder.Companion.DONE
24 import java.text.BreakIterator
25
26 /**
27 * Compose version of [android.text.SegmentFinder].
28 *
29 * Finds text segment boundaries within text. Subclasses can implement different types of text
30 * segments. Grapheme clusters and words are examples of possible text segments. These are
31 * implemented by [GraphemeClusterSegmentFinder] and [WordSegmentFinder].
32 *
33 * Text segments may not overlap, so every character belongs to at most one text segment. A
34 * character may belong to no text segments.
35 *
36 * For example, WordSegmentFinder subdivides the text "Hello, World!" into four text segments:
37 * "Hello", ",", "World", "!". The space character does not belong to any text segments.
38 */
39 internal interface SegmentFinder {
40 /**
41 * Returns the character offset of the previous text segment start boundary before the specified
42 * character offset, or DONE if there are none.
43 */
previousStartBoundarynull44 fun previousStartBoundary(offset: Int): Int
45
46 /**
47 * Returns the character offset of the previous text segment end boundary before the specified
48 * character offset, or DONE if there are none.
49 */
50 fun previousEndBoundary(offset: Int): Int
51
52 /**
53 * Returns the character offset of the next text segment start boundary after the specified
54 * character offset, or DONE if there are none.
55 */
56 fun nextStartBoundary(offset: Int): Int
57
58 /**
59 * Returns the character offset of the next text segment end boundary after the specified
60 * character offset, or DONE if there are none.
61 */
62 fun nextEndBoundary(offset: Int): Int
63
64 companion object {
65 /**
66 * Return value of previousStartBoundary(int), previousEndBoundary(int),
67 * nextStartBoundary(int), and nextEndBoundary(int) when there are no boundaries of the
68 * specified type in the specified direction.
69 */
70 const val DONE = -1
71 }
72 }
73
74 /**
75 * Implementation of [SegmentFinder] using words as the text segment. Word boundaries are found
76 * using `WordIterator`. Whitespace characters are excluded, so they are not included in any text
77 * segments.
78 *
79 * For example, the text "Hello, World!" would be subdivided into four text segments: "Hello", ",",
80 * "World", "!". The space character does not belong to any text segments.
81 *
82 * @see
83 * [Unicode Text Segmentation - Word Boundaries](https://unicode.org/reports/tr29/.Word_Boundaries)
84 */
85 internal class WordSegmentFinder(
86 private val text: CharSequence,
87 private val wordIterator: WordIterator
88 ) : SegmentFinder {
previousStartBoundarynull89 override fun previousStartBoundary(offset: Int): Int {
90 var boundary = offset
91 do {
92 boundary = wordIterator.prevBoundary(boundary)
93 if (boundary == BreakIterator.DONE) {
94 return DONE
95 }
96 } while (Character.isWhitespace(text[boundary]))
97 return boundary
98 }
99
previousEndBoundarynull100 override fun previousEndBoundary(offset: Int): Int {
101 var boundary = offset
102 do {
103 boundary = wordIterator.prevBoundary(boundary)
104 if (boundary == BreakIterator.DONE || boundary == 0) {
105 return DONE
106 }
107 } while (Character.isWhitespace(text[boundary - 1]))
108 return boundary
109 }
110
nextStartBoundarynull111 override fun nextStartBoundary(offset: Int): Int {
112 var boundary = offset
113 do {
114 boundary = wordIterator.nextBoundary(boundary)
115 if (boundary == BreakIterator.DONE || boundary == text.length) {
116 return DONE
117 }
118 } while (Character.isWhitespace(text[boundary]))
119 return boundary
120 }
121
nextEndBoundarynull122 override fun nextEndBoundary(offset: Int): Int {
123 var boundary = offset
124 do {
125 boundary = wordIterator.nextBoundary(boundary)
126 if (boundary == BreakIterator.DONE) {
127 return DONE
128 }
129 } while (Character.isWhitespace(text[boundary - 1]))
130 return boundary
131 }
132 }
133
134 internal abstract class GraphemeClusterSegmentFinder : SegmentFinder {
135 /** Return the offset of the previous grapheme bounds before the given [offset]. */
previousnull136 abstract fun previous(offset: Int): Int
137
138 /** Return the offset of the next grapheme bounds after the given [offset]. */
139 abstract fun next(offset: Int): Int
140
141 override fun previousStartBoundary(offset: Int): Int {
142 return previous(offset)
143 }
144
previousEndBoundarynull145 override fun previousEndBoundary(offset: Int): Int {
146 val previousBoundary = previous(offset)
147 if (previousBoundary == DONE) {
148 return DONE
149 }
150
151 // Check that there is another cursor position before, otherwise this is not a valid
152 // end boundary.
153 return if (previous(previousBoundary) == DONE) {
154 DONE
155 } else {
156 previousBoundary
157 }
158 }
159
nextStartBoundarynull160 override fun nextStartBoundary(offset: Int): Int {
161 val nextBoundary = next(offset)
162 if (nextBoundary == DONE) {
163 return DONE
164 }
165
166 // Check that there is another cursor position after, otherwise this is not a valid
167 // end boundary.
168 return if (next(nextBoundary) == DONE) {
169 DONE
170 } else {
171 nextBoundary
172 }
173 }
174
nextEndBoundarynull175 override fun nextEndBoundary(offset: Int): Int {
176 return next(offset)
177 }
178 }
179
180 internal class GraphemeClusterSegmentFinderUnderApi29(private val text: CharSequence) :
181 GraphemeClusterSegmentFinder() {
182
183 private val breakIterator =
<lambda>null184 BreakIterator.getCharacterInstance().also { it.setText(text.toString()) }
185
previousnull186 override fun previous(offset: Int): Int {
187 return breakIterator.preceding(offset)
188 }
189
nextnull190 override fun next(offset: Int): Int {
191 return breakIterator.following(offset)
192 }
193 }
194
195 @RequiresApi(29)
196 internal class GraphemeClusterSegmentFinderApi29(
197 private val text: CharSequence,
198 private val textPaint: TextPaint
199 ) : GraphemeClusterSegmentFinder() {
previousnull200 override fun previous(offset: Int): Int {
201 // getTextRunCursor will return -1 or DONE when it can't find the previous cursor position.
202 return textPaint.getTextRunCursor(text, 0, text.length, false, offset, Paint.CURSOR_BEFORE)
203 }
204
nextnull205 override fun next(offset: Int): Int {
206 // getTextRunCursor will return -1 or DONE when it can't find the next cursor position.
207 return textPaint.getTextRunCursor(text, 0, text.length, false, offset, Paint.CURSOR_AFTER)
208 }
209 }
210
createGraphemeClusterSegmentFindernull211 internal fun createGraphemeClusterSegmentFinder(
212 text: CharSequence,
213 textPaint: TextPaint
214 ): SegmentFinder {
215 return if (Build.VERSION.SDK_INT >= 29) {
216 GraphemeClusterSegmentFinderApi29(text, textPaint)
217 } else {
218 GraphemeClusterSegmentFinderUnderApi29(text)
219 }
220 }
221
222 @RequiresApi(34)
223 internal object Api34SegmentFinder {
toAndroidSegmentFindernull224 internal fun SegmentFinder.toAndroidSegmentFinder(): android.text.SegmentFinder {
225 return object : android.text.SegmentFinder() {
226 override fun previousStartBoundary(offset: Int): Int =
227 this@toAndroidSegmentFinder.previousStartBoundary(offset)
228
229 override fun previousEndBoundary(offset: Int): Int =
230 this@toAndroidSegmentFinder.previousEndBoundary(offset)
231
232 override fun nextStartBoundary(offset: Int): Int =
233 this@toAndroidSegmentFinder.nextStartBoundary(offset)
234
235 override fun nextEndBoundary(offset: Int): Int =
236 this@toAndroidSegmentFinder.nextEndBoundary(offset)
237 }
238 }
239 }
240