@@ -78,36 +78,40 @@ class TextLayoutService {
7878 final Spanometer spanometer = Spanometer (paragraph, context);
7979
8080 int spanIndex = 0 ;
81- ParagraphSpan span = paragraph.spans[0 ];
8281 LineBuilder currentLine =
8382 LineBuilder .first (paragraph, spanometer, maxWidth: constraints.width);
8483
8584 // The only way to exit this while loop is by hitting one of the `break;`
8685 // statements (e.g. when we reach `endOfText`, when ellipsis has been
8786 // appended).
8887 while (true ) {
89- // *********************************************** //
90- // *** HANDLE HARD LINE BREAKS AND END OF TEXT *** //
91- // *********************************************** //
92-
93- if (currentLine.end.isHard) {
94- if (currentLine.isNotEmpty) {
88+ // ************************** //
89+ // *** HANDLE END OF TEXT *** //
90+ // ************************** //
91+
92+ // All spans have been consumed.
93+ final bool reachedEnd = spanIndex == spanCount;
94+ if (reachedEnd) {
95+ // In some cases, we need to extend the line to the end of text and
96+ // build it:
97+ //
98+ // 1. Line is not empty. This could happen when the last span is a
99+ // placeholder.
100+ //
101+ // 2. We haven't reached `LineBreakType.endOfText` yet. This could
102+ // happen when the last character is a new line.
103+ if (currentLine.isNotEmpty || currentLine.end.type != LineBreakType .endOfText) {
104+ currentLine.extendToEndOfText ();
95105 lines.add (currentLine.build ());
96- if (currentLine.end.type != LineBreakType .endOfText) {
97- currentLine = currentLine.nextLine ();
98- }
99- }
100-
101- if (currentLine.end.type == LineBreakType .endOfText) {
102- break ;
103106 }
107+ break ;
104108 }
105109
106110 // ********************************* //
107111 // *** THE MAIN MEASUREMENT PART *** //
108112 // ********************************* //
109113
110- final isLastSpan = spanIndex == spanCount - 1 ;
114+ final ParagraphSpan span = paragraph.spans[spanIndex] ;
111115
112116 if (span is PlaceholderSpan ) {
113117 if (currentLine.widthIncludingSpace + span.width <= constraints.width) {
@@ -121,11 +125,7 @@ class TextLayoutService {
121125 }
122126 currentLine.addPlaceholder (span);
123127 }
124-
125- if (isLastSpan) {
126- lines.add (currentLine.build ());
127- break ;
128- }
128+ spanIndex++ ;
129129 } else if (span is FlatTextSpan ) {
130130 spanometer.currentSpan = span;
131131 final LineBreakResult nextBreak = currentLine.findNextBreak (span.end);
@@ -138,6 +138,10 @@ class TextLayoutService {
138138
139139 // The line can extend to `nextBreak` without overflowing.
140140 currentLine.extendTo (nextBreak);
141+ if (nextBreak.type == LineBreakType .mandatory) {
142+ lines.add (currentLine.build ());
143+ currentLine = currentLine.nextLine ();
144+ }
141145 } else {
142146 // The chunk of text can't fit into the current line.
143147 final bool isLastLine =
@@ -165,23 +169,19 @@ class TextLayoutService {
165169 currentLine = currentLine.nextLine ();
166170 }
167171 }
172+
173+ // Only go to the next span if we've reached the end of this span.
174+ if (currentLine.end.index >= span.end) {
175+ currentLine.createBox ();
176+ ++ spanIndex;
177+ }
168178 } else {
169179 throw UnimplementedError ('Unknown span type: ${span .runtimeType }' );
170180 }
171181
172182 if (lines.length == maxLines) {
173183 break ;
174184 }
175-
176- // ********************************************* //
177- // *** ADVANCE TO THE NEXT SPAN IF NECESSARY *** //
178- // ********************************************* //
179-
180- // Only go to the next span if we've reached the end of this span.
181- if (currentLine.end.index >= span.end && spanIndex < spanCount - 1 ) {
182- currentLine.createBox ();
183- span = paragraph.spans[++ spanIndex];
184- }
185185 }
186186
187187 // ************************************************** //
@@ -205,20 +205,33 @@ class TextLayoutService {
205205 // ******************************** //
206206
207207 spanIndex = 0 ;
208- span = paragraph.spans[0 ];
209208 currentLine =
210209 LineBuilder .first (paragraph, spanometer, maxWidth: constraints.width);
211210
212- while (currentLine.end.type != LineBreakType .endOfText) {
211+ while (spanIndex < spanCount) {
212+ final ParagraphSpan span = paragraph.spans[spanIndex];
213+ bool breakToNextLine = false ;
214+
213215 if (span is PlaceholderSpan ) {
214216 currentLine.addPlaceholder (span);
217+ spanIndex++ ;
215218 } else if (span is FlatTextSpan ) {
216219 spanometer.currentSpan = span;
217220 final LineBreakResult nextBreak = currentLine.findNextBreak (span.end);
218221
219222 // For the purpose of max intrinsic width, we don't care if the line
220223 // fits within the constraints or not. So we always extend it.
221224 currentLine.extendTo (nextBreak);
225+ if (nextBreak.type == LineBreakType .mandatory) {
226+ // We don't want to break the line now because we want to update
227+ // min/max intrinsic widths below first.
228+ breakToNextLine = true ;
229+ }
230+
231+ // Only go to the next span if we've reached the end of this span.
232+ if (currentLine.end.index >= span.end) {
233+ spanIndex++ ;
234+ }
222235 }
223236
224237 final double widthOfLastSegment = currentLine.lastSegment.width;
@@ -231,19 +244,9 @@ class TextLayoutService {
231244 maxIntrinsicWidth = currentLine.widthIncludingSpace;
232245 }
233246
234- if (currentLine.end.isHard ) {
247+ if (breakToNextLine ) {
235248 currentLine = currentLine.nextLine ();
236249 }
237-
238- // Only go to the next span if we've reached the end of this span.
239- if (currentLine.end.index >= span.end) {
240- if (spanIndex < spanCount - 1 ) {
241- span = paragraph.spans[++ spanIndex];
242- } else {
243- // We reached the end of the last span in the paragraph.
244- break ;
245- }
246- }
247250 }
248251 }
249252
@@ -761,12 +764,19 @@ class LineBuilder {
761764 return widthOfTrailingSpace + spanometer.measure (end, newEnd);
762765 }
763766
767+ bool get _isLastBoxAPlaceholder {
768+ if (_boxes.isEmpty) {
769+ return false ;
770+ }
771+ return (_boxes.last is PlaceholderBox );
772+ }
773+
764774 /// Extends the line by setting a [newEnd] .
765775 void extendTo (LineBreakResult newEnd) {
766776 // If the current end of the line is a hard break, the line shouldn't be
767777 // extended any further.
768778 assert (
769- isEmpty || ! end.isHard,
779+ isEmpty || ! end.isHard || _isLastBoxAPlaceholder ,
770780 'Cannot extend a line that ends with a hard break.' ,
771781 );
772782
@@ -776,6 +786,28 @@ class LineBuilder {
776786 _addSegment (_createSegment (newEnd));
777787 }
778788
789+ /// Extends the line to the end of the paragraph.
790+ void extendToEndOfText () {
791+ if (end.type == LineBreakType .endOfText) {
792+ return ;
793+ }
794+
795+ final LineBreakResult endOfText = LineBreakResult .sameIndex (
796+ paragraph.toPlainText ().length,
797+ LineBreakType .endOfText,
798+ );
799+
800+ // The spanometer may not be ready in some cases. E.g. when the paragraph
801+ // is made up of only placeholders and no text.
802+ if (spanometer.isReady) {
803+ ascent = math.max (ascent, spanometer.ascent);
804+ descent = math.max (descent, spanometer.descent);
805+ _addSegment (_createSegment (endOfText));
806+ } else {
807+ end = endOfText;
808+ }
809+ }
810+
779811 void addPlaceholder (PlaceholderSpan placeholder) {
780812 // Increase the line's height to fit the placeholder, if necessary.
781813 final double ascent, descent;
@@ -1024,7 +1056,7 @@ class LineBuilder {
10241056 final LineBreakResult boxEnd = end;
10251057 // Avoid creating empty boxes. This could happen when the end of a span
10261058 // coincides with the end of a line. In this case, `createBox` is called twice.
1027- if (boxStart == boxEnd) {
1059+ if (boxStart.index == boxEnd.index ) {
10281060 return ;
10291061 }
10301062
@@ -1045,13 +1077,20 @@ class LineBuilder {
10451077 final double ellipsisWidth =
10461078 ellipsis == null ? 0.0 : spanometer.measureText (ellipsis);
10471079
1080+ final int endIndexWithoutNewlines = math.max (start.index, end.indexWithoutTrailingNewlines);
1081+ final bool hardBreak;
1082+ if (end.type != LineBreakType .endOfText && _isLastBoxAPlaceholder) {
1083+ hardBreak = false ;
1084+ } else {
1085+ hardBreak = end.isHard;
1086+ }
10481087 return EngineLineMetrics .rich (
10491088 lineNumber,
10501089 ellipsis: ellipsis,
10511090 startIndex: start.index,
10521091 endIndex: end.index,
1053- endIndexWithoutNewlines: end.indexWithoutTrailingNewlines ,
1054- hardBreak: end.isHard ,
1092+ endIndexWithoutNewlines: endIndexWithoutNewlines ,
1093+ hardBreak: hardBreak ,
10551094 width: width + ellipsisWidth,
10561095 widthWithTrailingSpaces: widthIncludingSpace + ellipsisWidth,
10571096 left: alignOffset,
@@ -1150,6 +1189,9 @@ class Spanometer {
11501189 }
11511190 }
11521191
1192+ /// Whether the spanometer is ready to take measurements.
1193+ bool get isReady => _currentSpan != null ;
1194+
11531195 /// The distance from the top of the current span to the alphabetic baseline.
11541196 double get ascent => _currentRuler! .alphabeticBaseline;
11551197
0 commit comments