mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Fix text cutoff when rendering paragraphs on DomCanvas (flutter/engine#23638)
This commit is contained in:
parent
5026c62732
commit
470c7befac
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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('"', '"');
|
||||
} 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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user