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 android.annotation.NonNull;
20 import android.graphics.text.Primitive.PrimitiveType;
21 
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.List;
25 import java.util.ListIterator;
26 
27 import static android.graphics.text.Primitive.PrimitiveType.PENALTY_INFINITY;
28 
29 
30 // Based on the native implementation of OptimizingLineBreaker in
31 // frameworks/base/core/jni/android_text_StaticLayout.cpp revision b808260
32 /**
33  * A more complex version of line breaking where we try to prevent the right edge from being too
34  * jagged.
35  */
36 public class OptimizingLineBreaker extends BaseLineBreaker {
37 
OptimizingLineBreaker(@onNull List<Primitive> primitives, @NonNull LineWidth lineWidth, @NonNull TabStops tabStops)38     public OptimizingLineBreaker(@NonNull List<Primitive> primitives, @NonNull LineWidth lineWidth,
39             @NonNull TabStops tabStops) {
40         super(primitives, lineWidth, tabStops);
41     }
42 
43     @Override
computeBreaks()44     public Result computeBreaks() {
45         Result result = new Result();
46         int numBreaks = mPrimitives.size();
47         assert numBreaks > 0;
48         if (numBreaks == 1) {
49             // This can be true only if it's an empty paragraph.
50             Primitive p = mPrimitives.get(0);
51             assert p.type == PrimitiveType.PENALTY;
52             result.mLineBreakOffset.add(0);
53             result.mLineWidths.add(p.width);
54             result.mLineAscents.add(0f);
55             result.mLineDescents.add(0f);
56             result.mLineFlags.add(0);
57             return result;
58         }
59         Node[] opt = new Node[numBreaks];
60         opt[0] = new Node(-1, 0, 0, 0, false);
61         opt[numBreaks - 1] = new Node(-1, 0, 0, 0, false);
62 
63         ArrayList<Integer> active = new ArrayList<Integer>();
64         active.add(0);
65         int lastBreak = 0;
66         for (int i = 0; i < numBreaks; i++) {
67             Primitive p = mPrimitives.get(i);
68             if (p.type == PrimitiveType.PENALTY) {
69                 boolean finalBreak = (i + 1 == numBreaks);
70                 Node bestBreak = null;
71 
72                 for (ListIterator<Integer> it = active.listIterator(); it.hasNext();
73                         /* incrementing done in loop */) {
74                     int pos = it.next();
75                     int lines = opt[pos].mPrevCount;
76                     float maxWidth = mLineWidth.getLineWidth(lines);
77                     // we have to compute metrics every time --
78                     // we can't really pre-compute this stuff and just deal with breaks
79                     // because of the way tab characters work, this makes it computationally
80                     // harder, but this way, we can still optimize while treating tab characters
81                     // correctly
82                     LineMetrics lineMetrics = computeMetrics(pos, i);
83                     if (lineMetrics.mPrintedWidth <= maxWidth) {
84                         float demerits = computeDemerits(maxWidth, lineMetrics.mPrintedWidth,
85                                 finalBreak, p.penalty) + opt[pos].mDemerits;
86                         if (bestBreak == null || demerits < bestBreak.mDemerits) {
87                             if (bestBreak == null) {
88                                 bestBreak = new Node(pos, opt[pos].mPrevCount + 1, demerits,
89                                         lineMetrics.mPrintedWidth, lineMetrics.mHasTabs);
90                             } else {
91                                 bestBreak.mPrev = pos;
92                                 bestBreak.mPrevCount = opt[pos].mPrevCount + 1;
93                                 bestBreak.mDemerits = demerits;
94                                 bestBreak.mWidth = lineMetrics.mPrintedWidth;
95                                 bestBreak.mHasTabs = lineMetrics.mHasTabs;
96                             }
97                         }
98                     } else {
99                         it.remove();
100                     }
101                 }
102                 if (p.penalty == -PENALTY_INFINITY) {
103                     active.clear();
104                 }
105                 if (bestBreak != null) {
106                     opt[i] = bestBreak;
107                     active.add(i);
108                     lastBreak = i;
109                 }
110                 if (active.isEmpty()) {
111                     // we can't give up!
112                     LineMetrics lineMetrics = new LineMetrics();
113                     int lines = opt[lastBreak].mPrevCount;
114                     float maxWidth = mLineWidth.getLineWidth(lines);
115                     int breakIndex = desperateBreak(lastBreak, numBreaks, maxWidth, lineMetrics);
116                     opt[breakIndex] = new Node(lastBreak, lines + 1, 0 /*doesn't matter*/,
117                             lineMetrics.mWidth, lineMetrics.mHasTabs);
118                     active.add(breakIndex);
119                     lastBreak = breakIndex;
120                     i = breakIndex; // incremented by i++
121                 }
122             }
123         }
124 
125         int idx = numBreaks - 1;
126         while (opt[idx].mPrev != -1) {
127             result.mLineBreakOffset.add(mPrimitives.get(idx).location);
128             result.mLineWidths.add(opt[idx].mWidth);
129             result.mLineAscents.add(0f);
130             result.mLineDescents.add(0f);
131             result.mLineFlags.add(opt[idx].mHasTabs ? TAB_MASK : 0);
132             idx = opt[idx].mPrev;
133         }
134 
135         Collections.reverse(result.mLineBreakOffset);
136         Collections.reverse(result.mLineWidths);
137         Collections.reverse(result.mLineAscents);
138         Collections.reverse(result.mLineDescents);
139         Collections.reverse(result.mLineFlags);
140         return result;
141     }
142 
143     @NonNull
computeMetrics(int start, int end)144     private LineMetrics computeMetrics(int start, int end) {
145         boolean f = false;
146         float w = 0, pw = 0;
147         for (int i = start; i < end; i++) {
148             Primitive p = mPrimitives.get(i);
149             if (p.type == PrimitiveType.BOX || p.type == PrimitiveType.GLUE) {
150                 w += p.width;
151                 if (p.type == PrimitiveType.BOX) {
152                     pw = w;
153                 }
154             } else if (p.type == PrimitiveType.VARIABLE) {
155                 w = mTabStops.width(w);
156                 f = true;
157             }
158         }
159         return new LineMetrics(w, pw, f);
160     }
161 
computeDemerits(float maxWidth, float width, boolean finalBreak, float penalty)162     private static float computeDemerits(float maxWidth, float width, boolean finalBreak,
163             float penalty) {
164         float deviation = finalBreak ? 0 : maxWidth - width;
165         return (deviation * deviation) + penalty;
166     }
167 
168     /**
169      * @return the last break position or -1 if failed.
170      */
171     @SuppressWarnings("ConstantConditions")  // method too complex to be analyzed.
desperateBreak(int start, int limit, float maxWidth, @NonNull LineMetrics lineMetrics)172     private int desperateBreak(int start, int limit, float maxWidth,
173             @NonNull LineMetrics lineMetrics) {
174         float w = 0, pw = 0;
175         boolean breakFound = false;
176         int breakIndex = 0, firstTabIndex = Integer.MAX_VALUE;
177         for (int i = start; i < limit; i++) {
178             Primitive p = mPrimitives.get(i);
179 
180             if (p.type == PrimitiveType.BOX || p.type == PrimitiveType.GLUE) {
181                 w += p.width;
182                 if (p.type == PrimitiveType.BOX) {
183                     pw = w;
184                 }
185             } else if (p.type == PrimitiveType.VARIABLE) {
186                 w = mTabStops.width(w);
187                 firstTabIndex = Math.min(firstTabIndex, i);
188             }
189 
190             if (pw > maxWidth && breakFound) {
191                 break;
192             }
193 
194             // must make progress
195             if (i > start &&
196                     (p.type == PrimitiveType.PENALTY || p.type == PrimitiveType.WORD_BREAK)) {
197                 breakFound = true;
198                 breakIndex = i;
199             }
200         }
201 
202         if (breakFound) {
203             lineMetrics.mWidth = w;
204             lineMetrics.mPrintedWidth = pw;
205             lineMetrics.mHasTabs = (start <= firstTabIndex && firstTabIndex < breakIndex);
206             return breakIndex;
207         } else {
208             return -1;
209         }
210     }
211 
212     private static class LineMetrics {
213         /** Actual width of the line. */
214         float mWidth;
215         /** Width of the line minus trailing whitespace. */
216         float mPrintedWidth;
217         boolean mHasTabs;
218 
LineMetrics()219         public LineMetrics() {
220         }
221 
LineMetrics(float width, float printedWidth, boolean hasTabs)222         public LineMetrics(float width, float printedWidth, boolean hasTabs) {
223             mWidth = width;
224             mPrintedWidth = printedWidth;
225             mHasTabs = hasTabs;
226         }
227     }
228 
229     /**
230      * A struct to store the info about a break.
231      */
232     @SuppressWarnings("SpellCheckingInspection")  // For the word struct.
233     private static class Node {
234         // -1 for the first node.
235         int mPrev;
236         // number of breaks so far.
237         int mPrevCount;
238         float mDemerits;
239         float mWidth;
240         boolean mHasTabs;
241 
Node(int prev, int prevCount, float demerits, float width, boolean hasTabs)242         public Node(int prev, int prevCount, float demerits, float width, boolean hasTabs) {
243             mPrev = prev;
244             mPrevCount = prevCount;
245             mDemerits = demerits;
246             mWidth = width;
247             mHasTabs = hasTabs;
248         }
249     }
250 }
251