| // 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); |
| } |
| } |
| } |