@@ -121,7 +121,7 @@ class TextLayoutService {
121121 // *** THE MAIN MEASUREMENT PART *** //
122122 // ********************************* //
123123
124- final ParagraphSpan span = paragraph.spans[spanIndex];
124+ ParagraphSpan span = paragraph.spans[spanIndex];
125125
126126 if (span is PlaceholderSpan ) {
127127 if (currentLine.widthIncludingSpace + span.width <= constraints.width) {
@@ -143,9 +143,6 @@ class TextLayoutService {
143143 currentLine.getAdditionalWidthTo (nextBreak.lineBreak);
144144
145145 if (currentLine.width + additionalWidth <= constraints.width) {
146- // TODO(mdebbar): Handle the case when `nextBreak` is just a span end
147- // that shouldn't extend the line yet.
148-
149146 // The line can extend to `nextBreak` without overflowing.
150147 currentLine.extendTo (nextBreak);
151148 if (nextBreak.type == LineBreakType .mandatory) {
@@ -168,8 +165,8 @@ class TextLayoutService {
168165 );
169166 lines.add (currentLine.build (ellipsis: ellipsis));
170167 break ;
171- } else if (currentLine.isEmpty ) {
172- // The current line is still empty , which means we are dealing
168+ } else if (currentLine.isNotBreakable ) {
169+ // The entire line is unbreakable , which means we are dealing
173170 // with a single block of text that doesn't fit in a single line.
174171 // We need to force-break it without adding an ellipsis.
175172
@@ -178,6 +175,16 @@ class TextLayoutService {
178175 currentLine = currentLine.nextLine ();
179176 } else {
180177 // Normal line break.
178+ currentLine.revertToLastBreakOpportunity ();
179+ // If a revert had occurred in the line, we need to revert the span
180+ // index accordingly.
181+ //
182+ // If no revert occurred, then `revertedToSpan` will be equal to
183+ // `span` and the following while loop won't do anything.
184+ final ParagraphSpan revertedToSpan = currentLine.lastSegment.span;
185+ while (span != revertedToSpan) {
186+ span = paragraph.spans[-- spanIndex];
187+ }
181188 lines.add (currentLine.build ());
182189 currentLine = currentLine.nextLine ();
183190 }
@@ -815,7 +822,7 @@ class LineBuilder {
815822 required this .start,
816823 required this .lineNumber,
817824 required this .accumulatedHeight,
818- }) : end = start;
825+ }) : _end = start;
819826
820827 /// Creates a [LineBuilder] for the first line in a paragraph.
821828 factory LineBuilder .first (
@@ -846,7 +853,14 @@ class LineBuilder {
846853 final double accumulatedHeight;
847854
848855 /// The index of the end of the line so far.
849- LineBreakResult end;
856+ LineBreakResult get end => _end;
857+ LineBreakResult _end;
858+ set end (LineBreakResult value) {
859+ if (value.type != LineBreakType .prohibited) {
860+ isBreakable = true ;
861+ }
862+ _end = value;
863+ }
850864
851865 /// The width of the line so far, excluding trailing white space.
852866 double width = 0.0 ;
@@ -869,6 +883,15 @@ class LineBuilder {
869883 /// The last segment in this line.
870884 LineSegment get lastSegment => _segments.last;
871885
886+ /// Returns true if there is at least one break opportunity in the line.
887+ bool isBreakable = false ;
888+
889+ /// Returns true if there's no break opportunity in the line.
890+ bool get isNotBreakable => ! isBreakable;
891+
892+ /// Whether the end of this line is a prohibited break.
893+ bool get isEndProhibited => end.type == LineBreakType .prohibited;
894+
872895 bool get isEmpty => _segments.isEmpty;
873896 bool get isNotEmpty => _segments.isNotEmpty;
874897
@@ -1026,6 +1049,8 @@ class LineBuilder {
10261049 boxDirection: _currentBoxDirection,
10271050 ));
10281051 _currentBoxStartOffset = widthIncludingSpace;
1052+ // Breaking is always allowed after a placeholder.
1053+ isBreakable = true ;
10291054 }
10301055
10311056 /// Creates a new segment to be appended to the end of this line.
@@ -1099,6 +1124,15 @@ class LineBuilder {
10991124 }
11001125 }
11011126
1127+ // Now let's fixes boxes if they need fixing.
1128+ //
1129+ // If we popped a segment of an already created box, we should pop the box
1130+ // too.
1131+ if (_currentBoxStart.index > poppedSegment.start.index) {
1132+ final RangeBox poppedBox = _boxes.removeLast ();
1133+ _currentBoxStartOffset -= poppedBox.width;
1134+ }
1135+
11021136 return poppedSegment;
11031137 }
11041138
@@ -1182,6 +1216,22 @@ class LineBuilder {
11821216 extendTo (nextBreak.copyWithIndex (breakingPoint));
11831217 }
11841218
1219+ /// Looks for the last break opportunity in the line and reverts the line to
1220+ /// that point.
1221+ ///
1222+ /// If the line already ends with a break opportunity, this method does
1223+ /// nothing.
1224+ void revertToLastBreakOpportunity () {
1225+ assert (isBreakable);
1226+ while (isEndProhibited) {
1227+ _popSegment ();
1228+ }
1229+ // Make sure the line is not empty and still breakable after popping a few
1230+ // segments.
1231+ assert (isNotEmpty);
1232+ assert (isBreakable);
1233+ }
1234+
11851235 LineBreakResult get _currentBoxStart {
11861236 if (_boxes.isEmpty) {
11871237 return start;
@@ -1360,13 +1410,21 @@ class LineBuilder {
13601410 return cumulativeWidth;
13611411 }
13621412
1413+ LineBreakResult ? _cachedNextBreak;
1414+
13631415 /// Finds the next line break after the end of this line.
13641416 DirectionalPosition findNextBreak () {
1417+ LineBreakResult ? nextBreak = _cachedNextBreak;
13651418 final String text = paragraph.toPlainText ();
1366- final int maxEnd = spanometer.currentSpan.end;
1367- final LineBreakResult result = nextLineBreak (text, end.index, maxEnd: maxEnd);
1419+ // Don't recompute the `nextBreak` until the line has reached the previously
1420+ // computed `nextBreak`.
1421+ if (nextBreak == null || end.index >= nextBreak.index) {
1422+ final int maxEnd = spanometer.currentSpan.end;
1423+ nextBreak = nextLineBreak (text, end.index, maxEnd: maxEnd);
1424+ _cachedNextBreak = nextBreak;
1425+ }
13681426 // The current end of the line is the beginning of the next block.
1369- return getDirectionalBlockEnd (text, end, result );
1427+ return getDirectionalBlockEnd (text, end, nextBreak );
13701428 }
13711429
13721430 /// Creates a new [LineBuilder] to build the next line in the paragraph.
0 commit comments