|  | // Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file | 
|  | // for details. All rights reserved. Use of this source code is governed by a | 
|  | // BSD-style license that can be found in the LICENSE file. | 
|  |  | 
|  | // @dart = 2.9 | 
|  |  | 
|  | part of view; | 
|  |  | 
|  | // TODO(jacobr): handle splitting lines on symbols such as '-' that aren't | 
|  | // whitespace but are valid word breaking points. | 
|  | /** | 
|  | * Utility class to efficiently word break and measure text without requiring | 
|  | * access to the DOM. | 
|  | */ | 
|  | class MeasureText { | 
|  | static CanvasRenderingContext2D _context; | 
|  |  | 
|  | final String font; | 
|  | num _spaceLength; | 
|  | num _typicalCharLength; | 
|  |  | 
|  | static const String ELLIPSIS = '...'; | 
|  |  | 
|  | MeasureText(this.font) { | 
|  | if (_context == null) { | 
|  | CanvasElement canvas = new Element.tag('canvas'); | 
|  | _context = canvas.getContext('2d'); | 
|  | } | 
|  | if (_spaceLength == null) { | 
|  | _context.font = font; | 
|  | _spaceLength = _context.measureText(' ').width; | 
|  | _typicalCharLength = _context.measureText('k').width; | 
|  | } | 
|  | } | 
|  |  | 
|  | // TODO(jacobr): we are DOA for i18N... | 
|  | // the right solution is for the server to send us text perparsed into words | 
|  | // perhaps even with hints on the guess for the correct breaks so on the | 
|  | // client all we have to do is verify and fix errors rather than perform the | 
|  | // full calculation. | 
|  | static bool isWhitespace(String character) { | 
|  | return character == ' ' || character == '\t' || character == '\n'; | 
|  | } | 
|  |  | 
|  | num get typicalCharLength { | 
|  | return _typicalCharLength; | 
|  | } | 
|  |  | 
|  | String quickTruncate(String text, num lineWidth, int maxLines) { | 
|  | int targetLength = lineWidth * maxLines ~/ _typicalCharLength; | 
|  | // Advance to next word break point. | 
|  | while (targetLength < text.length && !isWhitespace(text[targetLength])) { | 
|  | targetLength++; | 
|  | } | 
|  |  | 
|  | if (targetLength < text.length) { | 
|  | return '${text.substring(0, targetLength)}$ELLIPSIS'; | 
|  | } else { | 
|  | return text; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Add line broken text as html separated by <br> elements. | 
|  | * Returns the number of lines in the output. | 
|  | * This function is safe to call with [:sb == null:] in which case just the | 
|  | * line count is returned. | 
|  | */ | 
|  | int addLineBrokenText( | 
|  | StringBuffer sb, String text, num lineWidth, int maxLines) { | 
|  | // Strip surrounding whitespace. This ensures we create zero lines if there | 
|  | // is no visible text. | 
|  | text = text.trim(); | 
|  |  | 
|  | // We can often avoid performing a full line break calculation when only | 
|  | // the number of lines and not the actual linebreaks is required. | 
|  | if (sb == null) { | 
|  | _context.font = font; | 
|  | int textWidth = _context.measureText(text).width.toInt(); | 
|  | // By the pigeon hole principle, the resulting text will require at least | 
|  | // maxLines if the raw text is longer than the amount of text that will | 
|  | // fit on maxLines - 1.  We add the length of a whitespace | 
|  | // character to the lineWidth as each line is separated by a whitespace | 
|  | // character. We assume all whitespace characters have the same length. | 
|  | if (textWidth >= (lineWidth + _spaceLength) * (maxLines - 1)) { | 
|  | return maxLines; | 
|  | } else if (textWidth == 0) { | 
|  | return 0; | 
|  | } else if (textWidth < lineWidth) { | 
|  | return 1; | 
|  | } | 
|  | // Fall through to the regular line breaking calculation as the number | 
|  | // of lines required is unclear. | 
|  | } | 
|  | int lines = 0; | 
|  | lineBreak(text, lineWidth, maxLines, (int start, int end, num width) { | 
|  | lines++; | 
|  | if (lines == maxLines) { | 
|  | // Overflow case... there may be more lines of text than we can handle. | 
|  | // Add a few characters to the last line so that the browser will | 
|  | // render ellipses correctly. | 
|  | // TODO(jacobr): make this optional and only add characters until | 
|  | // the first whitespace character encountered. | 
|  | end = Math.min(end + 50, text.length); | 
|  | } | 
|  | if (sb != null) { | 
|  | if (lines > 1) { | 
|  | sb.write('<br>'); | 
|  | } | 
|  | // TODO(jacobr): HTML escape this text. | 
|  | sb.write(text.substring(start, end)); | 
|  | } | 
|  | }); | 
|  | return lines; | 
|  | } | 
|  |  | 
|  | void lineBreak(String text, num lineWidth, int maxLines, Function callback) { | 
|  | _context.font = font; | 
|  | int lines = 0; | 
|  | num currentLength = 0; | 
|  | int startIndex = 0; | 
|  | int wordStartIndex = null; | 
|  | int lastWordEndIndex = null; | 
|  | bool lastWhitespace = true; | 
|  | // TODO(jacobr): optimize this further. | 
|  | // To simplify the logic, we simulate injecting a whitespace character | 
|  | // at the end of the string. | 
|  | for (int i = 0, len = text.length; i <= len; i++) { | 
|  | // Treat the char after the end of the string as whitespace. | 
|  | bool whitespace = i == len || isWhitespace(text[i]); | 
|  | if (whitespace && !lastWhitespace) { | 
|  | num wordLength = | 
|  | _context.measureText(text.substring(wordStartIndex, i)).width; | 
|  | // TODO(jimhug): Replace the line above with this one to workaround | 
|  | //               dartium bug - error: unimplemented code | 
|  | // num wordLength = (i - wordStartIndex) * 17; | 
|  | currentLength += wordLength; | 
|  | if (currentLength > lineWidth) { | 
|  | // Edge case: | 
|  | // It could be the very first word we ran into was too long for a | 
|  | // line in which case  we let it have its own line. | 
|  | if (lastWordEndIndex != null) { | 
|  | lines++; | 
|  | callback(startIndex, lastWordEndIndex, currentLength - wordLength); | 
|  | } | 
|  | if (lines == maxLines) { | 
|  | return; | 
|  | } | 
|  | startIndex = wordStartIndex; | 
|  | currentLength = wordLength; | 
|  | } | 
|  | lastWordEndIndex = i; | 
|  | currentLength += _spaceLength; | 
|  | wordStartIndex = null; | 
|  | } else if (wordStartIndex == null && !whitespace) { | 
|  | wordStartIndex = i; | 
|  | } | 
|  | lastWhitespace = whitespace; | 
|  | } | 
|  | if (currentLength > 0) { | 
|  | callback(startIndex, text.length, currentLength); | 
|  | } | 
|  | } | 
|  | } |