paragraph.dart 7.97 KB
Newer Older
1 2 3 4
// 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.

5 6
import 'dart:ui' as ui;

7 8
import 'package:flutter/gestures.dart';

9 10
import 'box.dart';
import 'object.dart';
Hixie's avatar
Hixie committed
11
import 'semantics.dart';
12

13 14 15 16 17 18 19 20 21 22 23 24
/// How overflowing text should be handled.
enum TextOverflow {
  /// Clip the overflowing text to fix its container.
  clip,

  /// Fade the overflowing text to transparent.
  fade,

  /// Use an ellipsis to indicate that the text has overflowed.
  ellipsis,
}

25
/// A render object that displays a paragraph of text
26
class RenderParagraph extends RenderBox {
27 28 29
  /// Creates a paragraph render object.
  ///
  /// The [text], [overflow], and [softWrap] arguments must not be null.
30
  RenderParagraph(TextSpan text, {
31 32 33 34 35 36
    TextAlign textAlign,
    TextOverflow overflow: TextOverflow.clip,
    bool softWrap: true
  }) : _softWrap = softWrap,
       _overflow = overflow,
       _textPainter = new TextPainter(text: text, textAlign: textAlign) {
37
    assert(text != null);
38
    assert(text.debugAssertValid());
39 40
    assert(overflow != null);
    assert(softWrap != null);
41 42
  }

43
  final TextPainter _textPainter;
44

45
  /// The text to display
46
  TextSpan get text => _textPainter.text;
47
  set text(TextSpan value) {
48
    assert(value != null);
49
    if (_textPainter.text == value)
50
      return;
51
    _textPainter.text = value;
52 53
    _overflowPainter = null;
    _overflowShader = null;
54 55 56
    markNeedsLayout();
  }

57 58
  /// How the text should be aligned horizontally.
  TextAlign get textAlign => _textPainter.textAlign;
59
  set textAlign(TextAlign value) {
60 61 62 63 64 65
    if (_textPainter.textAlign == value)
      return;
    _textPainter.textAlign = value;
    markNeedsPaint();
  }

66 67 68 69 70
  /// Whether the text should break at soft line breaks.
  ///
  /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
  bool get softWrap => _softWrap;
  bool _softWrap;
71
  set softWrap(bool value) {
72 73 74 75 76 77 78 79 80 81
    assert(value != null);
    if (_softWrap == value)
      return;
    _softWrap = value;
    markNeedsLayout();
  }

  /// How visual overflow should be handled.
  TextOverflow get overflow => _overflow;
  TextOverflow _overflow;
82
  set overflow(TextOverflow value) {
83 84 85 86 87 88 89
    assert(value != null);
    if (_overflow == value)
      return;
    _overflow = value;
    markNeedsPaint();
  }

90 91
  void _layoutText({ double minWidth: 0.0, double maxWidth: double.INFINITY }) {
    _textPainter.layout(minWidth: minWidth, maxWidth: _softWrap ? maxWidth : double.INFINITY);
92 93
  }

94
  @override
95
  double computeMinIntrinsicWidth(double height) {
96 97
    _layoutText();
    return _textPainter.minIntrinsicWidth;
98 99
  }

100
  @override
101
  double computeMaxIntrinsicWidth(double height) {
102 103
    _layoutText();
    return _textPainter.maxIntrinsicWidth;
104 105
  }

106
  double _computeIntrinsicHeight(double width) {
107 108
    _layoutText(minWidth: width, maxWidth: width);
    return _textPainter.height;
109 110
  }

111
  @override
112 113
  double computeMinIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
114 115
  }

116
  @override
117 118
  double computeMaxIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
119 120
  }

121
  @override
122 123
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    assert(!needsLayout);
124 125 126
    assert(constraints != null);
    assert(constraints.debugAssertIsValid());
    _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
127
    return _textPainter.computeDistanceToActualBaseline(baseline);
128 129
  }

130
  @override
Adam Barth's avatar
Adam Barth committed
131 132
  bool hitTestSelf(Point position) => true;

133
  @override
134 135 136
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
    if (event is! PointerDownEvent)
      return;
137
    _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
