Commit fb4dbf45 authored by Adam Barth's avatar Adam Barth

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.
parent 755a180a
......@@ -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);
......@@ -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)));
......
......@@ -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.'))
)
);
}
......@@ -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.
......
......@@ -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)
]
)
);
}
......
......@@ -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
......
......@@ -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.
......@@ -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;
}
......
......@@ -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<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;
......
......@@ -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();
};
}
......
......@@ -1405,12 +1405,12 @@ 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);
}
......@@ -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" | [<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({
......@@ -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) {
......
......@@ -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();
......
......@@ -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, <TextSpan>[
new PlainTextSpan(value.composing.textBefore(value.text)),
new StyledTextSpan(composingStyle, <TextSpan>[
new PlainTextSpan(value.composing.textInside(value.text))
]),
new PlainTextSpan(value.composing.textAfter(value.text))
return new TextSpan(
style: style,
children: <TextSpan>[
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<int>.filled(text.length, 0x2022));
return new StyledTextSpan(style, <TextSpan>[ new PlainTextSpan(text) ]);
return new TextSpan(style: style, text: text);
}
}
......@@ -172,7 +172,7 @@ class _SemanticsDebuggerEntry {
message = message.trim();
if (message != '') {
textPainter ??= new TextPainter();
textPainter.text = new StyledTextSpan(textStyles, <TextSpan>[new PlainTextSpan(message)]);
textPainter.text = new TextSpan(style: textStyles, text: message);
textPainter.maxWidth = rect.width;
textPainter.maxHeight = rect.height;
textPainter.layout();
......
......@@ -15,11 +15,9 @@ class TestBlockPainter extends Painter {
void main() {
test('block intrinsics', () {
RenderParagraph paragraph = new RenderParagraph(
new StyledTextSpan(
new TextStyle(
height: 1.0
),
<TextSpan>[new PlainTextSpan('Hello World')]
new TextSpan(
style: new TextStyle(height: 1.0),
text: 'Hello World'
)
);
const BoxConstraints unconstrained = const BoxConstraints();
......
......@@ -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)
),
......
......@@ -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>[textSpan]);
_painter = new TextPainter(styledTextSpan);
_painter = new TextPainter(new TextSpan(style: _textStyle, text: _text));
_painter.maxWidth = double.INFINITY;
_painter.minWidth = 0.0;
......
......@@ -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,
<TextSpan>[new PlainTextSpan("${data.indicatorText}")]
TextSpan text = new TextSpan(
style: _textTheme.body1,
text: '${data.indicatorText}'
);
_indicator.labelPainter = new TextPainter(text)
..maxWidth = markerRect.width
......
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