1 /*
2  * Copyright (C) 2017 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 static org.junit.Assert.assertArrayEquals;
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertTrue;
23 
24 import android.graphics.Canvas;
25 import android.graphics.Paint;
26 import android.graphics.Typeface;
27 import android.platform.test.annotations.Presubmit;
28 import android.text.Layout.TabStops;
29 import android.text.style.ReplacementSpan;
30 import android.text.style.TabStopSpan;
31 
32 import androidx.test.InstrumentationRegistry;
33 import androidx.test.filters.SmallTest;
34 import androidx.test.filters.Suppress;
35 import androidx.test.runner.AndroidJUnit4;
36 
37 import org.junit.Test;
38 import org.junit.runner.RunWith;
39 
40 import java.util.Arrays;
41 
42 @Presubmit
43 @SmallTest
44 @RunWith(AndroidJUnit4.class)
45 public class TextLineTest {
stretchesToFullWidth(CharSequence line)46     private boolean stretchesToFullWidth(CharSequence line) {
47         final TextPaint paint = new TextPaint();
48         final TextLine tl = TextLine.obtain();
49         tl.set(paint, line, 0, line.length(), Layout.DIR_LEFT_TO_RIGHT,
50                 Layout.DIRS_ALL_LEFT_TO_RIGHT, false /* hasTabs */, null /* tabStops */,
51                 0, 0 /* no ellipsis */);
52         final float originalWidth = tl.metrics(null);
53         final float expandedWidth = 2 * originalWidth;
54 
55         tl.justify(expandedWidth);
56         final float newWidth = tl.metrics(null);
57         TextLine.recycle(tl);
58         return Math.abs(newWidth - expandedWidth) < 0.5;
59     }
60 
61     @Test
testJustify_spaces()62     public void testJustify_spaces() {
63         // There are no spaces to stretch.
64         assertFalse(stretchesToFullWidth("text"));
65 
66         assertTrue(stretchesToFullWidth("one space"));
67         assertTrue(stretchesToFullWidth("exactly two spaces"));
68         assertTrue(stretchesToFullWidth("up to three spaces"));
69     }
70 
71     // NBSP should also stretch when it's not used as a base for a combining mark. This doesn't work
72     // yet (b/68204709).
73     @Suppress
disabledTestJustify_NBSP()74     public void disabledTestJustify_NBSP() {
75         final char nbsp = '\u00A0';
76         assertTrue(stretchesToFullWidth("non-breaking" + nbsp + "space"));
77         assertTrue(stretchesToFullWidth("mix" + nbsp + "and match"));
78 
79         final char combining_acute = '\u0301';
80         assertFalse(stretchesToFullWidth("combining" + nbsp + combining_acute + "acute"));
81     }
82 
83     // The test font has following coverage and width.
84     // U+0020: 10em
85     // U+002E (.): 10em
86     // U+0043 (C): 100em
87     // U+0049 (I): 1em
88     // U+004C (L): 50em
89     // U+0056 (V): 5em
90     // U+0058 (X): 10em
91     // U+005F (_): 0em
92     // U+05D0    : 1em  // HEBREW LETTER ALEF
93     // U+05D1    : 5em  // HEBREW LETTER BET
94     // U+FFFD (invalid surrogate will be replaced to this): 7em
95     // U+10331 (\uD800\uDF31): 10em
96     private static final Typeface TYPEFACE = Typeface.createFromAsset(
97             InstrumentationRegistry.getInstrumentation().getTargetContext().getAssets(),
98             "fonts/StaticLayoutLineBreakingTestFont.ttf");
99 
getTextLine(String str, TextPaint paint, TabStops tabStops)100     private TextLine getTextLine(String str, TextPaint paint, TabStops tabStops) {
101         Layout layout =
102                 StaticLayout.Builder.obtain(str, 0, str.length(), paint, Integer.MAX_VALUE)
103                     .build();
104         TextLine tl = TextLine.obtain();
105         tl.set(paint, str, 0, str.length(),
106                 TextDirectionHeuristics.FIRSTSTRONG_LTR.isRtl(str, 0, str.length()) ? -1 : 1,
107                 layout.getLineDirections(0), tabStops != null, tabStops,
108                 0, 0 /* no ellipsis */);
109         return tl;
110     }
111 
getTextLine(String str, TextPaint paint)112     private TextLine getTextLine(String str, TextPaint paint) {
113         return getTextLine(str, paint, null);
114     }
115 
assertMeasurements(final TextLine tl, final int length, boolean trailing, final float[] expected)116     private void assertMeasurements(final TextLine tl, final int length, boolean trailing,
117             final float[] expected) {
118         for (int offset = 0; offset <= length; ++offset) {
119             assertEquals(expected[offset], tl.measure(offset, trailing, null), 0.0f);
120         }
121 
122         final boolean[] trailings = new boolean[length + 1];
123         Arrays.fill(trailings, trailing);
124         final float[] allMeasurements = tl.measureAllOffsets(trailings, null);
125         assertArrayEquals(expected, allMeasurements, 0.0f);
126     }
127 
128     @Test
testMeasure_LTR()129     public void testMeasure_LTR() {
130         final TextPaint paint = new TextPaint();
131         paint.setTypeface(TYPEFACE);
132         paint.setTextSize(10.0f);  // make 1em = 10px
133 
134         TextLine tl = getTextLine("IIIIIV", paint);
135         assertMeasurements(tl, 6, false,
136                 new float[]{0.0f, 10.0f, 20.0f, 30.0f, 40.0f, 50.0f, 100.0f});
137         assertMeasurements(tl, 6, true,
138                 new float[]{0.0f, 10.0f, 20.0f, 30.0f, 40.0f, 50.0f, 100.0f});
139     }
140 
141     @Test
testMeasure_RTL()142     public void testMeasure_RTL() {
143         final TextPaint paint = new TextPaint();
144         paint.setTypeface(TYPEFACE);
145         paint.setTextSize(10.0f);  // make 1em = 10px
146 
147         TextLine tl = getTextLine("\u05D0\u05D0\u05D0\u05D0\u05D0\u05D1", paint);
148         assertMeasurements(tl, 6, false,
149                 new float[]{0.0f, -10.0f, -20.0f, -30.0f, -40.0f, -50.0f, -100.0f});
150         assertMeasurements(tl, 6, true,
151                 new float[]{0.0f, -10.0f, -20.0f, -30.0f, -40.0f, -50.0f, -100.0f});
152     }
153 
154     @Test
testMeasure_BiDi()155     public void testMeasure_BiDi() {
156         final TextPaint paint = new TextPaint();
157         paint.setTypeface(TYPEFACE);
158         paint.setTextSize(10.0f);  // make 1em = 10px
159 
160         TextLine tl = getTextLine("II\u05D0\u05D0II", paint);
161         assertMeasurements(tl, 6, false,
162                 new float[]{0.0f, 10.0f, 40.0f, 30.0f, 40.0f, 50.0f, 60.0f});
163         assertMeasurements(tl, 6, true,
164                 new float[]{0.0f, 10.0f, 20.0f, 30.0f, 20.0f, 50.0f, 60.0f});
165     }
166 
167     private static final String LRI = "\u2066";  // LEFT-TO-RIGHT ISOLATE
168     private static final String RLI = "\u2067";  // RIGHT-TO-LEFT ISOLATE
169     private static final String PDI = "\u2069";  // POP DIRECTIONAL ISOLATE
170 
171     @Test
testMeasure_BiDi2()172     public void testMeasure_BiDi2() {
173         final TextPaint paint = new TextPaint();
174         paint.setTypeface(TYPEFACE);
175         paint.setTextSize(10.0f);  // make 1em = 10px
176 
177         TextLine tl = getTextLine("I" + RLI + "I\u05D0\u05D0" + PDI + "I", paint);
178         assertMeasurements(tl, 7, false,
179                 new float[]{0.0f, 10.0f, 30.0f, 30.0f, 20.0f, 40.0f, 40.0f, 50.0f});
180         assertMeasurements(tl, 7, true,
181                 new float[]{0.0f, 10.0f, 10.0f, 40.0f, 20.0f, 10.0f, 40.0f, 50.0f});
182     }
183 
184     @Test
testMeasure_BiDi3()185     public void testMeasure_BiDi3() {
186         final TextPaint paint = new TextPaint();
187         paint.setTypeface(TYPEFACE);
188         paint.setTextSize(10.0f);  // make 1em = 10px
189 
190         TextLine tl = getTextLine("\u05D0" + LRI + "\u05D0II" + PDI + "\u05D0", paint);
191         assertMeasurements(tl, 7, false,
192                 new float[]{0.0f, -10.0f, -30.0f, -30.0f, -20.0f, -40.0f, -40.0f, -50.0f});
193         assertMeasurements(tl, 7, true,
194                 new float[]{0.0f, -10.0f, -10.0f, -40.0f, -20.0f, -10.0f, -40.0f, -50.0f});
195     }
196 
197     @Test
testMeasure_Tab_LTR()198     public void testMeasure_Tab_LTR() {
199         final Object[] spans = { new TabStopSpan.Standard(100) };
200         final TabStops stops = new TabStops(100, spans);
201         final TextPaint paint = new TextPaint();
202         paint.setTypeface(TYPEFACE);
203         paint.setTextSize(10.0f);  // make 1em = 10px
204 
205         TextLine tl = getTextLine("II\tII", paint, stops);
206         assertMeasurements(tl, 5, false,
207                 new float[]{0.0f, 10.0f, 20.0f, 100.0f, 110.0f, 120.0f});
208         assertMeasurements(tl, 5, true,
209                 new float[]{0.0f, 10.0f, 20.0f, 100.0f, 110.0f, 120.0f});
210     }
211 
212     @Test
testMeasure_Tab_RTL()213     public void testMeasure_Tab_RTL() {
214         final Object[] spans = { new TabStopSpan.Standard(100) };
215         final TabStops stops = new TabStops(100, spans);
216         final TextPaint paint = new TextPaint();
217         paint.setTypeface(TYPEFACE);
218         paint.setTextSize(10.0f);  // make 1em = 10px
219 
220         TextLine tl = getTextLine("\u05D0\u05D0\t\u05D0\u05D0", paint, stops);
221         assertMeasurements(tl, 5, false,
222                 new float[]{0.0f, -10.0f, -20.0f, -100.0f, -110.0f, -120.0f});
223         assertMeasurements(tl, 5, true,
224                 new float[]{0.0f, -10.0f, -20.0f, -100.0f, -110.0f, -120.0f});
225     }
226 
227     @Test
testMeasure_Tab_BiDi()228     public void testMeasure_Tab_BiDi() {
229         final Object[] spans = { new TabStopSpan.Standard(100) };
230         final TabStops stops = new TabStops(100, spans);
231         final TextPaint paint = new TextPaint();
232         paint.setTypeface(TYPEFACE);
233         paint.setTextSize(10.0f);  // make 1em = 10px
234 
235         TextLine tl = getTextLine("I\u05D0\tI\u05D0", paint, stops);
236         assertMeasurements(tl, 5, false,
237                 new float[]{0.0f, 20.0f, 20.0f, 100.0f, 120.0f, 120.0f});
238         assertMeasurements(tl, 5, true,
239                 new float[]{0.0f, 10.0f, 10.0f, 100.0f, 110.0f, 110.0f});
240     }
241 
242     @Test
testMeasure_Tab_BiDi2()243     public void testMeasure_Tab_BiDi2() {
244         final Object[] spans = { new TabStopSpan.Standard(100) };
245         final TabStops stops = new TabStops(100, spans);
246         final TextPaint paint = new TextPaint();
247         paint.setTypeface(TYPEFACE);
248         paint.setTextSize(10.0f);  // make 1em = 10px
249 
250         TextLine tl = getTextLine("\u05D0I\t\u05D0I", paint, stops);
251         assertMeasurements(tl, 5, false,
252                 new float[]{0.0f, -20.0f, -20.0f, -100.0f, -120.0f, -120.0f});
253         assertMeasurements(tl, 5, true,
254                 new float[]{0.0f, -10.0f, -10.0f, -100.0f, -110.0f, -110.0f});
255     }
256 
257     @Test
testMeasure_wordSpacing()258     public void testMeasure_wordSpacing() {
259         final TextPaint paint = new TextPaint();
260         paint.setTypeface(TYPEFACE);
261         paint.setTextSize(10.0f);  // make 1em = 10px
262         paint.setWordSpacing(10.0f);
263 
264         TextLine tl = getTextLine("I I", paint);
265         assertMeasurements(tl, 3, false,
266                 new float[]{0.0f, 10.0f, 120.0f, 130.0f});
267     }
268 
269     @Test
testHandleRun_ellipsizedReplacementSpan_isSkipped()270     public void testHandleRun_ellipsizedReplacementSpan_isSkipped() {
271         final Spannable text = new SpannableStringBuilder("This is a... text");
272 
273         // Setup a replacement span that the measurement should not interact with.
274         final TestReplacementSpan span = new TestReplacementSpan();
275         text.setSpan(span, 9, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
276 
277         final TextLine tl = TextLine.obtain();
278         tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT,
279                 false /* hasTabs */, null /* tabStops */, 9, 12);
280         tl.measure(text.length(), false /* trailing */, null /* fmi */);
281 
282         assertFalse(span.mIsUsed);
283     }
284 
285     @Test
testHandleRun_notEllipsizedReplacementSpan_isNotSkipped()286     public void testHandleRun_notEllipsizedReplacementSpan_isNotSkipped() {
287         final Spannable text = new SpannableStringBuilder("This is a... text");
288 
289         // Setup a replacement span that the measurement should not interact with.
290         final TestReplacementSpan span = new TestReplacementSpan();
291         text.setSpan(span, 1, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
292 
293         final TextLine tl = TextLine.obtain();
294         tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT,
295                 false /* hasTabs */, null /* tabStops */, 9, 12);
296         tl.measure(text.length(), false /* trailing */, null /* fmi */);
297 
298         assertTrue(span.mIsUsed);
299     }
300 
301     @Test
testHandleRun_halfEllipsizedReplacementSpan_isNotSkipped()302     public void testHandleRun_halfEllipsizedReplacementSpan_isNotSkipped() {
303         final Spannable text = new SpannableStringBuilder("This is a... text");
304 
305         // Setup a replacement span that the measurement should not interact with.
306         final TestReplacementSpan span = new TestReplacementSpan();
307         text.setSpan(span, 7, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
308 
309         final TextLine tl = TextLine.obtain();
310         tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT,
311                 false /* hasTabs */, null /* tabStops */, 9, 12);
312         tl.measure(text.length(), false /* trailing */, null /* fmi */);
313         assertTrue(span.mIsUsed);
314     }
315 
316     private static class TestReplacementSpan extends ReplacementSpan {
317         boolean mIsUsed;
318 
319         @Override
getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm)320         public int getSize(Paint paint, CharSequence text, int start, int end,
321                 Paint.FontMetricsInt fm) {
322             mIsUsed = true;
323             return 0;
324         }
325 
326         @Override
draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint)327         public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top,
328                 int y,
329                 int bottom, Paint paint) {
330             mIsUsed = true;
331         }
332     }
333 }
334