未验证 提交 df30ffef 编写于 作者: M Mouad Debbar 提交者: GitHub

[web] Fix edge cases when force-breaking lines (#22910)

上级 8a95546a
......@@ -89,12 +89,13 @@ class TextLayoutService {
if (currentLine.end.isHard) {
if (currentLine.isNotEmpty) {
lines.add(currentLine.build());
if (currentLine.end.type != LineBreakType.endOfText) {
currentLine = currentLine.nextLine();
}
}
if (currentLine.end.type == LineBreakType.endOfText) {
break;
} else {
currentLine = currentLine.nextLine();
}
}
......@@ -400,13 +401,27 @@ class LineBuilder {
}) {
if (ellipsis == null) {
final double availableWidth = maxWidth - widthIncludingSpace;
final LineBreakResult breakingPoint = spanometer.forceBreak(
final int breakingPoint = spanometer.forceBreak(
end.index,
nextBreak.indexWithoutTrailingSpaces,
availableWidth: availableWidth,
allowEmpty: allowEmpty,
);
extendTo(breakingPoint);
// This condition can be true in the following case:
// 1. Next break is only one character away, with zero or many spaces. AND
// 2. There isn't enough width to fit the single character. AND
// 3. `allowEmpty` is false.
if (breakingPoint == nextBreak.indexWithoutTrailingSpaces) {
// In this case, we just extend to `nextBreak` instead of creating a new
// artifical break. It's safe (and better) to do so, because we don't
// want the trailing white space to go to the next line.
extendTo(nextBreak);
} else {
extendTo(
LineBreakResult.sameIndex(breakingPoint, LineBreakType.prohibited),
);
}
return;
}
......@@ -428,20 +443,20 @@ class LineBuilder {
// After the loop ends, two things are correct:
// 1. All remaining segments in `_segments` can fit within constraints.
// 2. Adding `segmentToBreak` causes the line to overflow.
while (_segments.isNotEmpty && width > availableWidth) {
while (_segments.isNotEmpty && widthIncludingSpace > availableWidth) {
segmentToBreak = _popSegment();
}
spanometer.currentSpan = segmentToBreak.span as FlatTextSpan;
final double availableWidthForSegment =
availableWidth - widthIncludingSpace;
final LineBreakResult breakingPoint = spanometer.forceBreak(
final int breakingPoint = spanometer.forceBreak(
segmentToBreak.start.index,
segmentToBreak.end.indexWithoutTrailingSpaces,
segmentToBreak.end.index,
availableWidth: availableWidthForSegment,
allowEmpty: allowEmpty,
);
extendTo(breakingPoint);
extendTo(LineBreakResult.sameIndex(breakingPoint, LineBreakType.prohibited));
}
/// Builds the [EngineLineMetrics] instance that represents this line.
......@@ -544,7 +559,19 @@ class Spanometer {
return _measureSubstring(context, text, 0, text.length);
}
LineBreakResult forceBreak(
/// In a continuous, unbreakable block of text from [start] to [end], finds
/// the point where text should be broken to fit in the given [availableWidth].
///
/// The [start] and [end] indices have to be within the same text span.
///
/// When [allowEmpty] is true, the result is guaranteed to be at least one
/// character after [start]. But if [allowEmpty] is false and there isn't
/// enough [availableWidth] to fit the first character, then [start] is
/// returned.
///
/// See also:
/// - [LineBuilder.forceBreak].
int forceBreak(
int start,
int end, {
required double availableWidth,
......@@ -559,8 +586,7 @@ class Spanometer {
assert(end >= span.start && end <= span.end);
if (availableWidth <= 0.0) {
return LineBreakResult.sameIndex(
allowEmpty ? start : start + 1, LineBreakType.prohibited);
return allowEmpty ? start : start + 1;
}
int low = start;
......@@ -580,7 +606,7 @@ class Spanometer {
if (low == start && !allowEmpty) {
low++;
}
return LineBreakResult.sameIndex(low, LineBreakType.prohibited);
return low;
}
double _measure(int start, int end) {
......
......@@ -11,7 +11,6 @@ import 'package:ui/ui.dart' as ui;
import 'layout_service_helper.dart';
const bool skipForceBreak = true;
const bool skipTextAlign = true;
const bool skipWordSpacing = true;
......@@ -147,6 +146,17 @@ void testMain() async {
l('k lm', 10, 14, hardBreak: true, width: 40.0, left: 0.0),
]);
// Constraints enough only for "abcdef" but not for the trailing space.
paragraph = plain(ahemStyle, 'abcdef gh')..layout(constrain(60.0));
expect(paragraph.maxIntrinsicWidth, 90);
expect(paragraph.minIntrinsicWidth, 60);
expect(paragraph.width, 60);
// expect(paragraph.height, 20);
expectLines(paragraph, [
l('abcdef ', 0, 7, hardBreak: false, width: 60.0, left: 0.0),
l('gh', 7, 9, hardBreak: true, width: 20.0, left: 0.0),
]);
// Constraints aren't enough even for a single character. In this case,
// we show a minimum of one character per line.
paragraph = plain(ahemStyle, 'AA')..layout(constrain(8.0));
......@@ -183,7 +193,7 @@ void testMain() async {
l('A', 2, 4, hardBreak: true, width: 10.0, left: 0.0),
l('', 4, 4, hardBreak: true, width: 0.0, left: 0.0),
]);
}, skip: skipForceBreak);
});
test('uses multi-line for text that contains new-line', () {
final CanvasParagraph paragraph = plain(ahemStyle, '12\n34')
......@@ -315,16 +325,14 @@ void testMain() async {
l('defg', 8, 12, hardBreak: true, width: 40.0, left: 0.0),
]);
if (!skipForceBreak) {
// Very long text.
paragraph = plain(ahemStyle, 'AAAAAAAAAAAA')..layout(constrain(50.0));
expect(paragraph.minIntrinsicWidth, 120);
expectLines(paragraph, [
l('AAAAA', 0, 5, hardBreak: false, width: 50.0, left: 0.0),
l('AAAAA', 5, 10, hardBreak: false, width: 50.0, left: 0.0),
l('AA', 10, 12, hardBreak: true, width: 20.0, left: 0.0),
]);
}
// Very long text.
paragraph = plain(ahemStyle, 'AAAAAAAAAAAA')..layout(constrain(50.0));
expect(paragraph.minIntrinsicWidth, 120);
expectLines(paragraph, [
l('AAAAA', 0, 5, hardBreak: false, width: 50.0, left: 0.0),
l('AAAAA', 5, 10, hardBreak: false, width: 50.0, left: 0.0),
l('AA', 10, 12, hardBreak: true, width: 20.0, left: 0.0),
]);
});
test('maxIntrinsicWidth', () {
......@@ -372,16 +380,14 @@ void testMain() async {
l('def ', 5, 11, hardBreak: true, width: 30.0, left: 0.0),
]);
if (!skipForceBreak) {
// Very long text.
paragraph = plain(ahemStyle, 'AAAAAAAAAAAA')..layout(constrain(50.0));
expect(paragraph.maxIntrinsicWidth, 120);
expectLines(paragraph, [
l('AAAAA', 0, 5, hardBreak: false, width: 50.0, left: 0.0),
l('AAAAA', 5, 10, hardBreak: false, width: 50.0, left: 0.0),
l('AA', 10, 12, hardBreak: true, width: 20.0, left: 0.0),
]);
}
// Very long text.
paragraph = plain(ahemStyle, 'AAAAAAAAAAAA')..layout(constrain(50.0));
expect(paragraph.maxIntrinsicWidth, 120);
expectLines(paragraph, [
l('AAAAA', 0, 5, hardBreak: false, width: 50.0, left: 0.0),
l('AAAAA', 5, 10, hardBreak: false, width: 50.0, left: 0.0),
l('AA', 10, 12, hardBreak: true, width: 20.0, left: 0.0),
]);
});
test('respects text overflow', () {
......@@ -418,6 +424,17 @@ void testMain() async {
l('AA...', 4, 6, hardBreak: false, width: 50.0, left: 0.0),
]);
// Constraints only enough to fit "AA" with the ellipsis, but not the
// trailing white space.
final CanvasParagraph trailingSpace = plain(overflowStyle, 'AA AAA')
..layout(constrain(50.0));
expect(trailingSpace.minIntrinsicWidth, 30);
expect(trailingSpace.maxIntrinsicWidth, 60);
// expect(trailingSpace.height, 10);
expectLines(trailingSpace, [
l('AA...', 0, 2, hardBreak: false, width: 50.0, left: 0.0),
]);
// Tiny constraints.
final CanvasParagraph paragraph = plain(overflowStyle, 'AAAA')
..layout(constrain(30.0));
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册