blob: 7e768ee2bfd136bc67b7bf8747a9073bceea6c3c [file] [log] [blame]
// 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);
}
}
}