138 139 140 141 142 143
    Offset offset = entry.localPosition.toOffset();
    TextPosition position = _textPainter.getPositionForOffset(offset);
    TextSpan span = _textPainter.text.getSpanForPosition(position);
    span?.recognizer?.addPointer(event);
  }

144 145 146 147
  bool _hasVisualOverflow = false;
  TextPainter _overflowPainter;
  ui.Shader _overflowShader;

148
  @override
149
  void performLayout() {
150
    _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
151 152 153 154 155
    // We grab _textPainter.size here because assigning to `size` will trigger
    // us to validate our intrinsic sizes, which will change _textPainter's
    // layout because the intrinsic size calculations are destructive.
    final Size textSize = _textPainter.size;
    size = constraints.constrain(textSize);
156

157
    final bool didOverflowWidth = size.width < textSize.width;
158 159 160 161 162
    // TODO(abarth): We're only measuring the sizes of the line boxes here. If
    // the glyphs draw outside the line boxes, we might think that there isn't
    // visual overflow when there actually is visual overflow. This can become
    // a problem if we start having horizontal overflow and introduce a clip
    // that affects the actual (but undetected) vertical overflow.
163
    _hasVisualOverflow = didOverflowWidth || size.height < textSize.height;
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
    if (didOverflowWidth) {
      switch (_overflow) {
        case TextOverflow.clip:
          _overflowPainter = null;
          _overflowShader = null;
          break;
        case TextOverflow.fade:
        case TextOverflow.ellipsis:
          _overflowPainter ??= new TextPainter(
            text: new TextSpan(style: _textPainter.text.style, text: '\u2026')
          )..layout();
          final double overflowUnit = _overflowPainter.width;
          double fadeEnd = size.width;
          if (_overflow == TextOverflow.ellipsis)
            fadeEnd -= overflowUnit / 2.0;
          final double fadeStart = fadeEnd - _overflowPainter.width;
          // TODO(abarth): This shader has an LTR bias.
          _overflowShader = new ui.Gradient.linear(
            <Point>[new Point(fadeStart, 0.0), new Point(fadeEnd, 0.0)],
            <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)]
          );
          break;
      }
    } else {
      _overflowPainter = null;
      _overflowShader = null;
    }
191 192
  }

193
  @override
194
  void paint(PaintingContext context, Offset offset) {
195 196
    // Ideally we could compute the min/max intrinsic width/height with a
    // non-destructive operation. However, currently, computing these values
197
    // will destroy state inside the painter. If that happens, we need to
198 199 200 201
    // get back the correct state by calling _layout again.
    //
    // TODO(abarth): Make computing the min/max intrinsic width/height
    // a non-destructive operation.
202 203 204
    //
    // If you remove this call, make sure that changing the textAlign still
    // works properly.
205
    _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
    final Canvas canvas = context.canvas;
    if (_hasVisualOverflow) {
      final Rect bounds = offset & size;
      if (_overflowPainter != null)
        canvas.saveLayer(bounds, new Paint());
      else
        canvas.save();
      canvas.clipRect(bounds);
    }
    _textPainter.paint(canvas, offset);
    if (_hasVisualOverflow) {
      if (_overflowShader != null) {
        canvas.translate(offset.dx, offset.dy);
        Paint paint = new Paint()
          ..transferMode = TransferMode.modulate
          ..shader = _overflowShader;
        canvas.drawRect(Point.origin & size, paint);
        if (_overflow == TextOverflow.ellipsis) {
          // TODO(abarth): This paint offset has an LTR bias.
          Offset ellipseOffset = new Offset(size.width - _overflowPainter.width, 0.0);
          _overflowPainter.paint(canvas, ellipseOffset);
        }
      }
      canvas.restore();
    }
231 232
  }

233
  @override
234 235 236 237
  SemanticAnnotator get semanticAnnotator => _annotate;

  void _annotate(SemanticsNode node) {
    node.label = text.toPlainText();
Hixie's avatar
Hixie committed
238
  }
239

240
  @override
241 242
  String debugDescribeChildren(String prefix) {
    return '$prefix \u2558\u2550\u2566\u2550\u2550 text \u2550\u2550\u2550\n'
Hixie's avatar
Hixie committed
243
           '${text.toString("$prefix   \u2551 ")}' // TextSpan includes a newline
244 245
           '$prefix   \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n'
           '$prefix\n';
246 247
  }
}