boxes = line.boxes;
final StringBuffer buffer = StringBuffer();
@@ -195,47 +173,28 @@ class CanvasParagraph implements ui.Paragraph {
int j = 0;
while (j < boxes.length) {
final RangeBox box = boxes[j++];
- if (box is SpanBox && box.span == span) {
- buffer.write(box.toText());
- continue;
- }
-
- if (buffer.isNotEmpty) {
- element.appendText(buffer.toString());
- buffer.clear();
- }
if (box is SpanBox) {
- span = box.span;
- element = html.document.createElement('span') as html.HtmlElement;
+ lastSpanElement = html.document.createElement('span') as html.HtmlElement;
applyTextStyleToElement(
- element: element,
+ element: lastSpanElement,
style: box.span.style,
isSpan: true,
);
- rootElement.append(element);
+ _positionSpanElement(lastSpanElement, line, box);
+ lastSpanElement.appendText(box.toText());
+ rootElement.append(lastSpanElement);
buffer.write(box.toText());
} else if (box is PlaceholderBox) {
- span = null;
- // If there's a line-end after this placeholder, we want the
to
- // be inserted in the root paragraph element.
- element = rootElement;
- rootElement.append(
- createPlaceholderElement(placeholder: box.placeholder),
- );
+ lastSpanElement = null;
} else {
throw UnimplementedError('Unknown box type: ${box.runtimeType}');
}
}
- if (buffer.isNotEmpty) {
- element.appendText(buffer.toString());
- buffer.clear();
- }
-
final String? ellipsis = line.ellipsis;
if (ellipsis != null) {
- element.appendText(ellipsis);
+ (lastSpanElement ?? rootElement).appendText(ellipsis);
}
}
@@ -307,6 +266,9 @@ void _applyNecessaryParagraphStyles({
}) {
final html.CssStyleDeclaration cssStyle = element.style;
+ // TODO(mdebbar): Now that we absolutely position each span inside the
+ // paragraph, do we still need these style on ?
+
if (style.textAlign != null) {
cssStyle.textAlign = textAlignToCssValue(
style.textAlign, style.textDirection ?? ui.TextDirection.ltr);
@@ -352,6 +314,14 @@ void _applySpanStylesToParagraph({
}
}
+void _positionSpanElement(html.Element element, EngineLineMetrics line, RangeBox box) {
+ final ui.TextBox textBox = box.toTextBox(line);
+ element.style
+ ..position = 'absolute'
+ ..top = '${textBox.top}px'
+ ..left = '${textBox.left}px';
+}
+
/// A common interface for all types of spans that make up a paragraph.
///
/// These spans are stored as a flat list in the paragraph object.
diff --git a/lib/web_ui/test/html/paragraph/general_golden_test.dart b/lib/web_ui/test/html/paragraph/general_golden_test.dart
index e3caf19f03b13..2ac41fe3d0fbc 100644
--- a/lib/web_ui/test/html/paragraph/general_golden_test.dart
+++ b/lib/web_ui/test/html/paragraph/general_golden_test.dart
@@ -371,7 +371,7 @@ Future testMain() async {
});
void testFontFeatures(EngineCanvas canvas) {
- const String text = 'Aa Bb Dd Ee Ff Difficult';
+ const String text = 'Bb Difficult ';
const FontFeature enableSmallCaps = FontFeature('smcp');
const FontFeature disableSmallCaps = FontFeature('smcp', 0);
@@ -446,7 +446,7 @@ Future testMain() async {
enableOnum,
],
));
- builder.addText('$text - $numeric');
+ builder.addText('$text $numeric');
builder.pop(); // enableSmallCaps, enableOnum
},
)..layout(constrain(double.infinity));
diff --git a/lib/web_ui/test/text/canvas_paragraph_builder_test.dart b/lib/web_ui/test/text/canvas_paragraph_builder_test.dart
index 82283e9b9ada4..2997f99d6f427 100644
--- a/lib/web_ui/test/text/canvas_paragraph_builder_test.dart
+++ b/lib/web_ui/test/text/canvas_paragraph_builder_test.dart
@@ -7,27 +7,28 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
-bool get isIosSafari => browserEngine == BrowserEngine.webkit &&
- operatingSystem == OperatingSystem.iOs;
+bool get isIosSafari =>
+ browserEngine == BrowserEngine.webkit &&
+ operatingSystem == OperatingSystem.iOs;
+
+/// Some text measurements are sensitive to browser implementations. Position
+/// info in the following tests only pass in Chrome, they are slightly different
+/// on each browser. So we need to ignore position info on non-Chrome browsers
+/// when comparing expectations with actual output.
+bool get isBlink => browserEngine == BrowserEngine.blink;
String fontFamilyToAttribute(String fontFamily) {
fontFamily = canonicalizeFontFamily(fontFamily)!;
if (browserEngine == BrowserEngine.firefox) {
- fontFamily = fontFamily.replaceAll('"', '"');
+ return fontFamily.replaceAll('"', '"');
} else if (browserEngine == BrowserEngine.blink ||
browserEngine == BrowserEngine.samsung ||
browserEngine == BrowserEngine.webkit) {
- fontFamily = fontFamily.replaceAll('"', '');
+ return fontFamily.replaceAll('"', '');
}
- return 'font-family: $fontFamily;';
+ return fontFamily;
}
-final String defaultFontFamily = fontFamilyToAttribute('Ahem');
-const String defaultColor = 'color: rgb(255, 0, 0);';
-const String defaultFontSize = 'font-size: 14px;';
-const String paragraphStyle =
- 'position: absolute; white-space: pre;';
-
void main() {
internalBootstrapBrowserTest(() => testMain);
}
@@ -47,24 +48,29 @@ Future testMain() async {
expect(paragraph.spans, hasLength(1));
paragraph.layout(const ParagraphConstraints(width: double.infinity));
- expect(
- paragraph.toDomElement().outerHtml,
- ''
- ''
+ expectOuterHtml(
+ paragraph,
+ ''
+ ''
'Hello'
''
'
',
+ ignorePositions: !isBlink,
);
// Should break "Hello" into "Hel" and "lo".
paragraph.layout(const ParagraphConstraints(width: 39.0));
- expect(
- paragraph.toDomElement().outerHtml,
- ''
- ''
- 'Hel
lo'
+ expectOuterHtml(
+ paragraph,
+ ''
+ ''
+ 'Hel'
+ ''
+ ''
+ 'lo'
''
'
',
+ ignorePositions: !isBlink,
);
final ParagraphSpan span = paragraph.spans.single;
@@ -88,8 +94,8 @@ Future testMain() async {
paragraph.layout(const ParagraphConstraints(width: double.infinity));
expect(
paragraph.toDomElement().outerHtml,
- ''
- ''
+ ''
+ ''
'Hello'
''
'
',
@@ -109,16 +115,11 @@ Future testMain() async {
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(const ParagraphConstraints(width: double.infinity));
expect(
paragraph.toDomElement().outerHtml,
- ''
- ''
+ ''
+ ''
'Hello'
''
'
',
@@ -135,16 +136,11 @@ Future testMain() async {
expect(paragraph.paragraphStyle, style);
expect(paragraph.toPlainText(), 'HelloWorld');
- double expectedHeight = 14.0;
- if (isIosSafari) {
- // On iOS Safari, the height measurement is one extra pixel.
- expectedHeight++;
- }
paragraph.layout(const ParagraphConstraints(width: 100.0));
expect(
paragraph.toDomElement().outerHtml,
- ''
- ''
+ ''
+ ''
'Hell...'
''
'
',
@@ -171,8 +167,8 @@ Future testMain() async {
paragraph.layout(const ParagraphConstraints(width: double.infinity));
expect(
paragraph.toDomElement().outerHtml,
- ''
- ''
+ ''
+ ''
'Hello'
''
'
',
@@ -207,30 +203,38 @@ Future testMain() async {
expect(paragraph.spans, hasLength(2));
paragraph.layout(const ParagraphConstraints(width: double.infinity));
- expect(
- paragraph.toDomElement().outerHtml,
- ''
- ''
+ expectOuterHtml(
+ paragraph,
+ ''
+ ''
'Hello'
''
- ''
- ' world'
+ ''
+ ' '
+ ''
+ ''
+ 'world'
''
'
',
+ ignorePositions: !isBlink,
);
- // Should break "Hello world" into "Hello" and " world".
+ // Should break "Hello world" into 2 lines: "Hello" and " world".
paragraph.layout(const ParagraphConstraints(width: 75.0));
- expect(
- paragraph.toDomElement().outerHtml,
- ''
- ''
+ expectOuterHtml(
+ paragraph,
+ ''
+ ''
'Hello'
''
- ''
- '
world'
+ ''
+ ' '
+ ''
+ ''
+ 'world'
''
'
',
+ ignorePositions: !isBlink,
);
final FlatTextSpan hello = paragraph.spans.first as FlatTextSpan;
@@ -272,19 +276,23 @@ Future testMain() async {
expect(paragraph.spans, hasLength(3));
paragraph.layout(const ParagraphConstraints(width: double.infinity));
- expect(
- paragraph.toDomElement().outerHtml,
- ''
- ''
+ expectOuterHtml(
+ paragraph,
+ ''
+ ''
'Hello'
''
- ''
- ' world'
+ ''
+ ' '
+ ''
+ ''
+ 'world'
''
- ''
+ ''
'!'
''
'
',
+ ignorePositions: !isBlink,
);
final FlatTextSpan hello = paragraph.spans[0] as FlatTextSpan;
@@ -336,30 +344,44 @@ Future testMain() async {
// There's a new line between "First" and "Second", but "Second" and
// "ThirdLongLine" remain together since constraints are infinite.
paragraph.layout(const ParagraphConstraints(width: double.infinity));
- expect(
- paragraph.toDomElement().outerHtml,
- ''
- ''
- 'First
Second '
+ expectOuterHtml(
+ paragraph,
+ ''
+ ''
+ 'First'
+ ''
+ ''
+ 'Second'
''
- ''
+ ''
+ ' '
+ ''
+ ''
'ThirdLongLine'
''
'
',
+ ignorePositions: !isBlink,
);
// Should break the paragraph into "First", "Second" and "ThirdLongLine".
paragraph.layout(const ParagraphConstraints(width: 180.0));
- expect(
- paragraph.toDomElement().outerHtml,
- ''
- ''
- 'First
Second
'
+ expectOuterHtml(
+ paragraph,
+ ''
+ ''
+ 'First'
+ ''
+ ''
+ 'Second'
+ ''
+ ''
+ ' '
''
- ''
+ ''
'ThirdLongLine'
''
'
',
+ ignorePositions: !isBlink,
);
});
@@ -384,24 +406,74 @@ Future testMain() async {
// The paragraph should take the font size and family from the span with the
// greatest font size.
paragraph.layout(const ParagraphConstraints(width: double.infinity));
- expect(
- paragraph.toDomElement().outerHtml,
- ''
- ''
- 'First '
+ expectOuterHtml(
+ paragraph,
+ ''
+ ''
+ 'First'
''
- ''
- 'Second '
+ ''
+ ' '
''
- ''
+ ''
+ 'Second'
+ ''
+ ''
+ ' '
+ ''
+ ''
'Third'
''
'
',
+ // Since we are using unknown font families, we can't predict the text
+ // measurements.
+ ignorePositions: true,
);
debugEmulateFlutterTesterEnvironment = true;
});
}
+const String defaultFontFamily = 'Ahem';
+const num defaultFontSize = 14;
+
+String paragraphStyle({
+ String fontFamily = defaultFontFamily,
+ num fontSize = defaultFontSize,
+ num? lineHeight,
+}) {
+ return [
+ if (lineHeight != null) 'line-height: $lineHeight;',
+ 'font-size: ${fontSize}px;',
+ 'font-family: ${fontFamilyToAttribute(fontFamily)};',
+ 'position: absolute;',
+ 'white-space: pre;',
+ ].join(' ');
+}
+
+String spanStyle({
+ required num? top,
+ required num? left,
+ String fontFamily = defaultFontFamily,
+ num fontSize = defaultFontSize,
+ String? fontWeight,
+ String? fontStyle,
+ num? lineHeight,
+ num? letterSpacing,
+}) {
+ return [
+ 'color: rgb(255, 0, 0);',
+ if (lineHeight != null) 'line-height: $lineHeight;',
+ 'font-size: ${fontSize}px;',
+ if (fontWeight != null) 'font-weight: $fontWeight;',
+ if (fontStyle != null) 'font-style: $fontStyle;',
+ 'font-family: ${fontFamilyToAttribute(fontFamily)};',
+ if (letterSpacing != null) 'letter-spacing: ${letterSpacing}px;',
+ 'position: absolute;',
+ if (top != null) 'top: ${top}px;',
+ if (left != null) 'left: ${left}px;',
+ ].join(' ');
+}
+
TextStyle styleWithDefaults({
Color color = const Color(0xFFFF0000),
String fontFamily = FlutterViewEmbedder.defaultFontFamily,
@@ -421,3 +493,23 @@ TextStyle styleWithDefaults({
letterSpacing: letterSpacing,
);
}
+
+void expectOuterHtml(CanvasParagraph paragraph, String expected, {required bool ignorePositions}) {
+ String outerHtml = paragraph.toDomElement().outerHtml!;
+ if (ignorePositions) {
+ outerHtml = removePositionInfo(outerHtml);
+ expected = removePositionInfo(expected);
+ }
+
+ expect(outerHtml, expected);
+}
+
+/// Removes "top" and "left" CSS styles from the given html string.
+///
+/// This is needed when the positioning information in the html output is
+/// unknown and could be different depending on browser and environment.
+String removePositionInfo(String outerHtml) {
+ return outerHtml
+ .replaceAll(RegExp(r'\s*top:\s*[\d\.]+px\s*;\s*'), '')
+ .replaceAll(RegExp(r'\s*left:\s*[\d\.]+px\s*;\s*'), '');
+}