Skip to content

Commit

Permalink
Improve TextSpan
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
abarth committed Feb 24, 2016
1 parent 755a180 commit fb4dbf4
Show file tree
Hide file tree
Showing 16 changed files with 140 additions and 163 deletions.
10 changes: 5 additions & 5 deletions examples/layers/rendering/flex_layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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, <TextSpan>[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, <TextSpan>[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, <TextSpan>[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, <TextSpan>[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);
Expand All @@ -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, <TextSpan>[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)));
Expand Down
2 changes: 1 addition & 1 deletion examples/layers/rendering/hello_world.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.'))
)
);
}
6 changes: 3 additions & 3 deletions examples/layers/rendering/touch_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
<TextSpan>[ 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.
Expand Down
19 changes: 17 additions & 2 deletions examples/layers/widgets/styled_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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: <TextSpan>[
new TextSpan(
style: _kBold,
children: <TextSpan>[
new TextSpan(
style: _kUnderline,
text: name
),
new TextSpan(text: ':')
]
),
new TextSpan(text: text)
]
)
);
}

Expand Down
4 changes: 1 addition & 3 deletions packages/flutter/lib/src/material/time_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,7 @@ List<TextPainter> _initPainters(List<String> 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
Expand Down
130 changes: 62 additions & 68 deletions packages/flutter/lib/src/painting/text_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/// An immutable span of unstyled text.
class PlainTextSpan extends TextSpan {
const PlainTextSpan(this.text);
class TextSpan {
const TextSpan({
this.style,
this.text,
this.children
});

/// The style to apply to the text and the children.
final TextStyle style;

/// The text contained in the span.
///
/// If both text and children are non-null, the text will preceed the
/// children.
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.
final TextStyle style;

/// The children to which the style is applied.
/// Additional spans to include as children.
///
/// If both text and children are non-null, the text will preceed the
/// children.
final List<TextSpan> 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);
}
}
if (hasStyle)
builder.pop();
}

void writePlainText(StringBuffer result) {
if (text != null)
result.write(text);
if (children != null) {
for (TextSpan child in children)
child.writePlainText(result);
}
builder.pop();
}

ui.ParagraphStyle get paragraphStyle => style.paragraphStyle;
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<String> result = <String>[];
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.
Expand All @@ -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;
}

Expand Down
8 changes: 4 additions & 4 deletions packages/flutter/lib/src/rendering/editable_line.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -49,12 +49,12 @@ class RenderEditableLine extends RenderBox {
ValueChanged<TextSelection> 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;
Expand Down
4 changes: 3 additions & 1 deletion packages/flutter/lib/src/rendering/paragraph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ class RenderParagraph extends RenderBox {

Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
yield (SemanticsNode node) {
node.label = text.toPlainText();
StringBuffer buffer = new StringBuffer();
text.writePlainText(buffer);
node.label = buffer.toString();
};
}

Expand Down
65 changes: 17 additions & 48 deletions packages/flutter/lib/src/widgets/basic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1405,58 +1405,24 @@ class Flexible extends ParentDataWidget<Flex> {
}
}

/// 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);
}

final TextSpan text;

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" | [<text-style> <elements>*]``
///
/// 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({
Expand Down Expand Up @@ -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, <TextSpan>[text]);
return new RawText(text: text);
return new RichText(
text: new TextSpan(
style: _getEffectiveStyle(context),
text: data
)
);
}

void debugFillDescription(List<String> description) {
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter/lib/src/widgets/checked_mode_banner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class _CheckedModeBannerPainter extends CustomPainter {
);

static final TextPainter textPainter = new TextPainter()
..text = new StyledTextSpan(kTextStyles, <TextSpan>[new PlainTextSpan('SLOW MODE')])
..text = new TextSpan(style: kTextStyles, text: 'SLOW MODE')
..maxWidth = kOffset * 2.0
..maxHeight = kHeight
..layout();
Expand Down
Loading

0 comments on commit fb4dbf4

Please sign in to comment.