Files
sdk/client/view/MeasureText.dart
T
jacobr@google.com 2af03fc05b Enhanced performance by removing unused UI, reducing the # of absolutely positioned elements thus reducing the # of render layers, and increasing the amount of logic performed using string concat instead of DOM manipulation.
Also added back a bugfix  change I'd thought I'd landed to HTMLDocument,

Added some significant polish and missing features to the keyboard shortcuts
by correctly taking into account that some views may be partially visible.

New hotness available at:
http://dart.googleplex.com/jacobr-swarm-js.html

I'm finally not embarrassed by the performance when scrolling between pages of views on the iPad.  Not quite 60fps but close enough to 30fps under the worst conditions scrolling horizontally.

BUG=
TEST=

Review URL: https://chromereviews.googleplex.com/3543015

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@165 260f80e4-7a28-3924-810f-c04153c831b5
2011-10-06 22:15:50 +00:00

162 lines
5.8 KiB
Dart

// 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.
// 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 final 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).toInt();
// 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.add('<br>');
}
// TODO(jacobr): HTML escape this text.
sb.add(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);
}
}
}