OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 // TODO(jacobr): handle splitting lines on symbols such as '-' that aren't | |
6 // whitespace but are valid word breaking points. | |
7 /** | |
8 * Utility class to efficiently word break and measure text without requiring | |
9 * access to the DOM. | |
10 */ | |
11 class MeasureText { | |
12 static CanvasRenderingContext2D _context; | |
13 | |
14 final String font; | |
15 num _spaceLength; | |
16 num _typicalCharLength; | |
17 | |
18 static final String ELLIPSIS = '...'; | |
19 | |
20 MeasureText(this.font) { | |
21 if (_context === null) { | |
22 CanvasElement canvas = new Element.tag('canvas'); | |
23 _context = canvas.getContext('2d'); | |
24 } | |
25 if (_spaceLength === null) { | |
26 _context.font = font; | |
27 _spaceLength = _context.measureText(' ').width; | |
28 _typicalCharLength = _context.measureText('k').width; | |
29 } | |
30 } | |
31 | |
32 // TODO(jacobr): we are DOA for i18N... | |
33 // the right solution is for the server to send us text perparsed into words | |
34 // perhaps even with hints on the guess for the correct breaks so on the | |
35 // client all we have to do is verify and fix errors rather than perform the | |
36 // full calculation. | |
37 static bool isWhitespace(String character) { | |
38 return character == ' ' || character == '\t' || character == '\n'; | |
39 } | |
40 | |
41 num get typicalCharLength() { | |
42 return _typicalCharLength; | |
43 } | |
44 | |
45 String quickTruncate(String text, num lineWidth, int maxLines) { | |
46 int targetLength = (lineWidth * maxLines / _typicalCharLength).toInt(); | |
47 // Advance to next word break point. | |
48 while(targetLength < text.length && !isWhitespace(text[targetLength])) { | |
49 targetLength++; | |
50 } | |
51 | |
52 if (targetLength < text.length) { | |
53 return text.substring(0, targetLength) + ELLIPSIS; | |
54 } else { | |
55 return text; | |
56 } | |
57 } | |
58 | |
59 /** | |
60 * Add line broken text as html separated by <br> elements. | |
61 * Returns the number of lines in the output. | |
62 * This function is safe to call with [:sb === null:] in which case just the | |
63 * line count is returned. | |
64 */ | |
65 int addLineBrokenText(StringBuffer sb, String text, num lineWidth, | |
66 int maxLines) { | |
67 // Strip surrounding whitespace. This ensures we create zero lines if there | |
68 // is no visible text. | |
69 text = text.trim(); | |
70 | |
71 // We can often avoid performing a full line break calculation when only | |
72 // the number of lines and not the actual linebreaks is required. | |
73 if (sb === null) { | |
74 _context.font = font; | |
75 int textWidth = _context.measureText(text).width.toInt(); | |
76 // By the pigeon hole principle, the resulting text will require at least | |
77 // maxLines if the raw text is longer than the amount of text that will | |
78 // fit on maxLines - 1. We add the length of a whitespace | |
79 // character to the lineWidth as each line is separated by a whitespace | |
80 // character. We assume all whitespace characters have the same length. | |
81 if (textWidth >= (lineWidth + _spaceLength) * (maxLines - 1)) { | |
82 return maxLines; | |
83 } else if (textWidth == 0) { | |
84 return 0; | |
85 } else if (textWidth < lineWidth) { | |
86 return 1; | |
87 } | |
88 // Fall through to the regular line breaking calculation as the number | |
89 // of lines required is unclear. | |
90 } | |
91 int lines = 0; | |
92 lineBreak(text, lineWidth, maxLines, (int start, int end, num width) { | |
93 lines++; | |
94 if (lines == maxLines) { | |
95 // Overflow case... there may be more lines of text than we can handle. | |
96 // Add a few characters to the last line so that the browser will | |
97 // render ellipses correctly. | |
98 // TODO(jacobr): make this optional and only add characters until | |
99 // the first whitespace character encountered. | |
100 end = Math.min(end + 50, text.length); | |
101 } | |
102 if (sb !== null) { | |
103 if (lines > 1) { | |
104 sb.add('<br>'); | |
105 } | |
106 // TODO(jacobr): HTML escape this text. | |
107 sb.add(text.substring(start, end)); | |
108 } | |
109 }); | |
110 return lines; | |
111 } | |
112 | |
113 void lineBreak(String text, num lineWidth, int maxLines, | |
114 Function callback) { | |
115 _context.font = font; | |
116 int lines = 0; | |
117 num currentLength = 0; | |
118 int startIndex = 0; | |
119 int wordStartIndex = null; | |
120 int lastWordEndIndex = null; | |
121 bool lastWhitespace = true; | |
122 // TODO(jacobr): optimize this further. | |
123 // To simplify the logic, we simulate injecting a whitespace character | |
124 // at the end of the string. | |
125 for (int i = 0, len = text.length; i <= len; i++) { | |
126 // Treat the char after the end of the string as whitespace. | |
127 bool whitespace = i == len || isWhitespace(text[i]); | |
128 if (whitespace && !lastWhitespace) { | |
129 num wordLength = _context.measureText(text.substring( | |
130 wordStartIndex, i)).width; | |
131 // TODO(jimhug): Replace the line above with this one to workaround | |
132 // dartium bug - error: unimplemented code | |
133 // num wordLength = (i - wordStartIndex) * 17; | |
134 currentLength += wordLength; | |
135 if (currentLength > lineWidth) { | |
136 // Edge case: | |
137 // It could be the very first word we ran into was too long for a | |
138 // line in which case we let it have its own line. | |
139 if (lastWordEndIndex !== null) { | |
140 lines++; | |
141 callback(startIndex, lastWordEndIndex, currentLength - wordLength); | |
142 } | |
143 if (lines == maxLines) { | |
144 return; | |
145 } | |
146 startIndex = wordStartIndex; | |
147 currentLength = wordLength; | |
148 } | |
149 lastWordEndIndex = i; | |
150 currentLength += _spaceLength; | |
151 wordStartIndex = null; | |
152 } else if (wordStartIndex === null && !whitespace) { | |
153 wordStartIndex = i; | |
154 } | |
155 lastWhitespace = whitespace; | |
156 } | |
157 if (currentLength > 0) { | |
158 callback(startIndex, text.length, currentLength); | |
159 } | |
160 } | |
161 } | |
OLD | NEW |