From 152de0da4952d31afb1bf400c33f935358ae47b3 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Fri, 4 Dec 2020 16:40:14 -0800 Subject: [PATCH] [web] Add complex rich text test cases and fix them --- .../lib/src/engine/text/layout_service.dart | 46 +++++- .../test/text/layout_service_rich_test.dart | 152 ++++++++++++++++++ 2 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 lib/web_ui/test/text/layout_service_rich_test.dart diff --git a/lib/web_ui/lib/src/engine/text/layout_service.dart b/lib/web_ui/lib/src/engine/text/layout_service.dart index b0e368a77ba76..f1bc16fc86235 100644 --- a/lib/web_ui/lib/src/engine/text/layout_service.dart +++ b/lib/web_ui/lib/src/engine/text/layout_service.dart @@ -188,6 +188,7 @@ class TextLayoutService { if (span is PlaceholderSpan) { // TODO(mdebbar): Do placeholders affect min/max intrinsic width? } else if (span is FlatTextSpan) { + spanometer.currentSpan = span; final LineBreakResult nextBreak = currentLine.findNextBreak(span.end); // For the purpose of max intrinsic width, we don't care if the line @@ -251,6 +252,9 @@ class LineSegment { /// The width of the trailing white space in the segment. double get widthOfTrailingSpace => widthIncludingSpace - width; + + /// Whether this segment is made of only white space. + bool get isSpaceOnly => start.index == end.indexWithoutTrailingSpaces; } /// Builds instances of [EngineLineMetrics] for the given [paragraph]. @@ -358,8 +362,12 @@ class LineBuilder { void _addSegment(LineSegment segment) { _segments.add(segment); - // Add the width of previous trailing space. - width += widthOfTrailingSpace + segment.width; + // Adding a space-only segment has no effect on `width` because it doesn't + // include trailing white space. + if (!segment.isSpaceOnly) { + // Add the width of previous trailing space. + width += widthOfTrailingSpace + segment.width; + } widthIncludingSpace += segment.widthIncludingSpace; end = segment.end; } @@ -370,17 +378,39 @@ class LineBuilder { LineSegment _popSegment() { final LineSegment poppedSegment = _segments.removeLast(); - double widthOfPrevTrailingSpace; if (_segments.isEmpty) { - widthOfPrevTrailingSpace = 0.0; + width = 0.0; + widthIncludingSpace = 0.0; end = start; } else { - widthOfPrevTrailingSpace = lastSegment.widthOfTrailingSpace; + widthIncludingSpace -= poppedSegment.widthIncludingSpace; end = lastSegment.end; - } - width = width - poppedSegment.width - widthOfPrevTrailingSpace; - widthIncludingSpace -= poppedSegment.widthIncludingSpace; + // Now, let's figure out what to do with `width`. + + // Popping a space-only segment has no effect on `width`. + if (!poppedSegment.isSpaceOnly) { + // First, we subtract the width of the popped segment. + width -= poppedSegment.width; + + // Second, we subtract all trailing spaces from `width`. There could be + // multiple trailing segments that are space-only. + double widthOfTrailingSpace = 0.0; + int i = _segments.length - 1; + while (i >= 0 && _segments[i].isSpaceOnly) { + // Since the segment is space-only, `widthIncludingSpace` contains + // the width of the space and nothing else. + widthOfTrailingSpace += _segments[i].widthIncludingSpace; + i--; + } + if (i >= 0) { + // Having `i >= 0` means in the above loop we stopped at a + // non-space-only segment. We should also subtract its trailing spaces. + widthOfTrailingSpace += _segments[i].widthOfTrailingSpace; + } + width -= widthOfTrailingSpace; + } + } return poppedSegment; } diff --git a/lib/web_ui/test/text/layout_service_rich_test.dart b/lib/web_ui/test/text/layout_service_rich_test.dart new file mode 100644 index 0000000000000..d6b9df01e9bb9 --- /dev/null +++ b/lib/web_ui/test/text/layout_service_rich_test.dart @@ -0,0 +1,152 @@ +// Copyright 2013 The Flutter Authors. 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.12 + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +import 'layout_service_helper.dart'; + +const ui.Color white = ui.Color(0xFFFFFFFF); +const ui.Color black = ui.Color(0xFF000000); +const ui.Color red = ui.Color(0xFFFF0000); +const ui.Color green = ui.Color(0xFF00FF00); +const ui.Color blue = ui.Color(0xFF0000FF); + +final EngineParagraphStyle ahemStyle = EngineParagraphStyle( + fontFamily: 'ahem', + fontSize: 10, +); + +ui.ParagraphConstraints constrain(double width) { + return ui.ParagraphConstraints(width: width); +} + +CanvasParagraph rich( + EngineParagraphStyle style, + void Function(CanvasParagraphBuilder) callback, +) { + final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style); + callback(builder); + return builder.build(); +} + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { + await ui.webOnlyInitializeTestDomRenderer(); + + test('measures spans in the same line correctly', () { + final CanvasParagraph paragraph = rich(ahemStyle, (builder) { + builder.pushStyle(EngineTextStyle.only(fontSize: 12.0)); + // 12.0 * 6 = 72.0 (with spaces) + // 12.0 * 5 = 60.0 (without spaces) + builder.addText('Lorem '); + + builder.pushStyle(EngineTextStyle.only(fontSize: 13.0)); + // 13.0 * 6 = 78.0 (with spaces) + // 13.0 * 5 = 65.0 (without spaces) + builder.addText('ipsum '); + + builder.pushStyle(EngineTextStyle.only(fontSize: 11.0)); + // 11.0 * 5 = 55.0 + builder.addText('dolor'); + })..layout(constrain(double.infinity)); + + expect(paragraph.maxIntrinsicWidth, 205.0); + expect(paragraph.minIntrinsicWidth, 65.0); // "ipsum" + expect(paragraph.width, double.infinity); + expectLines(paragraph, [ + l('Lorem ipsum dolor', 0, 17, hardBreak: true, width: 205.0, left: 0.0), + ]); + }); + + test('breaks lines correctly at the end of spans', () { + final CanvasParagraph paragraph = rich(ahemStyle, (builder) { + builder.addText('Lorem '); + builder.pushStyle(EngineTextStyle.only(fontSize: 15.0)); + builder.addText('sit '); + builder.pop(); + builder.addText('.'); + })..layout(constrain(60.0)); + + expect(paragraph.maxIntrinsicWidth, 130.0); + expect(paragraph.minIntrinsicWidth, 50.0); // "Lorem" + expect(paragraph.width, 60.0); + expectLines(paragraph, [ + l('Lorem ', 0, 6, hardBreak: false, width: 50.0, left: 0.0), + l('sit ', 6, 10, hardBreak: false, width: 45.0, left: 0.0), + l('.', 10, 11, hardBreak: true, width: 10.0, left: 0.0), + ]); + }); + + test('breaks lines correctly in the middle of spans', () { + final CanvasParagraph paragraph = rich(ahemStyle, (builder) { + builder.addText('Lorem ipsum '); + builder.pushStyle(EngineTextStyle.only(fontSize: 11.0)); + builder.addText('sit dolor'); + })..layout(constrain(100.0)); + + expect(paragraph.maxIntrinsicWidth, 219.0); + expect(paragraph.minIntrinsicWidth, 55.0); // "dolor" + expect(paragraph.width, 100.0); + expectLines(paragraph, [ + l('Lorem ', 0, 6, hardBreak: false, width: 50.0, left: 0.0), + l('ipsum sit ', 6, 16, hardBreak: false, width: 93.0, left: 0.0), + l('dolor', 16, 21, hardBreak: true, width: 55.0, left: 0.0), + ]); + }); + + test('handles space-only spans', () { + final CanvasParagraph paragraph = rich(ahemStyle, (builder) { + builder.pushStyle(EngineTextStyle.only(color: red)); + builder.addText('Lorem '); + builder.pop(); + builder.pushStyle(EngineTextStyle.only(color: blue)); + builder.addText(' '); + builder.pushStyle(EngineTextStyle.only(color: green)); + builder.addText(' '); + builder.pushStyle(EngineTextStyle.only(color: black)); + builder.addText('ipsum'); + }); + paragraph.layout(constrain(80.0)); + + expect(paragraph.maxIntrinsicWidth, 160.0); + expect(paragraph.minIntrinsicWidth, 50.0); // "Lorem" or "ipsum" + expect(paragraph.width, 80.0); + expectLines(paragraph, [ + l('Lorem ', 0, 11, hardBreak: false, width: 50.0, widthWithTrailingSpaces: 110.0, left: 0.0), + l('ipsum', 11, 16, hardBreak: true, width: 50.0, left: 0.0), + ]); + }); + + test('should not break at span end if it is not a line break', () { + final CanvasParagraph paragraph = rich(ahemStyle, (builder) { + builder.pushStyle(EngineTextStyle.only(color: red)); + builder.addText('Lorem'); + builder.pop(); + builder.pushStyle(EngineTextStyle.only(color: blue)); + builder.addText(' '); + builder.pushStyle(EngineTextStyle.only(color: black)); + builder.addText('ip'); + builder.pushStyle(EngineTextStyle.only(color: green)); + builder.addText('su'); + builder.pushStyle(EngineTextStyle.only(color: white)); + builder.addText('m'); + })..layout(constrain(50.0)); + + expect(paragraph.maxIntrinsicWidth, 110.0); + expect(paragraph.minIntrinsicWidth, 50.0); // "Lorem" or "ipsum" + expect(paragraph.width, 50.0); + expectLines(paragraph, [ + l('Lorem ', 0, 6, hardBreak: false, width: 50.0, left: 0.0), + l('ipsum', 6, 11, hardBreak: true, width: 50.0, left: 0.0), + ]); + }); +}