text_painter.dart 9.35 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
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphStyle, TextBox;
6

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

9
import 'basic_types.dart';
10
import 'text_editing.dart';
11
import 'text_style.dart';
12

Adam Barth's avatar
Adam Barth committed
13 14 15 16 17 18 19 20 21 22 23 24 25
// 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;
}

Florian Loitsch's avatar
Florian Loitsch committed
26
/// An immutable span of text.
Adam Barth's avatar
Adam Barth committed
27 28 29 30
class TextSpan {
  const TextSpan({
    this.style,
    this.text,
31 32
    this.children,
    this.recognizer
Adam Barth's avatar
Adam Barth committed
33 34 35 36
  });

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

Florian Loitsch's avatar
Florian Loitsch committed
38
  /// The text contained in the span.
Adam Barth's avatar
Adam Barth committed
39 40 41
  ///
  /// If both text and children are non-null, the text will preceed the
  /// children.
42 43
  final String text;

Adam Barth's avatar
Adam Barth committed
44 45 46 47
  /// Additional spans to include as children.
  ///
  /// If both text and children are non-null, the text will preceed the
  /// children.
48 49
  final List<TextSpan> children;

50 51 52
  /// If non-null, will receive events that hit this text span.
  final GestureRecognizer recognizer;

53
  void build(ui.ParagraphBuilder builder) {
Adam Barth's avatar
Adam Barth committed
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
    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();
  }

69 70 71 72 73
  bool visitTextSpan(bool visitor(TextSpan span)) {
    if (text != null) {
      if (!visitor(this))
        return false;
    }
Adam Barth's avatar
Adam Barth committed
74
    if (children != null) {
75 76 77 78
      for (TextSpan child in children) {
        if (!child.visitTextSpan(visitor))
          return false;
      }
79
    }
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
    return true;
  }

  TextSpan getSpanForPosition(TextPosition position) {
    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;
  }

  String toPlainText() {
    StringBuffer buffer = new StringBuffer();
    visitTextSpan((TextSpan span) {
      buffer.write(span.text);
      return true;
    });
    return buffer.toString();
110 111
  }

Adam Barth's avatar
Adam Barth committed
112 113 114 115 116 117 118
  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"');
Ian Hickson's avatar
Ian Hickson committed
119 120 121
    if (children != null)
      for (TextSpan child in children)
        buffer.writeln(child.toString(indent));
Adam Barth's avatar
Adam Barth committed
122 123
    return buffer.toString();
  }
124

Hixie's avatar
Hixie committed
125
  bool operator ==(dynamic other) {
126 127
    if (identical(this, other))
      return true;
Adam Barth's avatar
Adam Barth committed
128 129 130
    if (other is! TextSpan)
      return false;
    final TextSpan typedOther = other;
Adam Barth's avatar
Adam Barth committed
131 132
    return typedOther.text == text
        && typedOther.style == style
133
        && typedOther.recognizer == recognizer
Adam Barth's avatar
Adam Barth committed
134
        && _deepEquals(typedOther.children, children);
135
  }
136
  int get hashCode => hashValues(style, text, recognizer, hashList(children));
137 138
}

Florian Loitsch's avatar
Florian Loitsch committed
139
/// An object that paints a [TextSpan] into a canvas.
Adam Barth's avatar
Adam Barth committed
140
class TextPainter {
Hixie's avatar
Hixie committed
141
  TextPainter([ TextSpan text ]) {
142 143 144
    this.text = text;
  }

145
  ui.Paragraph _paragraph;
146 147 148
  bool _needsLayout = true;

  TextSpan _text;
Florian Loitsch's avatar
Florian Loitsch committed
149
  /// The (potentially styled) text to paint.
150 151 152 153 154
  TextSpan get text => _text;
  void set text(TextSpan value) {
    if (_text == value)
      return;
    _text = value;
155
    ui.ParagraphBuilder builder = new ui.ParagraphBuilder();
156
    _text.build(builder);
Adam Barth's avatar
Adam Barth committed
157
    _paragraph = builder.build(_text.style?.paragraphStyle ?? new ui.ParagraphStyle());
158 159 160
    _needsLayout = true;
  }

Florian Loitsch's avatar
Florian Loitsch committed
161
  /// The minimum width at which to layout the text.
162 163 164 165 166 167 168 169
  double get minWidth => _paragraph.minWidth;
  void set minWidth(value) {
    if (_paragraph.minWidth == value)
      return;
    _paragraph.minWidth = value;
    _needsLayout = true;
  }

Florian Loitsch's avatar
Florian Loitsch committed
170
  /// The maximum width at which to layout the text.
171 172 173 174 175 176 177 178
  double get maxWidth => _paragraph.maxWidth;
  void set maxWidth(value) {
    if (_paragraph.maxWidth == value)
      return;
    _paragraph.maxWidth = value;
    _needsLayout = true;
  }

Florian Loitsch's avatar
Florian Loitsch committed
179
  /// The minimum height at which to layout the text.
180 181 182 183 184 185 186 187
  double get minHeight => _paragraph.minHeight;
  void set minHeight(value) {
    if (_paragraph.minHeight == value)
      return;
    _paragraph.minHeight = value;
    _needsLayout = true;
  }

Florian Loitsch's avatar
Florian Loitsch committed
188
  /// The maximum height at which to layout the text.
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
  double get maxHeight => _paragraph.maxHeight;
  void set maxHeight(value) {
    if (_paragraph.maxHeight == value)
      return;
    _paragraph.maxHeight = value;
  }

