mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Fix edge cases when force-breaking lines (flutter/engine#22910)
This commit is contained in:
parent
de95b6881d
commit
ebb7ea5f1b
@ -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));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user