[web] Fix text cutoff when rendering paragraphs on DomCanvas (flutter/engine#23638)

This commit is contained in:
Mouad Debbar 2021-01-13 13:39:01 -08:00 committed by GitHub
parent 5026c62732
commit 470c7befac
6 changed files with 176 additions and 44 deletions

View File

@ -254,9 +254,6 @@ html.Element _drawParagraphElement(
assert(paragraph.isLaidOut);
final html.HtmlElement paragraphElement = paragraph.toDomElement();
paragraphElement.style
..height = '${paragraph.height}px'
..width = '${paragraph.width}px';
if (transform != null) {
setElementTransform(

View File

@ -96,6 +96,7 @@ class CanvasParagraph implements EngineParagraph {
isLaidOut = true;
_lastUsedConstraints = constraints;
_cachedDomElement = null;
}
// TODO(mdebbar): Returning true means we always require a bitmap canvas. Revisit
@ -115,6 +116,7 @@ class CanvasParagraph implements EngineParagraph {
@override
html.HtmlElement toDomElement() {
assert(isLaidOut);
final html.HtmlElement? domElement = _cachedDomElement;
if (domElement == null) {
return _cachedDomElement ??= _createDomElement();
@ -123,49 +125,73 @@ class CanvasParagraph implements EngineParagraph {
}
html.HtmlElement _createDomElement() {
final html.HtmlElement element =
final html.HtmlElement rootElement =
domRenderer.createElement('p') as html.HtmlElement;
// 1. Set paragraph-level styles.
_applyParagraphStyleToElement(element: element, style: paragraphStyle);
final html.CssStyleDeclaration cssStyle = element.style;
_applyParagraphStyleToElement(element: rootElement, style: paragraphStyle);
final html.CssStyleDeclaration cssStyle = rootElement.style;
cssStyle
..position = 'absolute'
..whiteSpace = 'pre-wrap'
..overflowWrap = 'break-word';
// Prevent the browser from doing any line breaks in the paragraph. We want
// to insert our own <BR> breaks based on layout results.
..whiteSpace = 'pre';
if (paragraphStyle._maxLines != null || paragraphStyle._ellipsis != null) {
cssStyle..overflowY = 'hidden';
cssStyle
..overflowY = 'hidden'
..height = '${height}px';
}
if (paragraphStyle._ellipsis != null &&
(paragraphStyle._maxLines == null || paragraphStyle._maxLines == 1)) {
cssStyle
..width = '${width}px'
..overflowX = 'hidden'
..whiteSpace = 'pre'
..textOverflow = 'ellipsis';
}
// 2. Append all spans to the paragraph.
for (final ParagraphSpan span in spans) {
if (span is FlatTextSpan) {
final html.HtmlElement spanElement =
domRenderer.createElement('span') as html.HtmlElement;
_applyTextStyleToElement(
element: spanElement,
style: span.style,
isSpan: true,
);
domRenderer.appendText(spanElement, span.textOf(this));
domRenderer.append(element, spanElement);
} else if (span is PlaceholderSpan) {
domRenderer.append(
element,
_createPlaceholderElement(placeholder: span),
);
ParagraphSpan? span;
late html.HtmlElement element;
final List<EngineLineMetrics> lines = computeLineMetrics();
for (int i = 0; i < lines.length; i++) {
// Insert a <BR> element before each line except the first line.
if (i > 0) {
domRenderer.append(element, domRenderer.createElement('br'));
}
for (final RangeBox box in lines[i].boxes!) {
if (box is SpanBox) {
if (box.span != span) {
span = box.span;
element = domRenderer.createElement('span') as html.HtmlElement;
_applyTextStyleToElement(
element: element,
style: box.span.style,
isSpan: true
);
domRenderer.append(rootElement, element);
}
domRenderer.appendText(element, box.toText());
} else if (box is PlaceholderBox) {
span = box.placeholder;
// If there's a line-end after this placeholder, we want the <BR> to
// be inserted in the root paragraph element.
element = rootElement;
domRenderer.append(
rootElement,
_createPlaceholderElement(placeholder: box.placeholder),
);
} else {
throw UnimplementedError('Unknown box type: ${box.runtimeType}');
}
}
}
return element;
return rootElement;
}
@override

View File

@ -491,6 +491,13 @@ class SpanBox extends RangeBox {
return startIndex < this.end.index && this.start.index < endIndex;
}
/// Returns the substring of the paragraph that's represented by this box.
///
/// Trailing newlines are omitted, if any.
String toText() {
return spanometer.paragraph.toPlainText().substring(start.index, end.indexWithoutTrailingNewlines);
}
/// Returns a [ui.TextBox] representing this range box in the given [line].
///
/// The coordinates of the resulting [ui.TextBox] are relative to the

View File

@ -480,6 +480,8 @@ class DomParagraph implements EngineParagraph {
final html.CssStyleDeclaration paragraphStyle = paragraphElement.style;
paragraphStyle
..height = '${height}px'
..width = '${width}px'
..position = 'absolute'
..whiteSpace = 'pre-wrap'
..overflowWrap = 'break-word'

View File

@ -547,8 +547,8 @@ void _testCullRectComputation() {
'renders clipped text with high quality',
() async {
// To reproduce blurriness we need real clipping.
final Paragraph paragraph =
(ParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto'))..addText('Am I blurry?')).build();
final DomParagraph paragraph =
(DomParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto'))..addText('Am I blurry?')).build();
paragraph.layout(const ParagraphConstraints(width: 1000));
final Rect canvasSize = Rect.fromLTRB(

View File

@ -8,17 +8,30 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
const String paragraphStyle =
'font-family: sans-serif; position: absolute; white-space: pre-wrap; overflow-wrap: break-word;';
bool get isIosSafari => browserEngine == BrowserEngine.webkit &&
operatingSystem == OperatingSystem.iOs;
String get defaultFontFamily {
String fontFamily = canonicalizeFontFamily('Ahem')!;
if (browserEngine == BrowserEngine.firefox) {
fontFamily = fontFamily.replaceAll('"', '&quot;');
} else if (browserEngine == BrowserEngine.blink || browserEngine == BrowserEngine.webkit) {
fontFamily = fontFamily.replaceAll('"', '');
}
return 'font-family: $fontFamily;';
}
const String defaultColor = 'color: rgb(255, 0, 0);';
const String defaultFontFamily = 'font-family: sans-serif;';
const String defaultFontSize = 'font-size: 14px;';
final String paragraphStyle =
'$defaultFontFamily position: absolute; white-space: pre;';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
void testMain() async {
await webOnlyInitializeTestDomRenderer();
setUpAll(() {
WebExperiments.ensureInitialized();
});
@ -32,6 +45,9 @@ void testMain() {
final CanvasParagraph paragraph = builder.build();
expect(paragraph.paragraphStyle, style);
expect(paragraph.toPlainText(), 'Hello');
expect(paragraph.spans, hasLength(1));
paragraph.layout(ParagraphConstraints(width: double.infinity));
expect(
paragraph.toDomElement().outerHtml,
'<p style="font-size: 13px; $paragraphStyle">'
@ -40,7 +56,17 @@ void testMain() {
'</span>'
'</p>',
);
expect(paragraph.spans, hasLength(1));
// Should break "Hello" into "Hel" and "lo".
paragraph.layout(ParagraphConstraints(width: 39.0));
expect(
paragraph.toDomElement().outerHtml,
'<p style="font-size: 13px; $paragraphStyle">'
'<span style="$defaultColor font-size: 13px; $defaultFontFamily">'
'Hel<br>lo'
'</span>'
'</p>',
);
final ParagraphSpan span = paragraph.spans.single;
expect(span, isA<FlatTextSpan>());
@ -58,11 +84,13 @@ void testMain() {
final CanvasParagraph paragraph = builder.build();
expect(paragraph.paragraphStyle, style);
expect(paragraph.toPlainText(), 'Hello');
expect(paragraph.spans, hasLength(1));
paragraph.layout(ParagraphConstraints(width: double.infinity));
expect(
paragraph.toDomElement().outerHtml,
'<p style="$paragraphStyle"><span style="$defaultColor $defaultFontSize $defaultFontFamily">Hello</span></p>',
);
expect(paragraph.spans, hasLength(1));
final FlatTextSpan textSpan = paragraph.spans.single as FlatTextSpan;
expect(textSpan.style, styleWithDefaults());
@ -77,9 +105,16 @@ void testMain() {
final CanvasParagraph paragraph = builder.build();
expect(paragraph.paragraphStyle, style);
expect(paragraph.toPlainText(), 'Hello');
double expectedHeight = 14.0;
if (isIosSafari) {
// On iOS Safari, the height measurement is one extra pixel.
expectedHeight++;
}
paragraph.layout(ParagraphConstraints(width: double.infinity));
expect(
paragraph.toDomElement().outerHtml,
'<p style="$paragraphStyle overflow-y: hidden;">'
'<p style="$paragraphStyle overflow-y: hidden; height: ${expectedHeight}px;">'
'<span style="$defaultColor $defaultFontSize $defaultFontFamily">'
'Hello'
'</span>'
@ -96,13 +131,16 @@ void testMain() {
final CanvasParagraph paragraph = builder.build();
expect(paragraph.paragraphStyle, style);
expect(paragraph.toPlainText(), 'Hello');
final String expectedParagraphStyle = paragraphStyle.replaceFirst(
'white-space: pre-wrap',
'white-space: pre',
);
double expectedHeight = 14.0;
if (isIosSafari) {
// On iOS Safari, the height measurement is one extra pixel.
expectedHeight++;
}
paragraph.layout(ParagraphConstraints(width: double.infinity));
expect(
paragraph.toDomElement().outerHtml,
'<p style="$expectedParagraphStyle overflow: hidden; text-overflow: ellipsis;">'
'<p style="$paragraphStyle overflow: hidden; height: ${expectedHeight}px; text-overflow: ellipsis;">'
'<span style="$defaultColor $defaultFontSize $defaultFontFamily">'
'Hello'
'</span>'
@ -125,6 +163,9 @@ void testMain() {
final CanvasParagraph paragraph = builder.build();
expect(paragraph.toPlainText(), 'Hello');
expect(paragraph.spans, hasLength(1));
paragraph.layout(ParagraphConstraints(width: double.infinity));
expect(
paragraph.toDomElement().outerHtml,
'<p style="line-height: 1.5; font-size: 13px; $paragraphStyle">'
@ -133,7 +174,6 @@ void testMain() {
'</span>'
'</p>',
);
expect(paragraph.spans, hasLength(1));
final FlatTextSpan span = paragraph.spans.single as FlatTextSpan;
expect(span.textOf(paragraph), 'Hello');
@ -161,6 +201,9 @@ void testMain() {
final CanvasParagraph paragraph = builder.build();
expect(paragraph.toPlainText(), 'Hello world');
expect(paragraph.spans, hasLength(2));
paragraph.layout(ParagraphConstraints(width: double.infinity));
expect(
paragraph.toDomElement().outerHtml,
'<p style="font-size: 13px; $paragraphStyle">'
@ -172,7 +215,20 @@ void testMain() {
'</span>'
'</p>',
);
expect(paragraph.spans, hasLength(2));
// Should break "Hello world" into "Hello" and " world".
paragraph.layout(ParagraphConstraints(width: 70.0));
expect(
paragraph.toDomElement().outerHtml,
'<p style="font-size: 13px; $paragraphStyle">'
'<span style="$defaultColor font-size: 13px; font-weight: bold; $defaultFontFamily">'
'Hello'
'</span>'
'<span style="$defaultColor font-size: 13px; font-style: italic; $defaultFontFamily">'
' <br>world'
'</span>'
'</p>',
);
final FlatTextSpan hello = paragraph.spans.first as FlatTextSpan;
expect(hello.textOf(paragraph), 'Hello');
@ -210,6 +266,9 @@ void testMain() {
final CanvasParagraph paragraph = builder.build();
expect(paragraph.toPlainText(), 'Hello world!');
expect(paragraph.spans, hasLength(3));
paragraph.layout(ParagraphConstraints(width: double.infinity));
expect(
paragraph.toDomElement().outerHtml,
'<p style="font-size: 13px; $paragraphStyle">'
@ -224,7 +283,6 @@ void testMain() {
'</span>'
'</p>',
);
expect(paragraph.spans, hasLength(3));
final FlatTextSpan hello = paragraph.spans[0] as FlatTextSpan;
expect(hello.textOf(paragraph), 'Hello');
@ -259,6 +317,48 @@ void testMain() {
),
);
});
test('Paragraph with new lines generates correct DOM', () {
final EngineParagraphStyle style = EngineParagraphStyle(fontSize: 13.0);
final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style);
builder.addText('First\nSecond ');
builder.pushStyle(TextStyle(fontStyle: FontStyle.italic));
builder.addText('ThirdLongLine');
final CanvasParagraph paragraph = builder.build();
expect(paragraph.toPlainText(), 'First\nSecond ThirdLongLine');
expect(paragraph.spans, hasLength(2));
// There's a new line between "First" and "Second", but "Second" and
// "ThirdLongLine" remain together since constraints are infinite.
paragraph.layout(ParagraphConstraints(width: double.infinity));
expect(
paragraph.toDomElement().outerHtml,
'<p style="font-size: 13px; $paragraphStyle">'
'<span style="$defaultColor font-size: 13px; $defaultFontFamily">'
'First<br>Second '
'</span>'
'<span style="$defaultColor font-size: 13px; font-style: italic; $defaultFontFamily">'
'ThirdLongLine'
'</span>'
'</p>',
);
// Should break the paragraph into "First", "Second" and "ThirdLongLine".
paragraph.layout(ParagraphConstraints(width: 180.0));
expect(
paragraph.toDomElement().outerHtml,
'<p style="font-size: 13px; $paragraphStyle">'
'<span style="$defaultColor font-size: 13px; $defaultFontFamily">'
'First<br>Second <br>'
'</span>'
'<span style="$defaultColor font-size: 13px; font-style: italic; $defaultFontFamily">'
'ThirdLongLine'
'</span>'
'</p>',
);
});
}
TextStyle styleWithDefaults({