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