paragraph_test.dart 16.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// 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 TextBox;

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

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', () {
19
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
20 21 22
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
23 24
    layout(paragraph);

Dan Field's avatar
Dan Field committed
25
    const Rect caret = Rect.fromLTWH(0.0, 0.0, 2.0, 20.0);
26

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

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

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

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

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

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

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

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

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

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

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

71 72
    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);
73
  },
74
  // Ahem-based tests don't yet quite work on Windows or some MacOS environments
75
  skip: isLinux || isBrowser);
76 77

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

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

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

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

  test('overflow test', () {
95
    final RenderParagraph paragraph = RenderParagraph(
96 97 98
      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.',
99
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
100
      ),
Ian Hickson's avatar
Ian Hickson committed
101
      textDirection: TextDirection.ltr,
102 103 104 105
      maxLines: 1,
      softWrap: true,
    );

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

    // Lay out in a narrow box to force wrapping.
115
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0)); // enough to fit "This" but not "This is"
116
    final double lineHeight = paragraph.size.height;
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 152

    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);
153
    expect(paragraph.size.height, equals(3 * lineHeight));
154 155 156

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

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

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

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

  test('maxLines', () {
171
    final RenderParagraph paragraph = RenderParagraph(
172 173 174 175
      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
176
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
177
      ),
Ian Hickson's avatar
Ian Hickson committed
178
      textDirection: TextDirection.ltr,
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
    );
    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);
197
  }, skip: isWindows || isBrowser); // Ahem-based tests don't yet quite work on Windows
198

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

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

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

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

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

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

318
    final RenderParagraph paragraph = RenderParagraph(
319 320 321 322 323 324 325 326 327 328
      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'));
  });

329 330 331 332 333 334 335 336 337 338 339 340 341
  test('inline widgets test', () {
    const TextSpan text = TextSpan(
      text: 'a',
      style: TextStyle(fontSize: 10.0),
      children: <InlineSpan>[
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        TextSpan(text: 'a'),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
      ],
    );
    // Fake the render boxes that correspond to the WidgetSpans. We use
    // RenderParagraph to reduce dependencies this test has.
342 343 344 345 346
    final List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
    ];
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365

    final RenderParagraph paragraph = RenderParagraph(
      text,
      textDirection: TextDirection.ltr,
      children: renderBoxes,
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));

    final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
        const TextSelection(baseOffset: 0, extentOffset: 8)
    );

    expect(boxes.length, equals(5));
    expect(boxes[0], const TextBox.fromLTRBD(0.0, 4.0, 10.0, 14.0, TextDirection.ltr));
    expect(boxes[1], const TextBox.fromLTRBD(10.0, 0.0, 24.0, 14.0, TextDirection.ltr));
    expect(boxes[2], const TextBox.fromLTRBD(24.0, 0.0, 38.0, 14.0, TextDirection.ltr));
    expect(boxes[3], const TextBox.fromLTRBD(38.0, 4.0, 48.0, 14.0, TextDirection.ltr));
    expect(boxes[4], const TextBox.fromLTRBD(48.0, 0.0, 62.0, 14.0, TextDirection.ltr));
  // Ahem-based tests don't yet quite work on Windows or some MacOS environments
366
  }, skip: isWindows || isMacOS || isBrowser);
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384

  test('inline widgets multiline test', () {
    const TextSpan text = TextSpan(
      text: 'a',
      style: TextStyle(fontSize: 10.0),
      children: <InlineSpan>[
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        TextSpan(text: 'a'),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
      ],
    );
    // Fake the render boxes that correspond to the WidgetSpans. We use
    // RenderParagraph to reduce dependencies this test has.
385 386 387 388 389 390 391 392 393
    final List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
    ];
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418

    final RenderParagraph paragraph = RenderParagraph(
      text,
      textDirection: TextDirection.ltr,
      children: renderBoxes,
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0));

    final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
        const TextSelection(baseOffset: 0, extentOffset: 12)
    );

    expect(boxes.length, equals(9));
    expect(boxes[0], const TextBox.fromLTRBD(0.0, 4.0, 10.0, 14.0, TextDirection.ltr));
    expect(boxes[1], const TextBox.fromLTRBD(10.0, 0.0, 24.0, 14.0, TextDirection.ltr));
    expect(boxes[2], const TextBox.fromLTRBD(24.0, 0.0, 38.0, 14.0, TextDirection.ltr));
    expect(boxes[3], const TextBox.fromLTRBD(38.0, 4.0, 48.0, 14.0, TextDirection.ltr));
    // Wraps
    expect(boxes[4], const TextBox.fromLTRBD(0.0, 14.0, 14.0, 28.0 , TextDirection.ltr));
    expect(boxes[5], const TextBox.fromLTRBD(14.0, 14.0, 28.0, 28.0, TextDirection.ltr));
    expect(boxes[6], const TextBox.fromLTRBD(28.0, 14.0, 42.0, 28.0, TextDirection.ltr));
    // Wraps
    expect(boxes[7], const TextBox.fromLTRBD(0.0, 28.0, 14.0, 42.0, TextDirection.ltr));
    expect(boxes[8], const TextBox.fromLTRBD(14.0, 28.0, 28.0, 42.0 , TextDirection.ltr));
  // Ahem-based tests don't yet quite work on Windows or some MacOS environments
419
  }, skip: isWindows || isMacOS || isBrowser);
420
}