1 /*
2  * Copyright (C) 2022 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 android.text;
18 
19 import android.annotation.IntRange;
20 import android.annotation.NonNull;
21 import android.graphics.TemporaryBuffer;
22 import android.graphics.text.GraphemeBreak;
23 
24 /**
25  * Implementation of {@code SegmentFinder} using grapheme clusters as the text segment. Whitespace
26  * characters are included as segments.
27  *
28  * <p>For example, the text "a pot" would be divided into five text segments: "a", " ", "p", "o",
29  * "t".
30  *
31  * @see <a href="https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries">Unicode Text
32  *     Segmentation - Grapheme Cluster Boundaries</a>
33  */
34 public class GraphemeClusterSegmentFinder extends SegmentFinder {
35     private static AutoGrowArray.FloatArray sTempAdvances = null;
36     private final boolean[] mIsGraphemeBreak;
37 
38     /**
39      * Constructs a GraphemeClusterSegmentFinder instance for the specified text which uses the
40      * provided TextPaint to determine grapheme cluster boundaries.
41      *
42      * @param text text to be segmented
43      * @param textPaint TextPaint used to draw the text
44      */
GraphemeClusterSegmentFinder( @onNull CharSequence text, @NonNull TextPaint textPaint)45     public GraphemeClusterSegmentFinder(
46             @NonNull CharSequence text, @NonNull TextPaint textPaint) {
47 
48         if (sTempAdvances == null) {
49             sTempAdvances = new AutoGrowArray.FloatArray(text.length());
50         } else if (sTempAdvances.size() < text.length()) {
51             sTempAdvances.resize(text.length());
52         }
53 
54         mIsGraphemeBreak = new boolean[text.length()];
55         float[] advances = sTempAdvances.getRawArray();
56         char[] chars = TemporaryBuffer.obtain(text.length());
57 
58         TextUtils.getChars(text, 0, text.length(), chars, 0);
59 
60         textPaint.getTextWidths(chars, 0, text.length(), advances);
61 
62         GraphemeBreak.isGraphemeBreak(advances, chars, /* start= */ 0, /* end= */ text.length(),
63                 mIsGraphemeBreak);
64         TemporaryBuffer.recycle(chars);
65     }
66 
previousBoundary(@ntRangefrom = 0) int offset)67     private int previousBoundary(@IntRange(from = 0) int offset) {
68         if (offset <= 0) return DONE;
69         do {
70             --offset;
71         } while (offset > 0 && !mIsGraphemeBreak[offset]);
72         return offset;
73     }
74 
nextBoundary(@ntRangefrom = 0) int offset)75     private int nextBoundary(@IntRange(from = 0) int offset) {
76         if (offset >= mIsGraphemeBreak.length) return DONE;
77         do {
78             ++offset;
79         } while (offset < mIsGraphemeBreak.length && !mIsGraphemeBreak[offset]);
80         return offset;
81     }
82 
83     @Override
previousStartBoundary(@ntRangefrom = 0) int offset)84     public int previousStartBoundary(@IntRange(from = 0) int offset) {
85         return previousBoundary(offset);
86     }
87 
88     @Override
previousEndBoundary(@ntRangefrom = 0) int offset)89     public int previousEndBoundary(@IntRange(from = 0) int offset) {
90         if (offset == 0) return DONE;
91         int boundary = previousBoundary(offset);
92         // Check that there is another cursor position before, otherwise this is not a valid
93         // end boundary.
94         if (boundary == DONE || previousBoundary(boundary) == DONE) {
95             return DONE;
96         }
97         return boundary;
98     }
99 
100     @Override
nextStartBoundary(@ntRangefrom = 0) int offset)101     public int nextStartBoundary(@IntRange(from = 0) int offset) {
102         if (offset == mIsGraphemeBreak.length) return DONE;
103         int boundary = nextBoundary(offset);
104         // Check that there is another cursor position after, otherwise this is not a valid
105         // start boundary.
106         if (boundary == DONE || nextBoundary(boundary) == DONE) {
107             return DONE;
108         }
109         return boundary;
110     }
111 
112     @Override
nextEndBoundary(@ntRangefrom = 0) int offset)113     public int nextEndBoundary(@IntRange(from = 0) int offset) {
114         return nextBoundary(offset);
115     }
116 }
117