paragraph_test.dart 12.2 KB
Newer Older
1 2 3 4
// Copyright 2016 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:io';
6 7 8
import 'dart:ui' as ui show TextBox;

import 'package:flutter/rendering.dart';
9
import 'package:flutter/services.dart';
10
import 'package:flutter_test/flutter_test.dart';
11 12 13 14 15 16 17

import 'rendering_tester.dart';

const String _kText = 'I polished up that handle so carefullee\nThat now I am the Ruler of the Queen\'s Navee!';

void main() {
  test('getOffsetForCaret control test', () {
18
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
19 20 21
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
22 23
    layout(paragraph);

24
    final Rect caret = Rect.fromLTWH(0.0, 0.0, 2.0, 20.0);
25

26
    final Offset offset5 = paragraph.getOffsetForCaret(const TextPosition(offset: 5), caret);
27 28
    expect(offset5.dx, greaterThan(0.0));

29
    final Offset offset25 = paragraph.getOffsetForCaret(const TextPosition(offset: 25), caret);
30 31
    expect(offset25.dx, greaterThan(offset5.dx));

32
    final Offset offset50 = paragraph.getOffsetForCaret(const TextPosition(offset: 50), caret);
33 34 35 36
    expect(offset50.dy, greaterThan(offset5.dy));
  });

  test('getPositionForOffset control test', () {
37
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
38 39 40
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
41 42
    layout(paragraph);

43
    final TextPosition position20 = paragraph.getPositionForOffset(const Offset(20.0, 5.0));
44 45
    expect(position20.offset, greaterThan(0.0));

46
    final TextPosition position40 = paragraph.getPositionForOffset(const Offset(40.0, 5.0));
47 48
    expect(position40.offset, greaterThan(position20.offset));

49
    final TextPosition positionBelow = paragraph.getPositionForOffset(const Offset(5.0, 20.0));
50 51 52 53
    expect(positionBelow.offset, greaterThan(position40.offset));
  });

  test('getBoxesForSelection control test', () {
54
    final RenderParagraph paragraph = RenderParagraph(
55
      const TextSpan(text: _kText, style: TextStyle(fontSize: 10.0)),
Ian Hickson's avatar
Ian Hickson committed
56 57
      textDirection: TextDirection.ltr,
    );
58 59 60
    layout(paragraph);

    List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
61
        const TextSelection(baseOffset: 5, extentOffset: 25)
62 63 64 65 66
    );

    expect(boxes.length, equals(1));

    boxes = paragraph.getBoxesForSelection(
67
        const TextSelection(baseOffset: 25, extentOffset: 50)
68 69
    );

70 71
    expect(boxes.any((ui.TextBox box) => box.left == 250 && box.top == 0), isTrue);
    expect(boxes.any((ui.TextBox box) => box.right == 100 && box.top == 10), isTrue);
72
  },
73 74
  // Ahem-based tests don't yet quite work on Windows or some MacOS environments
  skip: Platform.isWindows || Platform.isMacOS);
75 76

  test('getWordBoundary control test', () {
77
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
78 79 80
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
81 82
    layout(paragraph);

83
    final TextRange range5 = paragraph.getWordBoundary(const TextPosition(offset: 5));
84 85
    expect(range5.textInside(_kText), equals('polished'));

86
    final TextRange range50 = paragraph.getWordBoundary(const TextPosition(offset: 50));
87 88
    expect(range50.textInside(_kText), equals(' '));

89
    final TextRange range85 = paragraph.getWordBoundary(const TextPosition(offset: 75));
90 91
    expect(range85.textInside(_kText), equals('Queen\'s'));
  });
