1 /*
2  * Copyright (C) 2018 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.graphics.text;
18 
19 import com.android.layoutlib.bridge.impl.DelegateManager;
20 import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.icu.text.BreakIterator;
25 import android.text.Layout;
26 import android.text.Layout.BreakStrategy;
27 import android.text.Layout.HyphenationFrequency;
28 import android.graphics.text.Primitive.PrimitiveType;
29 
30 import java.text.CharacterIterator;
31 import java.util.ArrayList;
32 import java.util.List;
33 
34 import javax.swing.text.Segment;
35 import libcore.util.NativeAllocationRegistry_Delegate;
36 
37 /**
38  * Delegate that provides implementation for native methods in {@link android.text.StaticLayout}
39  * <p/>
40  * Through the layoutlib_create tool, selected methods of StaticLayout have been replaced
41  * by calls to methods of the same name in this delegate class.
42  *
43  */
44 public class LineBreaker_Delegate {
45 
46     private static final char CHAR_SPACE     = 0x20;
47     private static final char CHAR_TAB       = 0x09;
48     private static final char CHAR_NEWLINE   = 0x0A;
49     private static final char CHAR_ZWSP      = 0x200B;  // Zero width space.
50 
51     // ---- Builder delegate manager ----
52     private static final DelegateManager<Builder> sBuilderManager =
53         new DelegateManager<>(Builder.class);
54     private static long sFinalizer = -1;
55 
56     // ---- Result delegate manager ----
57     private static final DelegateManager<Result> sResultManager =
58         new DelegateManager<>(Result.class);
59     private static long sResultFinalizer = -1;
60 
61     @LayoutlibDelegate
nInit( @reakStrategy int breakStrategy, @HyphenationFrequency int hyphenationFrequency, boolean isJustified, @Nullable int[] indents)62     /*package*/ static long nInit(
63             @BreakStrategy int breakStrategy,
64             @HyphenationFrequency int hyphenationFrequency,
65             boolean isJustified,
66             @Nullable int[] indents) {
67         Builder builder = new Builder();
68         builder.mBreakStrategy = breakStrategy;
69         return sBuilderManager.addNewDelegate(builder);
70     }
71 
72     @LayoutlibDelegate
nGetReleaseFunc()73     /*package*/ static long nGetReleaseFunc() {
74         synchronized (MeasuredText_Delegate.class) {
75             if (sFinalizer == -1) {
76                 sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(
77                         sBuilderManager::removeJavaReferenceFor);
78             }
79         }
80         return sFinalizer;
81     }
82 
83     @LayoutlibDelegate
nComputeLineBreaks( long nativePtr, @NonNull char[] text, long measuredTextPtr, int length, float firstWidth, int firstWidthLineCount, float restWidth, @Nullable float[] variableTabStops, float defaultTabStop, int indentsOffset)84     /*package*/ static long nComputeLineBreaks(
85             /* non zero */ long nativePtr,
86 
87             // Inputs
88             @NonNull char[] text,
89             long measuredTextPtr,
90             int length,
91             float firstWidth,
92             int firstWidthLineCount,
93             float restWidth,
94             @Nullable float[] variableTabStops,
95             float defaultTabStop,
96             int indentsOffset) {
97         Builder builder = sBuilderManager.getDelegate(nativePtr);
98         if (builder == null) {
99             return 0;
100         }
101 
102         builder.mText = text;
103         builder.mWidths = new float[length];
104         builder.mLineWidth = new LineWidth(firstWidth, firstWidthLineCount, restWidth);
105         builder.mTabStopCalculator = new TabStops(variableTabStops, defaultTabStop);
106 
107         MeasuredText_Delegate.computeRuns(measuredTextPtr, builder);
108 
109         // compute all possible breakpoints.
110         BreakIterator it = BreakIterator.getLineInstance();
111         it.setText((CharacterIterator) new Segment(builder.mText, 0, length));
112 
113         // average word length in english is 5. So, initialize the possible breaks with a guess.
114         List<Integer> breaks = new ArrayList<Integer>((int) Math.ceil(length / 5d));
115         int loc;
116         it.first();
117         while ((loc = it.next()) != BreakIterator.DONE) {
118             breaks.add(loc);
119         }
120 
121         List<Primitive> primitives =
122                 computePrimitives(builder.mText, builder.mWidths, length, breaks);
123         switch (builder.mBreakStrategy) {
124             case Layout.BREAK_STRATEGY_SIMPLE:
125                 builder.mLineBreaker = new GreedyLineBreaker(primitives, builder.mLineWidth,
126                         builder.mTabStopCalculator);
127                 break;
128             case Layout.BREAK_STRATEGY_HIGH_QUALITY:
129                 // TODO
130 //                break;
131             case Layout.BREAK_STRATEGY_BALANCED:
132                 builder.mLineBreaker = new OptimizingLineBreaker(primitives, builder.mLineWidth,
133                         builder.mTabStopCalculator);
134                 break;
135             default:
136                 assert false : "Unknown break strategy: " + builder.mBreakStrategy;
137                 builder.mLineBreaker = new GreedyLineBreaker(primitives, builder.mLineWidth,
138                         builder.mTabStopCalculator);
139         }
140         Result result = new Result(builder.mLineBreaker.computeBreaks());
141         return sResultManager.addNewDelegate(result);
142     }
143 
144     @LayoutlibDelegate
nGetLineCount(long ptr)145     /*package*/ static int nGetLineCount(long ptr) {
146         Result result = sResultManager.getDelegate(ptr);
147         return result.mResult.mLineBreakOffset.size();
148     }
149 
150     @LayoutlibDelegate
nGetLineBreakOffset(long ptr, int idx)151     /*package*/ static int nGetLineBreakOffset(long ptr, int idx) {
152         Result result = sResultManager.getDelegate(ptr);
153         return result.mResult.mLineBreakOffset.get(idx);
154     }
155 
156     @LayoutlibDelegate
nGetLineWidth(long ptr, int idx)157     /*package*/ static float nGetLineWidth(long ptr, int idx) {
158         Result result = sResultManager.getDelegate(ptr);
159         return result.mResult.mLineWidths.get(idx);
160     }
161 
162     @LayoutlibDelegate
nGetLineAscent(long ptr, int idx)163     /*package*/ static float nGetLineAscent(long ptr, int idx) {
164         Result result = sResultManager.getDelegate(ptr);
165         return result.mResult.mLineAscents.get(idx);
166     }
167 
168     @LayoutlibDelegate
nGetLineDescent(long ptr, int idx)169     /*package*/ static float nGetLineDescent(long ptr, int idx) {
170         Result result = sResultManager.getDelegate(ptr);
171         return result.mResult.mLineDescents.get(idx);
172     }
173 
174     @LayoutlibDelegate
nGetLineFlag(long ptr, int idx)175     /*package*/ static int nGetLineFlag(long ptr, int idx) {
176         Result result = sResultManager.getDelegate(ptr);
177         return result.mResult.mLineFlags.get(idx);
178     }
179 
180     @LayoutlibDelegate
nGetReleaseResultFunc()181     /*package*/ static long nGetReleaseResultFunc() {
182         synchronized (MeasuredText_Delegate.class) {
183             if (sResultFinalizer == -1) {
184                 sResultFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(
185                         sBuilderManager::removeJavaReferenceFor);
186             }
187         }
188         return sResultFinalizer;
189     }
190 
191     /**
192      * Compute metadata each character - things which help in deciding if it's possible to break
193      * at a point or not.
194      */
195     @NonNull
computePrimitives(@onNull char[] text, @NonNull float[] widths, int length, @NonNull List<Integer> breaks)196     private static List<Primitive> computePrimitives(@NonNull char[] text, @NonNull float[] widths,
197             int length, @NonNull List<Integer> breaks) {
198         // Initialize the list with a guess of the number of primitives:
199         // 2 Primitives per non-whitespace char and approx 5 chars per word (i.e. 83% chars)
200         List<Primitive> primitives = new ArrayList<Primitive>(((int) Math.ceil(length * 1.833)));
201         int breaksSize = breaks.size();
202         int breakIndex = 0;
203         for (int i = 0; i < length; i++) {
204             char c = text[i];
205             if (c == CHAR_SPACE || c == CHAR_ZWSP) {
206                 primitives.add(PrimitiveType.GLUE.getNewPrimitive(i, widths[i]));
207             } else if (c == CHAR_TAB) {
208                 primitives.add(PrimitiveType.VARIABLE.getNewPrimitive(i));
209             } else if (c != CHAR_NEWLINE) {
210                 while (breakIndex < breaksSize && breaks.get(breakIndex) < i) {
211                     breakIndex++;
212                 }
213                 Primitive p;
214                 if (widths[i] != 0) {
215                     if (breakIndex < breaksSize && breaks.get(breakIndex) == i) {
216                         p = PrimitiveType.PENALTY.getNewPrimitive(i, 0, 0);
217                     } else {
218                         p = PrimitiveType.WORD_BREAK.getNewPrimitive(i, 0);
219                     }
220                     primitives.add(p);
221                 }
222 
223                 primitives.add(PrimitiveType.BOX.getNewPrimitive(i, widths[i]));
224             }
225         }
226         // final break at end of everything
227         primitives.add(
228                 PrimitiveType.PENALTY.getNewPrimitive(length, 0, -PrimitiveType.PENALTY_INFINITY));
229         return primitives;
230     }
231 
232     // TODO: Rename to LineBreakerRef and move everything other than LineBreaker to LineBreaker.
233     /**
234      * Java representation of the native Builder class.
235      */
236     public static class Builder {
237         char[] mText;
238         float[] mWidths;
239         private BaseLineBreaker mLineBreaker;
240         private int mBreakStrategy;
241         private LineWidth mLineWidth;
242         private TabStops mTabStopCalculator;
243     }
244 
245     public abstract static class Run {
246         int mStart;
247         int mEnd;
248 
Run(int start, int end)249         Run(int start, int end) {
250             mStart = start;
251             mEnd = end;
252         }
253 
addTo(Builder builder)254         abstract void addTo(Builder builder);
255     }
256 
257     public static class Result {
258         final BaseLineBreaker.Result mResult;
Result(BaseLineBreaker.Result result)259         public Result(BaseLineBreaker.Result result) {
260             mResult = result;
261         }
262     }
263 }
264