From fb4dbf4584d02e90620a3cf25d195eb164a518bf Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Wed, 24 Feb 2016 13:48:56 -0800 Subject: [PATCH] Improve TextSpan Now we just have one TextSpan class that handles both simple strings, trees of children, and styling both. This approach simplifies the interface for most clients. This patch also removes StyledText, which was weakly typed and tricky to use correctly. The replacement is RichText, which is strongly typed and uses TextSpan. --- examples/layers/rendering/flex_layout.dart | 10 +- examples/layers/rendering/hello_world.dart | 2 +- examples/layers/rendering/touch_input.dart | 6 +- examples/layers/widgets/styled_text.dart | 19 ++- .../flutter/lib/src/material/time_picker.dart | 4 +- .../lib/src/painting/text_painter.dart | 132 +++++++++--------- .../lib/src/rendering/editable_line.dart | 8 +- .../flutter/lib/src/rendering/paragraph.dart | 4 +- packages/flutter/lib/src/widgets/basic.dart | 65 +++------ .../lib/src/widgets/checked_mode_banner.dart | 2 +- .../flutter/lib/src/widgets/editable.dart | 23 +-- .../lib/src/widgets/semantics_debugger.dart | 2 +- .../flutter/test/rendering/block_test.dart | 8 +- .../flutter/test/rendering/overflow_test.dart | 4 +- packages/flutter_sprites/lib/src/label.dart | 4 +- packages/playfair/lib/src/base.dart | 12 +- 16 files changed, 141 insertions(+), 164 deletions(-) diff --git a/examples/layers/rendering/flex_layout.dart b/examples/layers/rendering/flex_layout.dart index 945c83ea34..adaa4bf6db 100644 --- a/examples/layers/rendering/flex_layout.dart +++ b/examples/layers/rendering/flex_layout.dart @@ -14,24 +14,24 @@ void main() { void addAlignmentRow(FlexAlignItems alignItems) { TextStyle style = const TextStyle(color: const Color(0xFF000000)); - RenderParagraph paragraph = new RenderParagraph(new StyledTextSpan(style, [new PlainTextSpan('$alignItems')])); + RenderParagraph paragraph = new RenderParagraph(new TextSpan(style: style, text: '$alignItems')); table.add(new RenderPadding(child: paragraph, padding: new EdgeDims.only(top: 20.0))); RenderFlex row = new RenderFlex(alignItems: alignItems, textBaseline: TextBaseline.alphabetic); style = new TextStyle(fontSize: 15.0, color: const Color(0xFF000000)); row.add(new RenderDecoratedBox( decoration: new BoxDecoration(backgroundColor: const Color(0x7FFFCCCC)), - child: new RenderParagraph(new StyledTextSpan(style, [new PlainTextSpan('foo foo foo')])) + child: new RenderParagraph(new TextSpan(style: style, text: 'foo foo foo')) )); style = new TextStyle(fontSize: 10.0, color: const Color(0xFF000000)); row.add(new RenderDecoratedBox( decoration: new BoxDecoration(backgroundColor: const Color(0x7FCCFFCC)), - child: new RenderParagraph(new StyledTextSpan(style, [new PlainTextSpan('foo foo foo')])) + child: new RenderParagraph(new TextSpan(style: style, text: 'foo foo foo')) )); RenderFlex subrow = new RenderFlex(alignItems: alignItems, textBaseline: TextBaseline.alphabetic); style = new TextStyle(fontSize: 25.0, color: const Color(0xFF000000)); subrow.add(new RenderDecoratedBox( decoration: new BoxDecoration(backgroundColor: const Color(0x7FCCCCFF)), - child: new RenderParagraph(new StyledTextSpan(style, [new PlainTextSpan('foo foo foo foo')])) + child: new RenderParagraph(new TextSpan(style: style, text: 'foo foo foo foo')) )); subrow.add(new RenderSolidColorBox(const Color(0x7FCCFFFF), desiredSize: new Size(30.0, 40.0))); row.add(subrow); @@ -48,7 +48,7 @@ void main() { void addJustificationRow(FlexJustifyContent justify) { const TextStyle style = const TextStyle(color: const Color(0xFF000000)); - RenderParagraph paragraph = new RenderParagraph(new StyledTextSpan(style, [new PlainTextSpan('$justify')])); + RenderParagraph paragraph = new RenderParagraph(new TextSpan(style: style, text: '$justify')); table.add(new RenderPadding(child: paragraph, padding: new EdgeDims.only(top: 20.0))); RenderFlex row = new RenderFlex(direction: FlexDirection.horizontal); row.add(new RenderSolidColorBox(const Color(0xFFFFCCCC), desiredSize: new Size(80.0, 60.0))); diff --git a/examples/layers/rendering/hello_world.dart b/examples/layers/rendering/hello_world.dart index 6716b0dd9e..50b6709d56 100644 --- a/examples/layers/rendering/hello_world.dart +++ b/examples/layers/rendering/hello_world.dart @@ -16,7 +16,7 @@ void main() { alignment: const FractionalOffset(0.5, 0.5), // We use a RenderParagraph to display the text 'Hello, world.' without // any explicit styling. - child: new RenderParagraph(new PlainTextSpan('Hello, world.')) + child: new RenderParagraph(new TextSpan(text: 'Hello, world.')) ) ); } diff --git a/examples/layers/rendering/touch_input.dart b/examples/layers/rendering/touch_input.dart index 68f442122e..05a50fcabb 100644 --- a/examples/layers/rendering/touch_input.dart +++ b/examples/layers/rendering/touch_input.dart @@ -97,9 +97,9 @@ class RenderDots extends RenderBox { void main() { // Create some styled text to tell the user to interact with the app. RenderParagraph paragraph = new RenderParagraph( - new StyledTextSpan( - new TextStyle(color: Colors.black87), - [ new PlainTextSpan("Touch me!") ] + new TextSpan( + style: new TextStyle(color: Colors.black87), + text: "Touch me!" ) ); // A stack is a render object that layers its children on top of each other. diff --git a/examples/layers/widgets/styled_text.dart b/examples/layers/widgets/styled_text.dart index 646f6e52e7..abd2ec9bc4 100644 --- a/examples/layers/widgets/styled_text.dart +++ b/examples/layers/widgets/styled_text.dart @@ -34,9 +34,24 @@ final TextStyle _kUnderline = const TextStyle( Widget toStyledText(String name, String text) { TextStyle lineStyle = (name == "Dave") ? _kDaveStyle : _kHalStyle; - return new StyledText( + return new RichText( key: new Key(text), - elements: [lineStyle, [_kBold, [_kUnderline, name], ":"], text] + text: new TextSpan( + style: lineStyle, + children: [ + new TextSpan( + style: _kBold, + children: [ + new TextSpan( + style: _kUnderline, + text: name + ), + new TextSpan(text: ':') + ] + ), + new TextSpan(text: text) + ] + ) ); } diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index e4a262d353..59294d39fc 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -240,9 +240,7 @@ List _initPainters(List labels) { for (int i = 0; i < painters.length; ++i) { String label = labels[i]; TextPainter painter = new TextPainter( - new StyledTextSpan(style, [ - new PlainTextSpan(label) - ]) + new TextSpan(style: style, text: label) ); painter ..maxWidth = double.INFINITY diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart index 7723474160..c91f21dfd9 100644 --- a/packages/flutter/lib/src/painting/text_painter.dart +++ b/packages/flutter/lib/src/painting/text_painter.dart @@ -9,92 +9,86 @@ import 'text_editing.dart'; import 'text_style.dart'; /// An immutable span of text. -abstract class TextSpan { - // This class must be immutable, because we won't notice when it changes. - const TextSpan(); - void build(ui.ParagraphBuilder builder); - ui.ParagraphStyle get paragraphStyle => null; - String toPlainText(); // for semantics - String toString([String prefix = '']); // for debugging -} +class TextSpan { + const TextSpan({ + this.style, + this.text, + this.children + }); -/// An immutable span of unstyled text. -class PlainTextSpan extends TextSpan { - const PlainTextSpan(this.text); - - /// The text contained in the span. - final String text; - - void build(ui.ParagraphBuilder builder) { - assert(text != null); - builder.addText(text); - } - - bool operator ==(dynamic other) { - if (other is! PlainTextSpan) - return false; - final PlainTextSpan typedOther = other; - return text == typedOther.text; - } - - int get hashCode => text.hashCode; - - String toPlainText() => text; - String toString([String prefix = '']) => '$prefix$runtimeType: "$text"'; -} - -/// An immutable text span that applies a style to a list of children. -class StyledTextSpan extends TextSpan { - const StyledTextSpan(this.style, this.children); - - /// The style to apply to the children. + /// The style to apply to the text and the children. final TextStyle style; - /// The children to which the style is applied. + /// The text contained in the span. + /// + /// If both text and children are non-null, the text will preceed the + /// children. + final String text; + + /// Additional spans to include as children. + /// + /// If both text and children are non-null, the text will preceed the + /// children. final List children; void build(ui.ParagraphBuilder builder) { - assert(style != null); - assert(children != null); - builder.pushStyle(style.textStyle); - for (TextSpan child in children) { - assert(child != null); - child.build(builder); + final bool hasStyle = style != null; + if (hasStyle) + builder.pushStyle(style.textStyle); + if (text != null) + builder.addText(text); + if (children != null) { + for (TextSpan child in children) { + assert(child != null); + child.build(builder); + } } - builder.pop(); + if (hasStyle) + builder.pop(); } - ui.ParagraphStyle get paragraphStyle => style.paragraphStyle; + void writePlainText(StringBuffer result) { + if (text != null) + result.write(text); + if (children != null) { + for (TextSpan child in children) + child.writePlainText(result); + } + } + + String toString([String prefix = '']) { + StringBuffer buffer = new StringBuffer(); + buffer.writeln('$prefix$runtimeType:'); + String indent = '$prefix '; + buffer.writeln(style.toString(indent)); + if (text != null) + buffer.writeln('$indent"$text"'); + for (TextSpan child in children) + buffer.writeln(child.toString(indent)); + return buffer.toString(); + } bool operator ==(dynamic other) { if (identical(this, other)) return true; - if (other is! StyledTextSpan) + if (other is! TextSpan) return false; - final StyledTextSpan typedOther = other; - if (style != typedOther.style || - children.length != typedOther.children.length) + final TextSpan typedOther = other; + if (typedOther.text != text) return false; - for (int i = 0; i < children.length; ++i) { - if (children[i] != typedOther.children[i]) - return false; + if (typedOther.style != style) + return false; + if ((typedOther.children == null) != (children == null)) + return false; + if (children != null) { + for (int i = 0; i < children.length; ++i) { + if (typedOther.children[i] != children[i]) + return false; + } } return true; } - - int get hashCode => hashValues(style, hashList(children)); - - String toPlainText() => children.map((TextSpan child) => child.toPlainText()).join(); - - String toString([String prefix = '']) { - List result = []; - result.add('$prefix$runtimeType:'); - var indent = '$prefix '; - result.add('${style.toString(indent)}'); - for (TextSpan child in children) - result.add(child.toString(indent)); - return result.join('\n'); - } + int get hashCode => hashValues(style, text, hashList(children)); } /// An object that paints a [TextSpan] into a canvas. @@ -115,7 +109,7 @@ class TextPainter { _text = value; ui.ParagraphBuilder builder = new ui.ParagraphBuilder(); _text.build(builder); - _paragraph = builder.build(_text.paragraphStyle ?? new ui.ParagraphStyle()); + _paragraph = builder.build(_text.style?.paragraphStyle ?? new ui.ParagraphStyle()); _needsLayout = true; } diff --git a/packages/flutter/lib/src/rendering/editable_line.dart b/packages/flutter/lib/src/rendering/editable_line.dart index bc4ad25cae..05e7c21df7 100644 --- a/packages/flutter/lib/src/rendering/editable_line.dart +++ b/packages/flutter/lib/src/rendering/editable_line.dart @@ -19,7 +19,7 @@ final String _kZeroWidthSpace = new String.fromCharCode(0x200B); /// A single line of editable text. class RenderEditableLine extends RenderBox { RenderEditableLine({ - StyledTextSpan text, + TextSpan text, Color cursorColor, bool showCursor: false, Color selectionColor, @@ -49,12 +49,12 @@ class RenderEditableLine extends RenderBox { ValueChanged onSelectionChanged; /// The text to display - StyledTextSpan get text => _textPainter.text; + TextSpan get text => _textPainter.text; final TextPainter _textPainter; - void set text(StyledTextSpan value) { + void set text(TextSpan value) { if (_textPainter.text == value) return; - StyledTextSpan oldStyledText = _textPainter.text; + TextSpan oldStyledText = _textPainter.text; if (oldStyledText.style != value.style) _layoutTemplate = null; _textPainter.text = value; diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index 330af42451..b622c3f4b1 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -100,7 +100,9 @@ class RenderParagraph extends RenderBox { Iterable getSemanticAnnotators() sync* { yield (SemanticsNode node) { - node.label = text.toPlainText(); + StringBuffer buffer = new StringBuffer(); + text.writePlainText(buffer); + node.label = buffer.toString(); }; } diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 94141b5546..cbd126a4ee 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -1405,12 +1405,12 @@ class Flexible extends ParentDataWidget { } } -/// A raw paragraph of text. +/// A paragraph of rich text. /// /// This class is rarely used directly. Instead, consider using [Text], which /// integrates with [DefaultTextStyle]. -class RawText extends LeafRenderObjectWidget { - RawText({ Key key, this.text }) : super(key: key) { +class RichText extends LeafRenderObjectWidget { + RichText({ Key key, this.text }) : super(key: key) { assert(text != null); } @@ -1418,45 +1418,11 @@ class RawText extends LeafRenderObjectWidget { RenderParagraph createRenderObject() => new RenderParagraph(text); - void updateRenderObject(RenderParagraph renderObject, RawText oldWidget) { + void updateRenderObject(RenderParagraph renderObject, RichText oldWidget) { renderObject.text = text; } } -/// A convience widget for paragraphs of text with heterogeneous style. -/// -/// The elements parameter is a recursive list of lists that matches the -/// following grammar: -/// -/// `elements ::= "string" | [ *]`` -/// -/// Where "string" is text to display and text-style is an instance of -/// TextStyle. The text-style applies to all of the elements that follow. -class StyledText extends StatelessComponent { - StyledText({ this.elements, Key key }) : super(key: key) { - assert(_toSpan(elements) != null); - } - - /// The recursive list of lists that describes the text and style to paint. - final dynamic elements; - - TextSpan _toSpan(dynamic element) { - if (element is String) - return new PlainTextSpan(element); - if (element is Iterable) { - dynamic first = element.first; - if (first is! TextStyle) - throw new ArgumentError("First element of Iterable is a ${first.runtimeType} not a TextStyle"); - return new StyledTextSpan(first, element.skip(1).map(_toSpan).toList()); - } - throw new ArgumentError("Element is ${element.runtimeType} not a String or an Iterable"); - } - - Widget build(BuildContext context) { - return new RawText(text: _toSpan(elements)); - } -} - /// The text style to apply to descendant [Text] widgets without explicit style. class DefaultTextStyle extends InheritedWidget { DefaultTextStyle({ @@ -1504,17 +1470,20 @@ class Text extends StatelessComponent { /// replace the closest enclosing [DefaultTextStyle]. final TextStyle style; + TextStyle _getEffectiveStyle(BuildContext context) { + if (style == null || style.inherit) + return DefaultTextStyle.of(context)?.merge(style) ?? style; + else + return style; + } + Widget build(BuildContext context) { - TextSpan text = new PlainTextSpan(data); - TextStyle combinedStyle; - if (style == null || style.inherit) { - combinedStyle = DefaultTextStyle.of(context)?.merge(style) ?? style; - } else { - combinedStyle = style; - } - if (combinedStyle != null) - text = new StyledTextSpan(combinedStyle, [text]); - return new RawText(text: text); + return new RichText( + text: new TextSpan( + style: _getEffectiveStyle(context), + text: data + ) + ); } void debugFillDescription(List description) { diff --git a/packages/flutter/lib/src/widgets/checked_mode_banner.dart b/packages/flutter/lib/src/widgets/checked_mode_banner.dart index c92318f8dc..2aa8af634b 100644 --- a/packages/flutter/lib/src/widgets/checked_mode_banner.dart +++ b/packages/flutter/lib/src/widgets/checked_mode_banner.dart @@ -25,7 +25,7 @@ class _CheckedModeBannerPainter extends CustomPainter { ); static final TextPainter textPainter = new TextPainter() - ..text = new StyledTextSpan(kTextStyles, [new PlainTextSpan('SLOW MODE')]) + ..text = new TextSpan(style: kTextStyles, text: 'SLOW MODE') ..maxWidth = kOffset * 2.0 ..maxHeight = kHeight ..layout(); diff --git a/packages/flutter/lib/src/widgets/editable.dart b/packages/flutter/lib/src/widgets/editable.dart index b1bf089253..a0ed888550 100644 --- a/packages/flutter/lib/src/widgets/editable.dart +++ b/packages/flutter/lib/src/widgets/editable.dart @@ -112,7 +112,7 @@ class InputValue { return typedOther.text == text && typedOther.selection == selection && typedOther.composing == composing; - } + } int get hashCode => hashValues( text.hashCode, @@ -126,7 +126,7 @@ class InputValue { TextRange composing }) { return new InputValue ( - text: text ?? this.text, + text: text ?? this.text, selection: selection ?? this.selection, composing: composing ?? this.composing ); @@ -394,24 +394,27 @@ class _EditableLineWidget extends LeafRenderObjectWidget { ..paintOffset = paintOffset; } - StyledTextSpan get _styledTextSpan { + TextSpan get _styledTextSpan { if (!hideText && value.composing.isValid) { TextStyle composingStyle = style.merge( const TextStyle(decoration: TextDecoration.underline) ); - return new StyledTextSpan(style, [ - new PlainTextSpan(value.composing.textBefore(value.text)), - new StyledTextSpan(composingStyle, [ - new PlainTextSpan(value.composing.textInside(value.text)) - ]), - new PlainTextSpan(value.composing.textAfter(value.text)) + return new TextSpan( + style: style, + children: [ + new TextSpan(text: value.composing.textBefore(value.text)), + new TextSpan( + style: composingStyle, + text: value.composing.textInside(value.text) + ), + new TextSpan(text: value.composing.textAfter(value.text)) ]); } String text = value.text; if (hideText) text = new String.fromCharCodes(new List.filled(text.length, 0x2022)); - return new StyledTextSpan(style, [ new PlainTextSpan(text) ]); + return new TextSpan(style: style, text: text); } } diff --git a/packages/flutter/lib/src/widgets/semantics_debugger.dart b/packages/flutter/lib/src/widgets/semantics_debugger.dart index 0d4cefa1ef..45e92506ee 100644 --- a/packages/flutter/lib/src/widgets/semantics_debugger.dart +++ b/packages/flutter/lib/src/widgets/semantics_debugger.dart @@ -172,7 +172,7 @@ class _SemanticsDebuggerEntry { message = message.trim(); if (message != '') { textPainter ??= new TextPainter(); - textPainter.text = new StyledTextSpan(textStyles, [new PlainTextSpan(message)]); + textPainter.text = new TextSpan(style: textStyles, text: message); textPainter.maxWidth = rect.width; textPainter.maxHeight = rect.height; textPainter.layout(); diff --git a/packages/flutter/test/rendering/block_test.dart b/packages/flutter/test/rendering/block_test.dart index 11d23b450b..aa4b98f940 100644 --- a/packages/flutter/test/rendering/block_test.dart +++ b/packages/flutter/test/rendering/block_test.dart @@ -15,11 +15,9 @@ class TestBlockPainter extends Painter { void main() { test('block intrinsics', () { RenderParagraph paragraph = new RenderParagraph( - new StyledTextSpan( - new TextStyle( - height: 1.0 - ), - [new PlainTextSpan('Hello World')] + new TextSpan( + style: new TextStyle(height: 1.0), + text: 'Hello World' ) ); const BoxConstraints unconstrained = const BoxConstraints(); diff --git a/packages/flutter/test/rendering/overflow_test.dart b/packages/flutter/test/rendering/overflow_test.dart index d8fbacdd90..187fb44174 100644 --- a/packages/flutter/test/rendering/overflow_test.dart +++ b/packages/flutter/test/rendering/overflow_test.dart @@ -15,7 +15,7 @@ void main() { root = new RenderPositionedBox( child: new RenderCustomPaint( - child: child = text = new RenderParagraph(new PlainTextSpan('Hello World')), + child: child = text = new RenderParagraph(new TextSpan(text: 'Hello World')), painter: new TestCallbackPainter( onPaint: () { baseline1 = child.getDistanceToBaseline(TextBaseline.alphabetic); @@ -29,7 +29,7 @@ void main() { root = new RenderPositionedBox( child: new RenderCustomPaint( child: child = new RenderOverflowBox( - child: text = new RenderParagraph(new PlainTextSpan('Hello World')), + child: text = new RenderParagraph(new TextSpan(text: 'Hello World')), maxHeight: height1 / 2.0, alignment: const FractionalOffset(0.0, 0.0) ), diff --git a/packages/flutter_sprites/lib/src/label.dart b/packages/flutter_sprites/lib/src/label.dart index 69b8f09d7f..09bfa552c5 100644 --- a/packages/flutter_sprites/lib/src/label.dart +++ b/packages/flutter_sprites/lib/src/label.dart @@ -35,9 +35,7 @@ class Label extends Node { void paint(Canvas canvas) { if (_painter == null) { - PlainTextSpan textSpan = new PlainTextSpan(_text); - StyledTextSpan styledTextSpan = new StyledTextSpan(_textStyle, [textSpan]); - _painter = new TextPainter(styledTextSpan); + _painter = new TextPainter(new TextSpan(style: _textStyle, text: _text)); _painter.maxWidth = double.INFINITY; _painter.minWidth = 0.0; diff --git a/packages/playfair/lib/src/base.dart b/packages/playfair/lib/src/base.dart index e69edced7a..9da7512d5a 100644 --- a/packages/playfair/lib/src/base.dart +++ b/packages/playfair/lib/src/base.dart @@ -166,9 +166,9 @@ class ChartPainter { ..value = _roundToPlaces(data.startY + stepSize * i, data.roundToPlaces); if (gridline.value < data.startY || gridline.value > data.endY) continue; // TODO(jackson): Align things so this doesn't ever happen - TextSpan text = new StyledTextSpan( - _textTheme.body1, - [new PlainTextSpan("${gridline.value}")] + TextSpan text = new TextSpan( + style: _textTheme.body1, + text: '${gridline.value}' ); gridline.labelPainter = new TextPainter(text) ..maxWidth = _rect.width @@ -213,9 +213,9 @@ class ChartPainter { ..start = _convertPointToRectSpace(new Point(data.startX, data.indicatorLine), markerRect) ..end = _convertPointToRectSpace(new Point(data.endX, data.indicatorLine), markerRect); if (data.indicatorText != null) { - TextSpan text = new StyledTextSpan( - _textTheme.body1, - [new PlainTextSpan("${data.indicatorText}")] + TextSpan text = new TextSpan( + style: _textTheme.body1, + text: '${data.indicatorText}' ); _indicator.labelPainter = new TextPainter(text) ..maxWidth = markerRect.width