92 93

  test('overflow test', () {
94
    final RenderParagraph paragraph = RenderParagraph(
95 96 97
      const TextSpan(
        text: 'This\n' // 4 characters * 10px font size = 40px width on the first line
              'is a wrapping test. It should wrap at manual newlines, and if softWrap is true, also at spaces.',
98
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
99
      ),
Ian Hickson's avatar
Ian Hickson committed
100
      textDirection: TextDirection.ltr,
101 102 103 104
      maxLines: 1,
      softWrap: true,
    );

105
    void relayoutWith({ int maxLines, bool softWrap, TextOverflow overflow }) {
106 107 108 109 110 111 112 113
      paragraph
        ..maxLines = maxLines
        ..softWrap = softWrap
        ..overflow = overflow;
      pumpFrame();
    }

    // Lay out in a narrow box to force wrapping.
114
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0)); // enough to fit "This" but not "This is"
115
    final double lineHeight = paragraph.size.height;
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151

    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.clip);
    expect(paragraph.size.height, equals(3 * lineHeight));

    relayoutWith(maxLines: null, softWrap: true, overflow: TextOverflow.clip);
    expect(paragraph.size.height, greaterThan(5 * lineHeight));

    // Try again with ellipsis overflow. We can't test that the ellipsis are
    // drawn, but we can test the sizing.
    relayoutWith(maxLines: 1, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(lineHeight));

    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(3 * lineHeight));

    // This is the one weird case. If maxLines is null, we would expect to allow
    // infinite wrapping. However, if we did, we'd never know when to append an
    // ellipsis, so this really means "append ellipsis as soon as we exceed the
    // width".
    relayoutWith(maxLines: null, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(2 * lineHeight));

    // Now with no soft wrapping.
    relayoutWith(maxLines: 1, softWrap: false, overflow: TextOverflow.clip);
    expect(paragraph.size.height, equals(lineHeight));

    relayoutWith(maxLines: 3, softWrap: false, overflow: TextOverflow.clip);
    expect(paragraph.size.height, equals(2 * lineHeight));

    relayoutWith(maxLines: null, softWrap: false, overflow: TextOverflow.clip);
    expect(paragraph.size.height, equals(2 * lineHeight));

    relayoutWith(maxLines: 1, softWrap: false, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(lineHeight));

    relayoutWith(maxLines: 3, softWrap: false, overflow: TextOverflow.ellipsis);
152
    expect(paragraph.size.height, equals(3 * lineHeight));
153 154 155

    relayoutWith(maxLines: null, softWrap: false, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(2 * lineHeight));
156 157 158 159 160

    // Test presence of the fade effect.
    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.fade);
    expect(paragraph.debugHasOverflowShader, isTrue);

161 162 163 164
    // Change back to ellipsis and check that the fade shader is cleared.
    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.debugHasOverflowShader, isFalse);

165 166
    relayoutWith(maxLines: 100, softWrap: true, overflow: TextOverflow.fade);
    expect(paragraph.debugHasOverflowShader, isFalse);
167
  });
168 169

  test('maxLines', () {
170
    final RenderParagraph paragraph = RenderParagraph(
171 172 173 174
      const TextSpan(
        text: 'How do you write like you\'re running out of time? Write day and night like you\'re running out of time?',
            // 0123456789 0123456789 012 345 0123456 012345 01234 012345678 012345678 0123 012 345 0123456 012345 01234
            // 0          1          2       3       4      5     6         7         8    9       10      11     12
175
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
176
      ),
Ian Hickson's avatar
Ian Hickson committed
177
      textDirection: TextDirection.ltr,
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));
    void layoutAt(int maxLines) {
      paragraph.maxLines = maxLines;
      pumpFrame();
    }

    layoutAt(null);
    expect(paragraph.size.height, 130.0);

    layoutAt(1);
    expect(paragraph.size.height, 10.0);

    layoutAt(2);
    expect(paragraph.size.height, 20.0);

    layoutAt(3);
    expect(paragraph.size.height, 30.0);
196
  }, skip: Platform.isWindows); // Ahem-based tests don't yet quite work on Windows
197

198
  test('changing color does not do layout', () {
199
    final RenderParagraph paragraph = RenderParagraph(
200 201
      const TextSpan(
        text: 'Hello',
202
        style: TextStyle(color: Color(0xFF000000)),
203
      ),
Ian Hickson's avatar
Ian Hickson committed
204
      textDirection: TextDirection.ltr,
205 206 207 208 209 210
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0), phase: EnginePhase.paint);
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isFalse);
    paragraph.text = const TextSpan(
      text: 'Hello World',
211
      style: TextStyle(color: Color(0xFF000000)),
212 213 214 215 216 217 218 219
    );
    expect(paragraph.debugNeedsLayout, isTrue);
    expect(paragraph.debugNeedsPaint, isFalse);
    pumpFrame(phase: EnginePhase.paint);
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isFalse);
    paragraph.text = const TextSpan(
      text: 'Hello World',
220
      style: TextStyle(color: Color(0xFFFFFFFF)),
221 222 223 224 225 226 227 228
    );
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isTrue);
    pumpFrame(phase: EnginePhase.paint);
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isFalse);
  });