  // Unfortunately, using full precision floating point here causes bad layouts
  // because floating point math isn't associative. If we add and subtract
  // padding, for example, we'll get different values when we estimate sizes and
  // when we actually compute layout because the operations will end up associated
  // differently. To work around this problem for now, we round fractional pixel
  // values up to the nearest whole pixel value. The right long-term fix is to do
  // layout using fixed precision arithmetic.
  double _applyFloatingPointHack(double layoutValue) {
    return layoutValue.ceilToDouble();
  }

Florian Loitsch's avatar
Florian Loitsch committed
207
  /// The width at which decreasing the width of the text would prevent it from painting itself completely within its bounds.
208
  double get minIntrinsicWidth {
209 210 211 212
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.minIntrinsicWidth);
  }

Florian Loitsch's avatar
Florian Loitsch committed
213
  /// The width at which increasing the width of the text no longer decreases the height.
214
  double get maxIntrinsicWidth {
215 216 217 218
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.maxIntrinsicWidth);
  }

219 220 221 222 223 224 225 226 227 228
  double get width {
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.width);
  }

  double get height {
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.height);
  }

229
  Size get size {
230
    assert(!_needsLayout);
231
    return new Size(width, height);
232 233
  }

Florian Loitsch's avatar
Florian Loitsch committed
234
  /// Returns the distance from the top of the text to the first baseline of the given type.
235 236 237 238 239 240 241 242 243 244
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    assert(!_needsLayout);
    switch (baseline) {
      case TextBaseline.alphabetic:
        return _paragraph.alphabeticBaseline;
      case TextBaseline.ideographic:
        return _paragraph.ideographicBaseline;
    }
  }

Florian Loitsch's avatar
Florian Loitsch committed
245
  /// Computes the visual position of the glyphs for painting the text.
246 247 248 249 250 251 252
  void layout() {
    if (!_needsLayout)
      return;
    _paragraph.layout();
    _needsLayout = false;
  }

Florian Loitsch's avatar
Florian Loitsch committed
253
  /// Paints the text onto the given canvas at the given offset.
254
  void paint(Canvas canvas, Offset offset) {
255 256 257
    assert(!_needsLayout && "Please call layout() before paint() to position the text before painting it." is String);
    _paragraph.paint(canvas, offset);
  }
258 259 260 261 262 263 264

  Offset _getOffsetFromUpstream(int offset, Rect caretPrototype) {
    List<ui.TextBox> boxes = _paragraph.getBoxesForRange(offset - 1, offset);
    if (boxes.isEmpty)
      return null;
    ui.TextBox box = boxes[0];
    double caretEnd = box.end;
265
    double dx = box.direction == TextDirection.rtl ? caretEnd : caretEnd - caretPrototype.width;
266 267 268 269 270 271 272 273 274
    return new Offset(dx, 0.0);
  }

  Offset _getOffsetFromDownstream(int offset, Rect caretPrototype) {
    List<ui.TextBox> boxes = _paragraph.getBoxesForRange(offset, offset + 1);
    if (boxes.isEmpty)
      return null;
    ui.TextBox box = boxes[0];
    double caretStart = box.start;
275
    double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
    return new Offset(dx, 0.0);
  }

  /// Returns the offset at which to paint the caret.
  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
    assert(!_needsLayout);
    int offset = position.offset;
    // TODO(abarth): Handle the directionality of the text painter itself.
    const Offset emptyOffset = Offset.zero;
    switch (position.affinity) {
      case TextAffinity.upstream:
        return _getOffsetFromUpstream(offset, caretPrototype)
            ?? _getOffsetFromDownstream(offset, caretPrototype)
            ?? emptyOffset;
      case TextAffinity.downstream:
        return _getOffsetFromDownstream(offset, caretPrototype)
            ?? _getOffsetFromUpstream(offset, caretPrototype)
            ?? emptyOffset;
    }
  }

  /// Returns a list of rects that bound the given selection.
  ///
  /// A given selection might have more than one rect if this text painter
  /// contains bidirectional text because logically contiguous text might not be
  /// visually contiguous.
  List<ui.TextBox> getBoxesForSelection(TextSelection selection) {
    assert(!_needsLayout);
    return _paragraph.getBoxesForRange(selection.start, selection.end);
  }

307 308 309 310 311
  TextPosition getPositionForOffset(Offset offset) {
    assert(!_needsLayout);
    return _paragraph.getPositionForOffset(offset);
  }

312
}