Commit e41120bc authored by Adam Barth's avatar Adam Barth

Improve the TextPainter API (#3621)

Instead of using properties, TextPainter now receives min and max width as
parameters to layout. Also, this patch integrates the intrinsic sizing logic
into the main layout function, which satisfies all the existing uses cases.
parent 53db3949
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
/// The Flutter painting library. /// The Flutter painting library.
/// ///
/// To use, import `package:flutter/painting.dart`. /// To use, import `package:flutter/painting.dart`.
/// ///
/// This library includes a variety of classes that wrap the Flutter /// This library includes a variety of classes that wrap the Flutter
...@@ -24,5 +24,6 @@ export 'src/painting/decoration.dart'; ...@@ -24,5 +24,6 @@ export 'src/painting/decoration.dart';
export 'src/painting/edge_insets.dart'; export 'src/painting/edge_insets.dart';
export 'src/painting/text_editing.dart'; export 'src/painting/text_editing.dart';
export 'src/painting/text_painter.dart'; export 'src/painting/text_painter.dart';
export 'src/painting/text_span.dart';
export 'src/painting/text_style.dart'; export 'src/painting/text_style.dart';
export 'src/painting/transforms.dart'; export 'src/painting/transforms.dart';
...@@ -253,7 +253,7 @@ class _RenderSlider extends RenderConstrainedBox { ...@@ -253,7 +253,7 @@ class _RenderSlider extends RenderConstrainedBox {
style: Typography.white.body1.copyWith(fontSize: 10.0), style: Typography.white.body1.copyWith(fontSize: 10.0),
text: newLabel text: newLabel
) )
..layoutToMaxIntrinsicWidth(); ..layout();
} else { } else {
_labelPainter.text = null; _labelPainter.text = null;
} }
......
...@@ -265,7 +265,7 @@ List<TextPainter> _initPainters(List<String> labels) { ...@@ -265,7 +265,7 @@ List<TextPainter> _initPainters(List<String> labels) {
String label = labels[i]; String label = labels[i];
painters[i] = new TextPainter( painters[i] = new TextPainter(
new TextSpan(style: style, text: label) new TextSpan(style: style, text: label)
)..layoutToMaxIntrinsicWidth(); )..layout();
} }
return painters; return painters;
} }
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui show ParagraphBuilder;
import 'package:flutter/gestures.dart';
import 'package:flutter/foundation.dart';
import 'basic_types.dart';
import 'text_editing.dart';
import 'text_style.dart';
// TODO(abarth): Should this be somewhere more general?
bool _deepEquals(List<Object> a, List<Object> b) {
if (a == null)
return b == null;
if (b == null || a.length != b.length)
return false;
for (int i = 0; i < a.length; ++i) {
if (a[i] != b[i])
return false;
}
return true;
}
/// An immutable span of text.
///
/// A [TextSpan] object can be styled using its [style] property.
/// The style will be applied to the [text] and the [children].
///
/// A [TextSpan] object can just have plain text, or it can have
/// children [TextSpan] objects with their own styles that (possibly
/// only partially) override the [style] of this object. If a
/// [TextSpan] has both [text] and [children], then the [text] is
/// treated as if it was an unstyled [TextSpan] at the start of the
/// [children] list.
///
/// To paint a [TextSpan] on a [Canvas], use a [TextPainter]. To display a text
/// span in a widget, use a [RichText]. For text with a single style, consider
/// using the [Text] widget.
///
/// See also:
///
/// * [Text]
/// * [RichText]
/// * [TextPainter]
class TextSpan {
/// Creates a [TextSpan] with the given values.
///
/// For the object to be useful, at least one of [text] or
/// [children] should be set.
const TextSpan({
this.style,
this.text,
this.children,
this.recognizer
});
/// 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;
/// Additional spans to include as children.
///
/// If both [text] and [children] are non-null, the text will preceed the
/// children.
///
/// Modifying the list after the [TextSpan] has been created is not
/// supported and may have unexpected results.
///
/// The list must not contain any nulls.
final List<TextSpan> children;
/// A gesture recognizer that will receive events that hit this text span.
///
/// [TextSpan] itself does not implement hit testing or event
/// dispatch. The owner of the [TextSpan] tree to which the object
/// belongs is responsible for dispatching events.
///
/// For an example, see [RenderParagraph] in the Flutter rendering library.
final GestureRecognizer recognizer;
/// Apply the [style], [text], and [children] of this object to the
/// given [ParagraphBuilder], from which a [Paragraph] can be obtained.
/// [Paragraph] objects can be drawn on [Canvas] objects.
///
/// Rather than using this directly, it's simpler to use the
/// [TextPainter] class to paint [TextSpan] objects onto [Canvas]
/// objects.
void build(ui.ParagraphBuilder builder) {
assert(debugAssertValid());
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();
}
/// Walks this text span and its decendants in pre-order and calls [visitor] for each span that has text.
bool visitTextSpan(bool visitor(TextSpan span)) {
if (text != null) {
if (!visitor(this))
return false;
}
if (children != null) {
for (TextSpan child in children) {
if (!child.visitTextSpan(visitor))
return false;
}
}
return true;
}
/// Returns the text span that contains the given position in the text.
TextSpan getSpanForPosition(TextPosition position) {
assert(debugAssertValid());
TextAffinity affinity = position.affinity;
int targetOffset = position.offset;
int offset = 0;
TextSpan result;
visitTextSpan((TextSpan span) {
assert(result == null);
int endOffset = offset + span.text.length;
if (targetOffset == offset && affinity == TextAffinity.downstream ||
targetOffset > offset && targetOffset < endOffset ||
targetOffset == endOffset && affinity == TextAffinity.upstream) {
result = span;
return false;
}
offset = endOffset;
return true;
});
return result;
}
/// Flattens the [TextSpan] tree into a single string.
///
/// Styles are not honored in this process.
String toPlainText() {
assert(debugAssertValid());
StringBuffer buffer = new StringBuffer();
visitTextSpan((TextSpan span) {
buffer.write(span.text);
return true;
});
return buffer.toString();
}
@override
String toString([String prefix = '']) {
StringBuffer buffer = new StringBuffer();
buffer.writeln('$prefix$runtimeType:');
String indent = '$prefix ';
if (style != null)
buffer.writeln(style.toString(indent));
if (text != null)
buffer.writeln('$indent"$text"');
if (children != null) {
for (TextSpan child in children) {
if (child != null) {
buffer.write(child.toString(indent));
} else {
buffer.writeln('$indent<null>');
}
}
}
if (style == null && text == null && children == null)
buffer.writeln('$indent(empty)');
return buffer.toString();
}
/// In checked mode, throws an exception if the object is not in a
/// valid configuration. Otherwise, returns true.
///
/// This is intended to be used as follows:
/// ```dart
/// assert(myTextSpan.debugAssertValid());
/// ```
bool debugAssertValid() {
assert(() {
if (!visitTextSpan((TextSpan span) {
if (span.children != null) {
for (TextSpan child in span.children) {
if (child == null)
return false;
}
}
return true;
})) {
throw new FlutterError(
'TextSpan contains a null child.\n'
'A TextSpan object with a non-null child list should not have any nulls in its child list.\n'
'The full text in question was:\n'
'${toString(" ")}'
);
}
return true;
});
return true;
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! TextSpan)
return false;
final TextSpan typedOther = other;
return typedOther.text == text
&& typedOther.style == style
&& typedOther.recognizer == recognizer
&& _deepEquals(typedOther.children, children);
}
@override
int get hashCode => hashValues(style, text, recognizer, hashList(children));
}
...@@ -289,12 +289,7 @@ class RenderChildView extends RenderBox { ...@@ -289,12 +289,7 @@ class RenderChildView extends RenderBox {
if (_view == null) { if (_view == null) {
_debugErrorMessage ??= new TextPainter() _debugErrorMessage ??= new TextPainter()
..text = new TextSpan(text: 'Child view are supported only when running in Mojo shell.'); ..text = new TextSpan(text: 'Child view are supported only when running in Mojo shell.');
_debugErrorMessage _debugErrorMessage.layout(minWidth: size.width, maxWidth: size.width);
..minWidth = size.width
..maxWidth = size.width
..minHeight = size.height
..maxHeight = size.height
..layout();
} }
return true; return true;
}); });
......
...@@ -48,12 +48,6 @@ class RenderEditableLine extends RenderBox { ...@@ -48,12 +48,6 @@ class RenderEditableLine extends RenderBox {
_selection = selection, _selection = selection,
_paintOffset = paintOffset { _paintOffset = paintOffset {
assert(!showCursor || cursorColor != null); assert(!showCursor || cursorColor != null);
// TODO(abarth): These min/max values should be the default for TextPainter.
_textPainter
..minWidth = 0.0
..maxWidth = double.INFINITY
..minHeight = 0.0
..maxHeight = double.INFINITY;
_tap = new TapGestureRecognizer() _tap = new TapGestureRecognizer()
..onTapDown = _handleTapDown ..onTapDown = _handleTapDown
..onTap = _handleTap ..onTap = _handleTap
...@@ -75,7 +69,6 @@ class RenderEditableLine extends RenderBox { ...@@ -75,7 +69,6 @@ class RenderEditableLine extends RenderBox {
if (oldStyledText.style != value.style) if (oldStyledText.style != value.style)
_layoutTemplate = null; _layoutTemplate = null;
_textPainter.text = value; _textPainter.text = value;
_constraintsForCurrentLayout = null;
markNeedsLayout(); markNeedsLayout();
} }
...@@ -258,27 +251,6 @@ class RenderEditableLine extends RenderBox { ...@@ -258,27 +251,6 @@ class RenderEditableLine extends RenderBox {
return new TextSelection(baseOffset: start, extentOffset: end); return new TextSelection(baseOffset: start, extentOffset: end);
} }
BoxConstraints _constraintsForCurrentLayout; // when null, we don't have a current layout
// TODO(abarth): This logic should live in TextPainter and be shared with RenderParagraph.
void _layoutText(BoxConstraints constraints) {
assert(constraints != null);
assert(constraints.debugAssertIsValid());
if (_constraintsForCurrentLayout == constraints)
return; // already cached this layout
_textPainter.maxWidth = constraints.maxWidth;
_textPainter.minWidth = constraints.minWidth;
_textPainter.minHeight = constraints.minHeight;
_textPainter.maxHeight = constraints.maxHeight;
_textPainter.layout();
// By default, we shrinkwrap to the intrinsic width.
double width = constraints.constrainWidth(_textPainter.maxIntrinsicWidth);
_textPainter.minWidth = width;
_textPainter.maxWidth = width;
_textPainter.layout();
_constraintsForCurrentLayout = constraints;
}
Rect _caretPrototype; Rect _caretPrototype;
@override @override
...@@ -287,7 +259,7 @@ class RenderEditableLine extends RenderBox { ...@@ -287,7 +259,7 @@ class RenderEditableLine extends RenderBox {
size = new Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight)); size = new Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight));
_caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, size.height - 2.0 * _kCaretHeightOffset); _caretPrototype = new Rect.fromLTWH(0.0, _kCaretHeightOffset, _kCaretWidth, size.height - 2.0 * _kCaretHeightOffset);
_selectionRects = null; _selectionRects = null;
_layoutText(new BoxConstraints(minHeight: constraints.minHeight, maxHeight: constraints.maxHeight)); _textPainter.layout();
Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height); Size contentSize = new Size(_textPainter.width + _kCaretGap + _kCaretWidth, _textPainter.height);
if (onPaintOffsetUpdateNeeded != null && (size != oldSize || contentSize != _contentSize)) if (onPaintOffsetUpdateNeeded != null && (size != oldSize || contentSize != _contentSize))
onPaintOffsetUpdateNeeded(new ViewportDimensions(containerSize: size, contentSize: contentSize)); onPaintOffsetUpdateNeeded(new ViewportDimensions(containerSize: size, contentSize: contentSize));
......
...@@ -20,8 +20,6 @@ class RenderParagraph extends RenderBox { ...@@ -20,8 +20,6 @@ class RenderParagraph extends RenderBox {
final TextPainter _textPainter; final TextPainter _textPainter;
BoxConstraints _constraintsForCurrentLayout; // when null, we don't have a current layout
/// The text to display /// The text to display
TextSpan get text => _textPainter.text; TextSpan get text => _textPainter.text;
void set text(TextSpan value) { void set text(TextSpan value) {
...@@ -29,27 +27,13 @@ class RenderParagraph extends RenderBox { ...@@ -29,27 +27,13 @@ class RenderParagraph extends RenderBox {
if (_textPainter.text == value) if (_textPainter.text == value)
return; return;
_textPainter.text = value; _textPainter.text = value;
_constraintsForCurrentLayout = null;
markNeedsLayout(); markNeedsLayout();
} }
// TODO(abarth): This logic should live in TextPainter and be shared with RenderEditableLine.
void _layoutText(BoxConstraints constraints) { void _layoutText(BoxConstraints constraints) {
assert(constraints != null); assert(constraints != null);
assert(constraints.debugAssertIsValid()); assert(constraints.debugAssertIsValid());
if (_constraintsForCurrentLayout == constraints) _textPainter.layout(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
return; // already cached this layout
_textPainter.maxWidth = constraints.maxWidth;
_textPainter.minWidth = constraints.minWidth;
_textPainter.minHeight = constraints.minHeight;
_textPainter.maxHeight = constraints.maxHeight;
_textPainter.layout();
// By default, we shrinkwrap to the intrinsic width.
double width = constraints.constrainWidth(_textPainter.maxIntrinsicWidth);
_textPainter.minWidth = width;
_textPainter.maxWidth = width;
_textPainter.layout();
_constraintsForCurrentLayout = constraints;
} }
@override @override
...@@ -66,7 +50,7 @@ class RenderParagraph extends RenderBox { ...@@ -66,7 +50,7 @@ class RenderParagraph extends RenderBox {
double _getIntrinsicHeight(BoxConstraints constraints) { double _getIntrinsicHeight(BoxConstraints constraints) {
_layoutText(constraints); _layoutText(constraints);
return constraints.constrainHeight(_textPainter.size.height); return constraints.constrainHeight(_textPainter.height);
} }
@override @override
......
...@@ -48,8 +48,7 @@ class BannerPainter extends CustomPainter { ...@@ -48,8 +48,7 @@ class BannerPainter extends CustomPainter {
final TextPainter textPainter = new TextPainter() final TextPainter textPainter = new TextPainter()
..text = new TextSpan(style: kTextStyles, text: message) ..text = new TextSpan(style: kTextStyles, text: message)
..maxWidth = kOffset * 2.0 ..layout(maxWidth: kOffset * 2.0);
..layout();
textPainter.paint(canvas, kRect.topLeft.toOffset() + new Offset(0.0, (kRect.height - textPainter.height) / 2.0)); textPainter.paint(canvas, kRect.topLeft.toOffset() + new Offset(0.0, (kRect.height - textPainter.height) / 2.0));
} }
......
...@@ -183,10 +183,9 @@ class _SemanticsDebuggerEntry { ...@@ -183,10 +183,9 @@ class _SemanticsDebuggerEntry {
message = message.trim(); message = message.trim();
if (message != '') { if (message != '') {
textPainter ??= new TextPainter(); textPainter ??= new TextPainter();
textPainter.text = new TextSpan(style: textStyles, text: message); textPainter
textPainter.maxWidth = rect.width; ..text = new TextSpan(style: textStyles, text: message)
textPainter.maxHeight = rect.height; ..layout(maxWidth: rect.width);
textPainter.layout();
} else { } else {
textPainter = null; textPainter = null;
} }
......
...@@ -36,17 +36,8 @@ class Label extends Node { ...@@ -36,17 +36,8 @@ class Label extends Node {
@override @override
void paint(Canvas canvas) { void paint(Canvas canvas) {
if (_painter == null) { if (_painter == null) {
_painter = new TextPainter(new TextSpan(style: _textStyle, text: _text)); _painter = new TextPainter(new TextSpan(style: _textStyle, text: _text))
..layout();
_painter.maxWidth = double.INFINITY;
_painter.minWidth = 0.0;
_painter.layout();
_width = _painter.maxIntrinsicWidth.ceil().toDouble();
_painter.maxWidth = _width;
_painter.minWidth = _width;
_painter.layout();
} }
Offset offset = Offset.zero; Offset offset = Offset.zero;
......
...@@ -175,8 +175,7 @@ class ChartPainter { ...@@ -175,8 +175,7 @@ class ChartPainter {
text: '${gridline.value}' text: '${gridline.value}'
); );
gridline.labelPainter = new TextPainter(text) gridline.labelPainter = new TextPainter(text)
..maxWidth = _rect.width ..layout(maxWidth: _rect.width);
..layout();
_horizontalGridlines.add(gridline); _horizontalGridlines.add(gridline);
yScaleWidth = math.max(yScaleWidth, gridline.labelPainter.maxIntrinsicWidth); yScaleWidth = math.max(yScaleWidth, gridline.labelPainter.maxIntrinsicWidth);
} }
...@@ -222,8 +221,7 @@ class ChartPainter { ...@@ -222,8 +221,7 @@ class ChartPainter {
text: '${data.indicatorText}' text: '${data.indicatorText}'
); );
_indicator.labelPainter = new TextPainter(text) _indicator.labelPainter = new TextPainter(text)
..maxWidth = markerRect.width ..layout(maxWidth: markerRect.width);
..layout();
_indicator.labelPosition = new Point( _indicator.labelPosition = new Point(
((_indicator.start.x + _indicator.end.x) / 2.0) - _indicator.labelPainter.maxIntrinsicWidth / 2.0, ((_indicator.start.x + _indicator.end.x) / 2.0) - _indicator.labelPainter.maxIntrinsicWidth / 2.0,
_indicator.start.y - _indicator.labelPainter.size.height - kIndicatorMargin _indicator.start.y - _indicator.labelPainter.size.height - kIndicatorMargin
......
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