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

24
    final Rect caret = new 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', () {
Ian Hickson's avatar
Ian Hickson committed
37 38 39 40
    final RenderParagraph paragraph = new RenderParagraph(
      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', () {
Ian Hickson's avatar
Ian Hickson committed
54
    final RenderParagraph paragraph = new 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', () {
Ian Hickson's avatar
Ian Hickson committed
77 78 79 80
    final RenderParagraph paragraph = new RenderParagraph(
      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 = new 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 170 171 172 173 174

  test('maxLines', () {
    final RenderParagraph paragraph = new RenderParagraph(
      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 199 200 201
  test('changing color does not do layout', () {
    final RenderParagraph paragraph = new RenderParagraph(
      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 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
          text: 'd',
        ),
      ],
    );
    final RenderParagraph paragraph = new RenderParagraph(
        testSpan,
        textDirection: TextDirection.ltr,
        textScaleFactor: 1.3
    );
    paragraph.layout(const BoxConstraints());
    // anyOf is needed here because Linux and Mac have different text
    // rendering widths in tests.
    // TODO(#12357): Figure out why this is, and fix it (if needed) once Blink
    // text rendering is replaced.
    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(
          new TextSelection(baseOffset: i, extentOffset: i + 1)
      ));
    }
    expect(boxes.length, equals(4));

    // anyOf is needed here and below because Linux and Mac have different text
    // rendering widths in tests.
    // TODO(#12357): Figure out why this is, and fix it (if needed) once Blink
    // text rendering is replaced.
    expect(boxes[0].toRect().width, anyOf(14.0, 13.0));
278
    expect(boxes[0].toRect().height, closeTo(13.0, 0.0001));
279
    expect(boxes[1].toRect().width, anyOf(27.0, 26.0));
280
    expect(boxes[1].toRect().height, closeTo(26.0, 0.0001));
281
    expect(boxes[2].toRect().width, anyOf(27.0, 26.0));
282
    expect(boxes[2].toRect().height, closeTo(26.0, 0.0001));
283
    expect(boxes[3].toRect().width, anyOf(14.0, 13.0));
284
    expect(boxes[3].toRect().height, closeTo(13.0, 0.0001));
285 286
  });

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

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

    final RenderParagraph paragraph = new RenderParagraph(
      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'));
  });

330
}