paragraph.dart 7.74 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
  void _layoutText(BoxConstraints constraints) {
91
    assert(constraints != null);
92
    assert(constraints.debugAssertIsValid());
93
    _textPainter.layout(minWidth: constraints.minWidth, maxWidth: _softWrap ? constraints.maxWidth : double.INFINITY);
94 95
  }

96
  @override
97
  double getMinIntrinsicWidth(BoxConstraints constraints) {
98 99
    _layoutText(constraints);
    return constraints.constrainWidth(_textPainter.minIntrinsicWidth);
100 101
  }

102
  @override
103
  double getMaxIntrinsicWidth(BoxConstraints constraints) {
104 105
    _layoutText(constraints);
    return constraints.constrainWidth(_textPainter.maxIntrinsicWidth);
106 107 108
  }

  double _getIntrinsicHeight(BoxConstraints constraints) {
109
    _layoutText(constraints);
110
    return constraints.constrainHeight(_textPainter.height);
111 112
  }

113
  @override
114
  double getMinIntrinsicHeight(BoxConstraints constraints) {
115
    assert(constraints.debugAssertIsValid());
116 117 118
    return _getIntrinsicHeight(constraints);
  }

119
  @override
120
  double getMaxIntrinsicHeight(BoxConstraints constraints) {
121
    assert(constraints.debugAssertIsValid());
122 123 124
    return _getIntrinsicHeight(constraints);
  }

125
  @override
126 127
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    assert(!needsLayout);
128 129
    _layoutText(constraints);
    return _textPainter.computeDistanceToActualBaseline(baseline);
130 131
  }

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

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

146 147 148 149
  bool _hasVisualOverflow = false;
  TextPainter _overflowPainter;
  ui.Shader _overflowShader;

150
  @override
151
  void performLayout() {
152 153
    _layoutText(constraints);
    size = constraints.constrain(_textPainter.size);
154 155 156 157 158 159 160 161 162 163 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

    final bool didOverflowWidth = size.width < _textPainter.width;
    // 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.
    _hasVisualOverflow = didOverflowWidth || size.height < _textPainter.height;
    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;
    }
189 190
  }

191
  @override
192
  void paint(PaintingContext context, Offset offset) {
193 194
    // Ideally we could compute the min/max intrinsic width/height with a
    // non-destructive operation. However, currently, computing these values
195
    // will destroy state inside the painter. If that happens, we need to
196 197 198 199
    // get back the correct state by calling _layout again.
    //
    // TODO(abarth): Make computing the min/max intrinsic width/height
    // a non-destructive operation.
200 201 202
    //
    // If you remove this call, make sure that changing the textAlign still
    // works properly.
203
    _layoutText(constraints);
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
    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();
    }
229 230
  }

231
  @override
Hixie's avatar
Hixie committed
232 233
  Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
    yield (SemanticsNode node) {
234
      node.label = text.toPlainText();
Hixie's avatar
Hixie committed
235 236
    };
  }
237

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