229
  test('nested TextSpans in paragraph handle textScaleFactor correctly.', () {
230
    const TextSpan testSpan = TextSpan(
231
      text: 'a',
232
      style: TextStyle(
233 234
        fontSize: 10.0,
      ),
235 236
      children: <TextSpan>[
        TextSpan(
237
          text: 'b',
238 239
          children: <TextSpan>[
            TextSpan(text: 'c'),
240
          ],
241
          style: TextStyle(
242 243 244
            fontSize: 20.0,
          ),
        ),
245
        TextSpan(
246 247 248 249
          text: 'd',
        ),
      ],
    );
250
    final RenderParagraph paragraph = RenderParagraph(
251 252
        testSpan,
        textDirection: TextDirection.ltr,
253
        textScaleFactor: 1.3,
254 255 256 257
    );
    paragraph.layout(const BoxConstraints());
    // anyOf is needed here because Linux and Mac have different text
    // rendering widths in tests.
258
    // TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
259 260 261 262 263 264 265 266
    expect(paragraph.size.width, anyOf(79.0, 78.0));
    expect(paragraph.size.height, 26.0);

    // Test the sizes of nested spans.
    final List<ui.TextBox> boxes = <ui.TextBox>[];
    final String text = testSpan.toStringDeep();
    for (int i = 0; i < text.length; ++i) {
      boxes.addAll(paragraph.getBoxesForSelection(
267
          TextSelection(baseOffset: i, extentOffset: i + 1)
268 269 270 271 272 273
      ));
    }
    expect(boxes.length, equals(4));

    // anyOf is needed here and below because Linux and Mac have different text
    // rendering widths in tests.
274
    // TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
275
    expect(boxes[0].toRect().width, anyOf(14.0, 13.0));
276
    expect(boxes[0].toRect().height, closeTo(13.0, 0.0001));
277
    expect(boxes[1].toRect().width, anyOf(27.0, 26.0));
278
    expect(boxes[1].toRect().height, closeTo(26.0, 0.0001));
279
    expect(boxes[2].toRect().width, anyOf(27.0, 26.0));
280
    expect(boxes[2].toRect().height, closeTo(26.0, 0.0001));
281
    expect(boxes[3].toRect().width, anyOf(14.0, 13.0));
282
    expect(boxes[3].toRect().height, closeTo(13.0, 0.0001));
283 284
  });

285
  test('toStringDeep', () {
286
    final RenderParagraph paragraph = RenderParagraph(
287
      const TextSpan(text: _kText),
Ian Hickson's avatar
Ian Hickson committed
288
      textDirection: TextDirection.ltr,
289
      locale: const Locale('ja', 'JP'),
290
    );
291
    expect(paragraph, hasAGoodToStringDeep);
292
    expect(
293
      paragraph.toStringDeep(minLevel: DiagnosticLevel.info),
294 295
      equalsIgnoringHashCodes(
        'RenderParagraph#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED\n'
Ian Hickson's avatar
Ian Hickson committed
296 297
        ' │ parentData: MISSING\n'
        ' │ constraints: MISSING\n'
298
        ' │ size: MISSING\n'
Ian Hickson's avatar
Ian Hickson committed
299 300 301 302
        ' │ textAlign: start\n'
        ' │ textDirection: ltr\n'
        ' │ softWrap: wrapping at box width\n'
        ' │ overflow: clip\n'
303
        ' │ locale: ja_JP\n'
Ian Hickson's avatar
Ian Hickson committed
304
        ' │ maxLines: unlimited\n'
305 306 307
        ' ╘═╦══ text ═══\n'
        '   ║ TextSpan:\n'
        '   ║   "I polished up that handle so carefullee\n'
308
        '   ║   That now I am the Ruler of the Queen\'s Navee!"\n'
309 310 311 312
        '   ╚═══════════\n'
      ),
    );
  });
313 314 315 316

  test('locale setter', () {
    // Regression test for https://github.com/flutter/flutter/issues/18175

317
    final RenderParagraph paragraph = RenderParagraph(
318 319 320 321 322 323 324 325 326 327
      const TextSpan(text: _kText),
      locale: const Locale('zh', 'HK'),
      textDirection: TextDirection.ltr,
    );
    expect(paragraph.locale, const Locale('zh', 'HK'));

    paragraph.locale = const Locale('ja', 'JP');
    expect(paragraph.locale, const Locale('ja', 'JP'));
  });

328
}