Skip to content

Commit 3a035ee

Browse files
authored
[web] Add complex rich text test cases and fix them (flutter#22948)
1 parent 8619a81 commit 3a035ee

File tree

2 files changed

+190
-8
lines changed

2 files changed

+190
-8
lines changed

lib/web_ui/lib/src/engine/text/layout_service.dart

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ class TextLayoutService {
188188
if (span is PlaceholderSpan) {
189189
// TODO(mdebbar): Do placeholders affect min/max intrinsic width?
190190
} else if (span is FlatTextSpan) {
191+
spanometer.currentSpan = span;
191192
final LineBreakResult nextBreak = currentLine.findNextBreak(span.end);
192193

193194
// For the purpose of max intrinsic width, we don't care if the line
@@ -251,6 +252,9 @@ class LineSegment {
251252

252253
/// The width of the trailing white space in the segment.
253254
double get widthOfTrailingSpace => widthIncludingSpace - width;
255+
256+
/// Whether this segment is made of only white space.
257+
bool get isSpaceOnly => start.index == end.indexWithoutTrailingSpaces;
254258
}
255259

256260
/// Builds instances of [EngineLineMetrics] for the given [paragraph].
@@ -358,8 +362,12 @@ class LineBuilder {
358362
void _addSegment(LineSegment segment) {
359363
_segments.add(segment);
360364

361-
// Add the width of previous trailing space.
362-
width += widthOfTrailingSpace + segment.width;
365+
// Adding a space-only segment has no effect on `width` because it doesn't
366+
// include trailing white space.
367+
if (!segment.isSpaceOnly) {
368+
// Add the width of previous trailing space.
369+
width += widthOfTrailingSpace + segment.width;
370+
}
363371
widthIncludingSpace += segment.widthIncludingSpace;
364372
end = segment.end;
365373
}
@@ -370,17 +378,39 @@ class LineBuilder {
370378
LineSegment _popSegment() {
371379
final LineSegment poppedSegment = _segments.removeLast();
372380

373-
double widthOfPrevTrailingSpace;
374381
if (_segments.isEmpty) {
375-
widthOfPrevTrailingSpace = 0.0;
382+
width = 0.0;
383+
widthIncludingSpace = 0.0;
376384
end = start;
377385
} else {
378-
widthOfPrevTrailingSpace = lastSegment.widthOfTrailingSpace;
386+
widthIncludingSpace -= poppedSegment.widthIncludingSpace;
379387
end = lastSegment.end;
380-
}
381388

382-
width = width - poppedSegment.width - widthOfPrevTrailingSpace;
383-
widthIncludingSpace -= poppedSegment.widthIncludingSpace;
389+
// Now, let's figure out what to do with `width`.
390+
391+
// Popping a space-only segment has no effect on `width`.
392+
if (!poppedSegment.isSpaceOnly) {
393+
// First, we subtract the width of the popped segment.
394+
width -= poppedSegment.width;
395+
396+
// Second, we subtract all trailing spaces from `width`. There could be
397+
// multiple trailing segments that are space-only.
398+
double widthOfTrailingSpace = 0.0;
399+
int i = _segments.length - 1;
400+
while (i >= 0 && _segments[i].isSpaceOnly) {
401+
// Since the segment is space-only, `widthIncludingSpace` contains
402+
// the width of the space and nothing else.
403+
widthOfTrailingSpace += _segments[i].widthIncludingSpace;
404+
i--;
405+
}
406+
if (i >= 0) {
407+
// Having `i >= 0` means in the above loop we stopped at a
408+
// non-space-only segment. We should also subtract its trailing spaces.
409+
widthOfTrailingSpace += _segments[i].widthOfTrailingSpace;
410+
}
411+
width -= widthOfTrailingSpace;
412+
}
413+
}
384414

385415
return poppedSegment;
386416
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// @dart = 2.12
6+
7+
import 'package:test/bootstrap/browser.dart';
8+
import 'package:test/test.dart';
9+
import 'package:ui/src/engine.dart';
10+
import 'package:ui/ui.dart' as ui;
11+
12+
import 'layout_service_helper.dart';
13+
14+
const ui.Color white = ui.Color(0xFFFFFFFF);
15+
const ui.Color black = ui.Color(0xFF000000);
16+
const ui.Color red = ui.Color(0xFFFF0000);
17+
const ui.Color green = ui.Color(0xFF00FF00);
18+
const ui.Color blue = ui.Color(0xFF0000FF);
19+
20+
final EngineParagraphStyle ahemStyle = EngineParagraphStyle(
21+
fontFamily: 'ahem',
22+
fontSize: 10,
23+
);
24+
25+
ui.ParagraphConstraints constrain(double width) {
26+
return ui.ParagraphConstraints(width: width);
27+
}
28+
29+
CanvasParagraph rich(
30+
EngineParagraphStyle style,
31+
void Function(CanvasParagraphBuilder) callback,
32+
) {
33+
final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style);
34+
callback(builder);
35+
return builder.build();
36+
}
37+
38+
void main() {
39+
internalBootstrapBrowserTest(() => testMain);
40+
}
41+
42+
void testMain() async {
43+
await ui.webOnlyInitializeTestDomRenderer();
44+
45+
test('measures spans in the same line correctly', () {
46+
final CanvasParagraph paragraph = rich(ahemStyle, (builder) {
47+
builder.pushStyle(EngineTextStyle.only(fontSize: 12.0));
48+
// 12.0 * 6 = 72.0 (with spaces)
49+
// 12.0 * 5 = 60.0 (without spaces)
50+
builder.addText('Lorem ');
51+
52+
builder.pushStyle(EngineTextStyle.only(fontSize: 13.0));
53+
// 13.0 * 6 = 78.0 (with spaces)
54+
// 13.0 * 5 = 65.0 (without spaces)
55+
builder.addText('ipsum ');
56+
57+
builder.pushStyle(EngineTextStyle.only(fontSize: 11.0));
58+
// 11.0 * 5 = 55.0
59+
builder.addText('dolor');
60+
})..layout(constrain(double.infinity));
61+
62+
expect(paragraph.maxIntrinsicWidth, 205.0);
63+
expect(paragraph.minIntrinsicWidth, 65.0); // "ipsum"
64+
expect(paragraph.width, double.infinity);
65+
expectLines(paragraph, [
66+
l('Lorem ipsum dolor', 0, 17, hardBreak: true, width: 205.0, left: 0.0),
67+
]);
68+
});
69+
70+
test('breaks lines correctly at the end of spans', () {
71+
final CanvasParagraph paragraph = rich(ahemStyle, (builder) {
72+
builder.addText('Lorem ');
73+
builder.pushStyle(EngineTextStyle.only(fontSize: 15.0));
74+
builder.addText('sit ');
75+
builder.pop();
76+
builder.addText('.');
77+
})..layout(constrain(60.0));
78+
79+
expect(paragraph.maxIntrinsicWidth, 130.0);
80+
expect(paragraph.minIntrinsicWidth, 50.0); // "Lorem"
81+
expect(paragraph.width, 60.0);
82+
expectLines(paragraph, [
83+
l('Lorem ', 0, 6, hardBreak: false, width: 50.0, left: 0.0),
84+
l('sit ', 6, 10, hardBreak: false, width: 45.0, left: 0.0),
85+
l('.', 10, 11, hardBreak: true, width: 10.0, left: 0.0),
86+
]);
87+
});
88+
89+
test('breaks lines correctly in the middle of spans', () {
90+
final CanvasParagraph paragraph = rich(ahemStyle, (builder) {
91+
builder.addText('Lorem ipsum ');
92+
builder.pushStyle(EngineTextStyle.only(fontSize: 11.0));
93+
builder.addText('sit dolor');
94+
})..layout(constrain(100.0));
95+
96+
expect(paragraph.maxIntrinsicWidth, 219.0);
97+
expect(paragraph.minIntrinsicWidth, 55.0); // "dolor"
98+
expect(paragraph.width, 100.0);
99+
expectLines(paragraph, [
100+
l('Lorem ', 0, 6, hardBreak: false, width: 50.0, left: 0.0),
101+
l('ipsum sit ', 6, 16, hardBreak: false, width: 93.0, left: 0.0),
102+
l('dolor', 16, 21, hardBreak: true, width: 55.0, left: 0.0),
103+
]);
104+
});
105+
106+
test('handles space-only spans', () {
107+
final CanvasParagraph paragraph = rich(ahemStyle, (builder) {
108+
builder.pushStyle(EngineTextStyle.only(color: red));
109+
builder.addText('Lorem ');
110+
builder.pop();
111+
builder.pushStyle(EngineTextStyle.only(color: blue));
112+
builder.addText(' ');
113+
builder.pushStyle(EngineTextStyle.only(color: green));
114+
builder.addText(' ');
115+
builder.pushStyle(EngineTextStyle.only(color: black));
116+
builder.addText('ipsum');
117+
});
118+
paragraph.layout(constrain(80.0));
119+
120+
expect(paragraph.maxIntrinsicWidth, 160.0);
121+
expect(paragraph.minIntrinsicWidth, 50.0); // "Lorem" or "ipsum"
122+
expect(paragraph.width, 80.0);
123+
expectLines(paragraph, [
124+
l('Lorem ', 0, 11, hardBreak: false, width: 50.0, widthWithTrailingSpaces: 110.0, left: 0.0),
125+
l('ipsum', 11, 16, hardBreak: true, width: 50.0, left: 0.0),
126+
]);
127+
});
128+
129+
test('should not break at span end if it is not a line break', () {
130+
final CanvasParagraph paragraph = rich(ahemStyle, (builder) {
131+
builder.pushStyle(EngineTextStyle.only(color: red));
132+
builder.addText('Lorem');
133+
builder.pop();
134+
builder.pushStyle(EngineTextStyle.only(color: blue));
135+
builder.addText(' ');
136+
builder.pushStyle(EngineTextStyle.only(color: black));
137+
builder.addText('ip');
138+
builder.pushStyle(EngineTextStyle.only(color: green));
139+
builder.addText('su');
140+
builder.pushStyle(EngineTextStyle.only(color: white));
141+
builder.addText('m');
142+
})..layout(constrain(50.0));
143+
144+
expect(paragraph.maxIntrinsicWidth, 110.0);
145+
expect(paragraph.minIntrinsicWidth, 50.0); // "Lorem" or "ipsum"
146+
expect(paragraph.width, 50.0);
147+
expectLines(paragraph, [
148+
l('Lorem ', 0, 6, hardBreak: false, width: 50.0, left: 0.0),
149+
l('ipsum', 6, 11, hardBreak: true, width: 50.0, left: 0.0),
150+
]);
151+
});
152+
}

0 commit comments

Comments
 (0)