Skip to content

Commit bc49cf8

Browse files
Fix text painter longest line resizing logic for TextWidthBasis.longestLine (#143024)
Fixes flutter/flutter#142309.
1 parent 4fdfe78 commit bc49cf8

File tree

4 files changed

+68
-37
lines changed

4 files changed

+68
-37
lines changed

dev/integration_tests/flutter_gallery/lib/demo/shrine/supplemental/product_card.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class ProductCard extends StatelessWidget {
3535

3636
// The fontSize to use for computing the heuristic UI scaling factor.
3737
const double defaultFontSize = 14.0;
38-
final double containerScaingFactor = MediaQuery.textScalerOf(context).scale(defaultFontSize) / defaultFontSize;
38+
final double containerScalingFactor = MediaQuery.textScalerOf(context).scale(defaultFontSize) / defaultFontSize;
3939

4040
return ScopedModelDescendant<AppStateModel>(
4141
builder: (BuildContext context, Widget? child, AppStateModel model) {
@@ -56,7 +56,7 @@ class ProductCard extends StatelessWidget {
5656
child: imageWidget,
5757
),
5858
SizedBox(
59-
height: kTextBoxHeight * containerScaingFactor,
59+
height: kTextBoxHeight * containerScalingFactor,
6060
width: 121.0,
6161
child: Column(
6262
mainAxisAlignment: MainAxisAlignment.end,

dev/integration_tests/flutter_gallery/lib/gallery/home.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,15 +180,15 @@ class _DemoItem extends StatelessWidget {
180180
final bool isDark = theme.brightness == Brightness.dark;
181181
// The fontSize to use for computing the heuristic UI scaling factor.
182182
const double defaultFontSize = 14.0;
183-
final double containerScaingFactor = MediaQuery.textScalerOf(context).scale(defaultFontSize) / defaultFontSize;
183+
final double containerScalingFactor = MediaQuery.textScalerOf(context).scale(defaultFontSize) / defaultFontSize;
184184
return RawMaterialButton(
185185
splashColor: theme.primaryColor.withOpacity(0.12),
186186
highlightColor: Colors.transparent,
187187
onPressed: () {
188188
_launchDemo(context);
189189
},
190190
child: Container(
191-
constraints: BoxConstraints(minHeight: _kDemoItemHeight * containerScaingFactor),
191+
constraints: BoxConstraints(minHeight: _kDemoItemHeight * containerScalingFactor),
192192
child: Row(
193193
children: <Widget>[
194194
Container(

packages/flutter/lib/src/painting/text_painter.dart

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -314,21 +314,32 @@ class _TextLayout {
314314
TextBaseline.ideographic => _paragraph.ideographicBaseline,
315315
};
316316
}
317+
318+
double _contentWidthFor(double minWidth, double maxWidth, TextWidthBasis widthBasis) {
319+
return switch (widthBasis) {
320+
TextWidthBasis.longestLine => clampDouble(longestLine, minWidth, maxWidth),
321+
TextWidthBasis.parent => clampDouble(maxIntrinsicLineExtent, minWidth, maxWidth),
322+
};
323+
}
317324
}
318325

319326
// This class stores the current text layout and the corresponding
320327
// paintOffset/contentWidth, as well as some cached text metrics values that
321328
// depends on the current text layout, which will be invalidated as soon as the
322329
// text layout is invalidated.
323330
class _TextPainterLayoutCacheWithOffset {
324-
_TextPainterLayoutCacheWithOffset(this.layout, this.textAlignment, double minWidth, double maxWidth, TextWidthBasis widthBasis)
325-
: contentWidth = _contentWidthFor(minWidth, maxWidth, widthBasis, layout),
326-
assert(textAlignment >= 0.0 && textAlignment <= 1.0);
331+
_TextPainterLayoutCacheWithOffset(this.layout, this.textAlignment, this.layoutMaxWidth, this.contentWidth)
332+
: assert(textAlignment >= 0.0 && textAlignment <= 1.0),
333+
assert(!layoutMaxWidth.isNaN),
334+
assert(!contentWidth.isNaN);
327335

328336
final _TextLayout layout;
329337

338+
// The input width used to lay out the paragraph.
339+
final double layoutMaxWidth;
340+
330341
// The content width the text painter should report in TextPainter.width.
331-
// This is also used to compute `paintOffset`
342+
// This is also used to compute `paintOffset`.
332343
double contentWidth;
333344

334345
// The effective text alignment in the TextPainter's canvas. The value is
@@ -352,20 +363,14 @@ class _TextPainterLayoutCacheWithOffset {
352363

353364
ui.Paragraph get paragraph => layout._paragraph;
354365

355-
static double _contentWidthFor(double minWidth, double maxWidth, TextWidthBasis widthBasis, _TextLayout layout) {
356-
return switch (widthBasis) {
357-
TextWidthBasis.longestLine => clampDouble(layout.longestLine, minWidth, maxWidth),
358-
TextWidthBasis.parent => clampDouble(layout.maxIntrinsicLineExtent, minWidth, maxWidth),
359-
};
360-
}
361-
362366
// Try to resize the contentWidth to fit the new input constraints, by just
363367
// adjusting the paint offset (so no line-breaking changes needed).
364368
//
365-
// Returns false if the new constraints require re-computing the line breaks,
366-
// in which case no side effects will occur.
369+
// Returns false if the new constraints require the text layout library to
370+
// re-compute the line breaks.
367371
bool _resizeToFit(double minWidth, double maxWidth, TextWidthBasis widthBasis) {
368372
assert(layout.maxIntrinsicLineExtent.isFinite);
373+
assert(minWidth <= maxWidth);
369374
// The assumption here is that if a Paragraph's width is already >= its
370375
// maxIntrinsicWidth, further increasing the input width does not change its
371376
// layout (but may change the paint offset if it's not left-aligned). This is
@@ -377,21 +382,30 @@ class _TextPainterLayoutCacheWithOffset {
377382
// of double.infinity, and to make the text visible the paintOffset.dx is
378383
// bound to be double.negativeInfinity, which invalidates all arithmetic
379384
// operations.
380-
final double newContentWidth = _contentWidthFor(minWidth, maxWidth, widthBasis, layout);
381-
if (newContentWidth == contentWidth) {
385+
386+
if (maxWidth == contentWidth && minWidth == contentWidth) {
387+
contentWidth = layout._contentWidthFor(minWidth, maxWidth, widthBasis);
382388
return true;
383389
}
384-
assert(minWidth <= maxWidth);
385-
// Always needsLayout when the current paintOffset and the paragraph width are not finite.
390+
391+
// Special case:
392+
// When the paint offset and the paragraph width are both +∞, it's likely
393+
// that the text layout engine skipped layout because there weren't anything
394+
// to paint. Always try to re-compute the text layout.
386395
if (!paintOffset.dx.isFinite && !paragraph.width.isFinite && minWidth.isFinite) {
387396
assert(paintOffset.dx == double.infinity);
388397
assert(paragraph.width == double.infinity);
389398
return false;
390399
}
400+
391401
final double maxIntrinsicWidth = paragraph.maxIntrinsicWidth;
392-
if ((paragraph.width - maxIntrinsicWidth) > -precisionErrorTolerance && (maxWidth - maxIntrinsicWidth) > -precisionErrorTolerance) {
393-
// Adjust the paintOffset and contentWidth to the new input constraints.
394-
contentWidth = newContentWidth;
402+
// Skip line breaking if the input width remains the same, of there will be
403+
// no soft breaks.
404+
final bool skipLineBreaking = maxWidth == layoutMaxWidth // Same input max width so relayout is unnecessary.
405+
|| ((paragraph.width - maxIntrinsicWidth) > -precisionErrorTolerance && (maxWidth - maxIntrinsicWidth) > -precisionErrorTolerance);
406+
if (skipLineBreaking) {
407+
// Adjust the content width in case the TextWidthBasis changed.
408+
contentWidth = layout._contentWidthFor(minWidth, maxWidth, widthBasis);
395409
return true;
396410
}
397411
return false;
@@ -631,10 +645,6 @@ class TextPainter {
631645
// recreated. The caller may not call `layout` again after text color is
632646
// updated. See: https://github.com/flutter/flutter/issues/85108
633647
bool _rebuildParagraphForPaint = true;
634-
// `_layoutCache`'s input width. This is only needed because there's no API to
635-
// create paint only updates that don't affect the text layout (e.g., changing
636-
// the color of the text), on ui.Paragraph or ui.ParagraphBuilder.
637-
double _inputWidth = double.nan;
638648

639649
bool get _debugAssertTextLayoutIsValid {
640650
assert(!debugDisposed);
@@ -1127,7 +1137,7 @@ class TextPainter {
11271137
// infinite paint offset.
11281138
final bool adjustMaxWidth = !maxWidth.isFinite && paintOffsetAlignment != 0;
11291139
final double? adjustedMaxWidth = !adjustMaxWidth ? maxWidth : cachedLayout?.layout.maxIntrinsicLineExtent;
1130-
_inputWidth = adjustedMaxWidth ?? maxWidth;
1140+
final double layoutMaxWidth = adjustedMaxWidth ?? maxWidth;
11311141

11321142
// Only rebuild the paragraph when there're layout changes, even when
11331143
// `_rebuildParagraphForPaint` is true. It's best to not eagerly rebuild
@@ -1137,18 +1147,21 @@ class TextPainter {
11371147
// 2. the user could be measuring the text layout so `paint` will never be
11381148
// called.
11391149
final ui.Paragraph paragraph = (cachedLayout?.paragraph ?? _createParagraph(text))
1140-
..layout(ui.ParagraphConstraints(width: _inputWidth));
1141-
final _TextPainterLayoutCacheWithOffset newLayoutCache = _TextPainterLayoutCacheWithOffset(
1142-
_TextLayout._(paragraph), paintOffsetAlignment, minWidth, maxWidth, textWidthBasis,
1143-
);
1150+
..layout(ui.ParagraphConstraints(width: layoutMaxWidth));
1151+
final _TextLayout layout = _TextLayout._(paragraph);
1152+
final double contentWidth = layout._contentWidthFor(minWidth, maxWidth, textWidthBasis);
1153+
1154+
final _TextPainterLayoutCacheWithOffset newLayoutCache;
11441155
// Call layout again if newLayoutCache had an infinite paint offset.
11451156
// This is not as expensive as it seems, line breaking is relatively cheap
11461157
// as compared to shaping.
11471158
if (adjustedMaxWidth == null && minWidth.isFinite) {
11481159
assert(maxWidth.isInfinite);
1149-
final double newInputWidth = newLayoutCache.layout.maxIntrinsicLineExtent;
1160+
final double newInputWidth = layout.maxIntrinsicLineExtent;
11501161
paragraph.layout(ui.ParagraphConstraints(width: newInputWidth));
1151-
_inputWidth = newInputWidth;
1162+
newLayoutCache = _TextPainterLayoutCacheWithOffset(layout, paintOffsetAlignment, newInputWidth, contentWidth);
1163+
} else {
1164+
newLayoutCache = _TextPainterLayoutCacheWithOffset(layout, paintOffsetAlignment, layoutMaxWidth, contentWidth);
11521165
}
11531166
_layoutCache = newLayoutCache;
11541167
}
@@ -1189,8 +1202,8 @@ class TextPainter {
11891202
// Unfortunately even if we know that there is only paint changes, there's
11901203
// no API to only make those updates so the paragraph has to be recreated
11911204
// and re-laid out.
1192-
assert(!_inputWidth.isNaN);
1193-
layoutCache.layout._paragraph = _createParagraph(text!)..layout(ui.ParagraphConstraints(width: _inputWidth));
1205+
assert(!layoutCache.layoutMaxWidth.isNaN);
1206+
layoutCache.layout._paragraph = _createParagraph(text!)..layout(ui.ParagraphConstraints(width: layoutCache.layoutMaxWidth));
11941207
assert(paragraph.width == layoutCache.layout._paragraph.width);
11951208
paragraph.dispose();
11961209
assert(debugSize == size);

packages/flutter/test/painting/text_painter_test.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,6 +1510,24 @@ void main() {
15101510
painter.dispose();
15111511
});
15121512

1513+
test('LongestLine TextPainter properly relayout when maxWidth changes.', () {
1514+
// Regression test for https://github.com/flutter/flutter/issues/142309.
1515+
final TextPainter painter = TextPainter()
1516+
..textAlign = TextAlign.justify
1517+
..textWidthBasis = TextWidthBasis.longestLine
1518+
..textDirection = TextDirection.ltr
1519+
..text = TextSpan(text: 'A' * 100, style: const TextStyle(fontSize: 10));
1520+
1521+
painter.layout(maxWidth: 1000);
1522+
expect(painter.width, 1000);
1523+
1524+
painter.layout(maxWidth: 100);
1525+
expect(painter.width, 100);
1526+
1527+
painter.layout(maxWidth: 1000);
1528+
expect(painter.width, 1000);
1529+
});
1530+
15131531
test('TextPainter line breaking does not round to integers', () {
15141532
const double fontSize = 1.25;
15151533
const String text = '12345';

0 commit comments

Comments
 (0)