Unverified Commit 0a3df1b5 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Text wrap width (#31987)

Add `textWidthBasis` param to Text to allow calculating width according to longest line.
parent 72a72b37
...@@ -15,6 +15,21 @@ import 'text_span.dart'; ...@@ -15,6 +15,21 @@ import 'text_span.dart';
export 'package:flutter/services.dart' show TextRange, TextSelection; export 'package:flutter/services.dart' show TextRange, TextSelection;
/// The different ways of considering the width of one or more lines of text.
///
/// See [Text.widthType].
enum TextWidthBasis {
/// Multiline text will take up the full width given by the parent. For single
/// line text, only the minimum amount of width needed to contain the text
/// will be used. A common use case for this is a standard series of
/// paragraphs.
parent,
/// The width will be exactly enough to contain the longest line and no
/// longer. A common use case for this is chat bubbles.
longestLine,
}
class _CaretMetrics { class _CaretMetrics {
const _CaretMetrics({this.offset, this.fullHeight}); const _CaretMetrics({this.offset, this.fullHeight});
/// The offset of the top left corner of the caret from the top left /// The offset of the top left corner of the caret from the top left
...@@ -60,10 +75,12 @@ class TextPainter { ...@@ -60,10 +75,12 @@ class TextPainter {
String ellipsis, String ellipsis,
Locale locale, Locale locale,
StrutStyle strutStyle, StrutStyle strutStyle,
TextWidthBasis textWidthBasis = TextWidthBasis.parent,
}) : assert(text == null || text.debugAssertIsValid()), }) : assert(text == null || text.debugAssertIsValid()),
assert(textAlign != null), assert(textAlign != null),
assert(textScaleFactor != null), assert(textScaleFactor != null),
assert(maxLines == null || maxLines > 0), assert(maxLines == null || maxLines > 0),
assert(textWidthBasis != null),
_text = text, _text = text,
_textAlign = textAlign, _textAlign = textAlign,
_textDirection = textDirection, _textDirection = textDirection,
...@@ -71,7 +88,8 @@ class TextPainter { ...@@ -71,7 +88,8 @@ class TextPainter {
_maxLines = maxLines, _maxLines = maxLines,
_ellipsis = ellipsis, _ellipsis = ellipsis,
_locale = locale, _locale = locale,
_strutStyle = strutStyle; _strutStyle = strutStyle,
_textWidthBasis = textWidthBasis;
ui.Paragraph _paragraph; ui.Paragraph _paragraph;
bool _needsLayout = true; bool _needsLayout = true;
...@@ -233,6 +251,18 @@ class TextPainter { ...@@ -233,6 +251,18 @@ class TextPainter {
_needsLayout = true; _needsLayout = true;
} }
/// {@macro flutter.dart:ui.text.TextWidthBasis}
TextWidthBasis get textWidthBasis => _textWidthBasis;
TextWidthBasis _textWidthBasis;
set textWidthBasis(TextWidthBasis value) {
assert(value != null);
if (_textWidthBasis == value)
return;
_textWidthBasis = value;
_paragraph = null;
_needsLayout = true;
}
ui.Paragraph _layoutTemplate; ui.Paragraph _layoutTemplate;
...@@ -317,7 +347,9 @@ class TextPainter { ...@@ -317,7 +347,9 @@ class TextPainter {
/// Valid only after [layout] has been called. /// Valid only after [layout] has been called.
double get width { double get width {
assert(!_needsLayout); assert(!_needsLayout);
return _applyFloatingPointHack(_paragraph.width); return _applyFloatingPointHack(
textWidthBasis == TextWidthBasis.longestLine ? _paragraph.longestLine : _paragraph.width,
);
} }
/// The vertical space required to paint this text. /// The vertical space required to paint this text.
......
...@@ -49,6 +49,7 @@ class RenderParagraph extends RenderBox { ...@@ -49,6 +49,7 @@ class RenderParagraph extends RenderBox {
TextOverflow overflow = TextOverflow.clip, TextOverflow overflow = TextOverflow.clip,
double textScaleFactor = 1.0, double textScaleFactor = 1.0,
int maxLines, int maxLines,
TextWidthBasis textWidthBasis = TextWidthBasis.parent,
Locale locale, Locale locale,
StrutStyle strutStyle, StrutStyle strutStyle,
}) : assert(text != null), }) : assert(text != null),
...@@ -59,6 +60,7 @@ class RenderParagraph extends RenderBox { ...@@ -59,6 +60,7 @@ class RenderParagraph extends RenderBox {
assert(overflow != null), assert(overflow != null),
assert(textScaleFactor != null), assert(textScaleFactor != null),
assert(maxLines == null || maxLines > 0), assert(maxLines == null || maxLines > 0),
assert(textWidthBasis != null),
_softWrap = softWrap, _softWrap = softWrap,
_overflow = overflow, _overflow = overflow,
_textPainter = TextPainter( _textPainter = TextPainter(
...@@ -70,6 +72,7 @@ class RenderParagraph extends RenderBox { ...@@ -70,6 +72,7 @@ class RenderParagraph extends RenderBox {
ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null, ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
locale: locale, locale: locale,
strutStyle: strutStyle, strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
); );
final TextPainter _textPainter; final TextPainter _textPainter;
...@@ -212,6 +215,17 @@ class RenderParagraph extends RenderBox { ...@@ -212,6 +215,17 @@ class RenderParagraph extends RenderBox {
markNeedsLayout(); markNeedsLayout();
} }
/// {@macro flutter.widgets.basic.TextWidthBasis}
TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis;
set textWidthBasis(TextWidthBasis value) {
assert(value != null);
if (_textPainter.textWidthBasis == value)
return;
_textPainter.textWidthBasis = value;
_overflowShader = null;
markNeedsLayout();
}
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) { void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis; final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
_textPainter.layout(minWidth: minWidth, maxWidth: widthMatters ? maxWidth : double.infinity); _textPainter.layout(minWidth: minWidth, maxWidth: widthMatters ? maxWidth : double.infinity);
......
...@@ -4811,12 +4811,14 @@ class RichText extends LeafRenderObjectWidget { ...@@ -4811,12 +4811,14 @@ class RichText extends LeafRenderObjectWidget {
this.maxLines, this.maxLines,
this.locale, this.locale,
this.strutStyle, this.strutStyle,
this.textWidthBasis = TextWidthBasis.parent,
}) : assert(text != null), }) : assert(text != null),
assert(textAlign != null), assert(textAlign != null),
assert(softWrap != null), assert(softWrap != null),
assert(overflow != null), assert(overflow != null),
assert(textScaleFactor != null), assert(textScaleFactor != null),
assert(maxLines == null || maxLines > 0), assert(maxLines == null || maxLines > 0),
assert(textWidthBasis != null),
super(key: key); super(key: key);
/// The text to display in this widget. /// The text to display in this widget.
...@@ -4875,6 +4877,9 @@ class RichText extends LeafRenderObjectWidget { ...@@ -4875,6 +4877,9 @@ class RichText extends LeafRenderObjectWidget {
/// {@macro flutter.painting.textPainter.strutStyle} /// {@macro flutter.painting.textPainter.strutStyle}
final StrutStyle strutStyle; final StrutStyle strutStyle;
/// {@macro flutter.widgets.text.DefaultTextStyle.textWidthBasis}
final TextWidthBasis textWidthBasis;
@override @override
RenderParagraph createRenderObject(BuildContext context) { RenderParagraph createRenderObject(BuildContext context) {
assert(textDirection != null || debugCheckHasDirectionality(context)); assert(textDirection != null || debugCheckHasDirectionality(context));
...@@ -4886,6 +4891,7 @@ class RichText extends LeafRenderObjectWidget { ...@@ -4886,6 +4891,7 @@ class RichText extends LeafRenderObjectWidget {
textScaleFactor: textScaleFactor, textScaleFactor: textScaleFactor,
maxLines: maxLines, maxLines: maxLines,
strutStyle: strutStyle, strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
locale: locale ?? Localizations.localeOf(context, nullOk: true), locale: locale ?? Localizations.localeOf(context, nullOk: true),
); );
} }
...@@ -4902,6 +4908,7 @@ class RichText extends LeafRenderObjectWidget { ...@@ -4902,6 +4908,7 @@ class RichText extends LeafRenderObjectWidget {
..textScaleFactor = textScaleFactor ..textScaleFactor = textScaleFactor
..maxLines = maxLines ..maxLines = maxLines
..strutStyle = strutStyle ..strutStyle = strutStyle
..textWidthBasis = textWidthBasis
..locale = locale ?? Localizations.localeOf(context, nullOk: true); ..locale = locale ?? Localizations.localeOf(context, nullOk: true);
} }
...@@ -4914,6 +4921,7 @@ class RichText extends LeafRenderObjectWidget { ...@@ -4914,6 +4921,7 @@ class RichText extends LeafRenderObjectWidget {
properties.add(EnumProperty<TextOverflow>('overflow', overflow, defaultValue: TextOverflow.clip)); properties.add(EnumProperty<TextOverflow>('overflow', overflow, defaultValue: TextOverflow.clip));
properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: 1.0)); properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: 1.0));
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited')); properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
properties.add(EnumProperty<TextWidthBasis>('textWidthBasis', textWidthBasis, defaultValue: TextWidthBasis.parent));
properties.add(StringProperty('text', text.toPlainText())); properties.add(StringProperty('text', text.toPlainText()));
} }
} }
......
...@@ -33,12 +33,14 @@ class DefaultTextStyle extends InheritedWidget { ...@@ -33,12 +33,14 @@ class DefaultTextStyle extends InheritedWidget {
this.softWrap = true, this.softWrap = true,
this.overflow = TextOverflow.clip, this.overflow = TextOverflow.clip,
this.maxLines, this.maxLines,
this.textWidthBasis = TextWidthBasis.parent,
@required Widget child, @required Widget child,
}) : assert(style != null), }) : assert(style != null),
assert(softWrap != null), assert(softWrap != null),
assert(overflow != null), assert(overflow != null),
assert(maxLines == null || maxLines > 0), assert(maxLines == null || maxLines > 0),
assert(child != null), assert(child != null),
assert(textWidthBasis != null),
super(key: key, child: child); super(key: key, child: child);
/// A const-constructible default text style that provides fallback values. /// A const-constructible default text style that provides fallback values.
...@@ -52,7 +54,8 @@ class DefaultTextStyle extends InheritedWidget { ...@@ -52,7 +54,8 @@ class DefaultTextStyle extends InheritedWidget {
textAlign = null, textAlign = null,
softWrap = true, softWrap = true,
maxLines = null, maxLines = null,
overflow = TextOverflow.clip; overflow = TextOverflow.clip,
textWidthBasis = TextWidthBasis.parent;
/// Creates a default text style that overrides the text styles in scope at /// Creates a default text style that overrides the text styles in scope at
/// this point in the widget tree. /// this point in the widget tree.
...@@ -77,6 +80,7 @@ class DefaultTextStyle extends InheritedWidget { ...@@ -77,6 +80,7 @@ class DefaultTextStyle extends InheritedWidget {
bool softWrap, bool softWrap,
TextOverflow overflow, TextOverflow overflow,
int maxLines, int maxLines,
TextWidthBasis textWidthBasis,
@required Widget child, @required Widget child,
}) { }) {
assert(child != null); assert(child != null);
...@@ -90,6 +94,7 @@ class DefaultTextStyle extends InheritedWidget { ...@@ -90,6 +94,7 @@ class DefaultTextStyle extends InheritedWidget {
softWrap: softWrap ?? parent.softWrap, softWrap: softWrap ?? parent.softWrap,
overflow: overflow ?? parent.overflow, overflow: overflow ?? parent.overflow,
maxLines: maxLines ?? parent.maxLines, maxLines: maxLines ?? parent.maxLines,
textWidthBasis: textWidthBasis ?? parent.textWidthBasis,
child: child, child: child,
); );
}, },
...@@ -121,6 +126,10 @@ class DefaultTextStyle extends InheritedWidget { ...@@ -121,6 +126,10 @@ class DefaultTextStyle extends InheritedWidget {
/// [Text.maxLines]. /// [Text.maxLines].
final int maxLines; final int maxLines;
/// The strategy to use when calculating the width of the Text.
/// See [TextWidthBasis] for possible values and their implications.
final TextWidthBasis textWidthBasis;
/// The closest instance of this class that encloses the given context. /// The closest instance of this class that encloses the given context.
/// ///
/// If no such instance exists, returns an instance created by /// If no such instance exists, returns an instance created by
...@@ -141,7 +150,8 @@ class DefaultTextStyle extends InheritedWidget { ...@@ -141,7 +150,8 @@ class DefaultTextStyle extends InheritedWidget {
textAlign != oldWidget.textAlign || textAlign != oldWidget.textAlign ||
softWrap != oldWidget.softWrap || softWrap != oldWidget.softWrap ||
overflow != oldWidget.overflow || overflow != oldWidget.overflow ||
maxLines != oldWidget.maxLines; maxLines != oldWidget.maxLines ||
textWidthBasis != oldWidget.textWidthBasis;
} }
@override @override
...@@ -152,6 +162,7 @@ class DefaultTextStyle extends InheritedWidget { ...@@ -152,6 +162,7 @@ class DefaultTextStyle extends InheritedWidget {
properties.add(FlagProperty('softWrap', value: softWrap, ifTrue: 'wrapping at box width', ifFalse: 'no wrapping except at line break characters', showName: true)); properties.add(FlagProperty('softWrap', value: softWrap, ifTrue: 'wrapping at box width', ifFalse: 'no wrapping except at line break characters', showName: true));
properties.add(EnumProperty<TextOverflow>('overflow', overflow, defaultValue: null)); properties.add(EnumProperty<TextOverflow>('overflow', overflow, defaultValue: null));
properties.add(IntProperty('maxLines', maxLines, defaultValue: null)); properties.add(IntProperty('maxLines', maxLines, defaultValue: null));
properties.add(EnumProperty<TextWidthBasis>('textWidthBasis', textWidthBasis, defaultValue: TextWidthBasis.parent));
} }
} }
...@@ -237,6 +248,7 @@ class Text extends StatelessWidget { ...@@ -237,6 +248,7 @@ class Text extends StatelessWidget {
this.textScaleFactor, this.textScaleFactor,
this.maxLines, this.maxLines,
this.semanticsLabel, this.semanticsLabel,
this.textWidthBasis,
}) : assert( }) : assert(
data != null, data != null,
'A non-null String must be provided to a Text widget.', 'A non-null String must be provided to a Text widget.',
...@@ -260,6 +272,7 @@ class Text extends StatelessWidget { ...@@ -260,6 +272,7 @@ class Text extends StatelessWidget {
this.textScaleFactor, this.textScaleFactor,
this.maxLines, this.maxLines,
this.semanticsLabel, this.semanticsLabel,
this.textWidthBasis,
}) : assert( }) : assert(
textSpan != null, textSpan != null,
'A non-null TextSpan must be provided to a Text.rich widget.', 'A non-null TextSpan must be provided to a Text.rich widget.',
...@@ -359,6 +372,9 @@ class Text extends StatelessWidget { ...@@ -359,6 +372,9 @@ class Text extends StatelessWidget {
/// ``` /// ```
final String semanticsLabel; final String semanticsLabel;
/// {@macro flutter.dart:ui.text.TextWidthBasis}
final TextWidthBasis textWidthBasis;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
...@@ -376,6 +392,7 @@ class Text extends StatelessWidget { ...@@ -376,6 +392,7 @@ class Text extends StatelessWidget {
textScaleFactor: textScaleFactor ?? MediaQuery.textScaleFactorOf(context), textScaleFactor: textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
maxLines: maxLines ?? defaultTextStyle.maxLines, maxLines: maxLines ?? defaultTextStyle.maxLines,
strutStyle: strutStyle, strutStyle: strutStyle,
textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
text: TextSpan( text: TextSpan(
style: effectiveTextStyle, style: effectiveTextStyle,
text: data, text: data,
......
...@@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart'; ...@@ -6,6 +6,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
...@@ -363,6 +364,43 @@ void main() { ...@@ -363,6 +364,43 @@ void main() {
expect(find.byType(Text), isNot(paints..clipRect())); expect(find.byType(Text), isNot(paints..clipRect()));
}); });
testWidgets('textWidthBasis affects the width of a Text widget', (WidgetTester tester) async {
Future<void> createText(TextWidthBasis textWidthBasis) {
return tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: Container(
// Each word takes up more than a half of a line. Together they
// wrap onto two lines, but leave a lot of extra space.
child: Text('twowordsthateachtakeupmorethanhalfof alineoftextsothattheywrapwithlotsofextraspace',
textDirection: TextDirection.ltr,
textWidthBasis: textWidthBasis,
),
),
),
),
),
);
}
const double fontHeight = 14.0;
const double screenWidth = 800.0;
// When textWidthBasis is parent, takes up full screen width.
await createText(TextWidthBasis.parent);
final Size textSizeParent = tester.getSize(find.byType(Text));
expect(textSizeParent.width, equals(screenWidth));
expect(textSizeParent.height, equals(fontHeight * 2));
// When textWidthBasis is longestLine, sets the width to as small as
// possible for the two lines.
await createText(TextWidthBasis.longestLine);
final Size textSizeLongestLine = tester.getSize(find.byType(Text));
expect(textSizeLongestLine.width, equals(630.0));
expect(textSizeLongestLine.height, equals(fontHeight * 2));
});
} }
Future<void> _pumpTextWidget({ WidgetTester tester, String text, TextOverflow overflow }) { Future<void> _pumpTextWidget({ WidgetTester tester, String text, TextOverflow overflow }) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment