| 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 |