text_painter_rtl_test.dart 29.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Ian Hickson's avatar
Ian Hickson committed
2 3 4 5 6 7 8 9 10 11 12 13
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';

const bool skipTestsWithKnownBugs = true;
const bool skipExpectsWithKnownBugs = false;

void main() {
  test('TextPainter - basic words', () {
14
    final TextPainter painter = TextPainter()
Ian Hickson's avatar
Ian Hickson committed
15 16 17 18
      ..textDirection = TextDirection.ltr;

    painter.text = const TextSpan(
      text: 'ABC DEF\nGHI',
19
      style: TextStyle(fontSize: 10.0),
Ian Hickson's avatar
Ian Hickson committed
20 21 22 23
    );
    painter.layout();

    expect(
24
      painter.getWordBoundary(const TextPosition(offset: 1)),
Ian Hickson's avatar
Ian Hickson committed
25 26 27
      const TextRange(start: 0, end: 3),
    );
    expect(
28
      painter.getWordBoundary(const TextPosition(offset: 5)),
Ian Hickson's avatar
Ian Hickson committed
29 30 31
      const TextRange(start: 4, end: 7),
    );
    expect(
32
      painter.getWordBoundary(const TextPosition(offset: 9)),
Ian Hickson's avatar
Ian Hickson committed
33 34
      const TextRange(start: 8, end: 11),
    );
35
    painter.dispose();
Ian Hickson's avatar
Ian Hickson committed
36 37 38
  });

  test('TextPainter - bidi overrides in LTR', () {
39
    final TextPainter painter = TextPainter()
Ian Hickson's avatar
Ian Hickson committed
40 41 42 43 44
      ..textDirection = TextDirection.ltr;

    painter.text = const TextSpan(
      text: '${Unicode.RLO}HEBREW1 ${Unicode.LRO}english2${Unicode.PDF} HEBREW3${Unicode.PDF}',
           //      0       12345678      9      101234567       18     90123456       27
45
      style: TextStyle(fontSize: 10.0),
Ian Hickson's avatar
Ian Hickson committed
46
    );
47
    TextSpan textSpan = painter.text! as TextSpan;
48
    expect(textSpan.text!.length, 28);
Ian Hickson's avatar
Ian Hickson committed
49 50
    painter.layout();

51 52 53
    // The skips here are because the old rendering code considers the bidi formatting characters
    // to be part of the word sometimes and not others, which is fine, but we'd mildly prefer if
    // we were consistently considering them part of words always.
54
    final TextRange hebrew1 = painter.getWordBoundary(const TextPosition(offset: 4));
55
    expect(hebrew1, const TextRange(start: 0, end: 8), skip: skipExpectsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
56
    final TextRange english2 = painter.getWordBoundary(const TextPosition(offset: 14));
57
    expect(english2, const TextRange(start: 9, end: 19), skip: skipExpectsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
58
    final TextRange hebrew3 = painter.getWordBoundary(const TextPosition(offset: 24));
Ian Hickson's avatar
Ian Hickson committed
59 60 61 62 63 64 65 66 67 68 69 70 71 72
    expect(hebrew3, const TextRange(start: 20, end: 28));

    //                              >>>>>>>>>>>>>>>                       embedding level 2
    //              <==============================================       embedding level 1
    //             ------------------------------------------------>      embedding level 0
    //            0 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 2
    //            0 6 5 4 3 2 1 0 9 0 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 7  <- index of character in string
    // Paints as:   3 W E R B E H   e n g l i s h 2   1 W E R B E H
    //             0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2   <- pixel offset at boundary
    //             0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
    //             0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

    expect(
      painter.getOffsetForCaret(const TextPosition(offset: 0, affinity: TextAffinity.upstream), Rect.zero),
73
      Offset.zero,
Ian Hickson's avatar
Ian Hickson committed
74 75
    );
    expect(
76
      painter.getOffsetForCaret(const TextPosition(offset: 0), Rect.zero),
77
      Offset.zero,
Ian Hickson's avatar
Ian Hickson committed
78 79 80 81 82 83
    );
    expect(
      painter.getOffsetForCaret(const TextPosition(offset: 1, affinity: TextAffinity.upstream), Rect.zero),
      const Offset(240.0, 0.0),
    );
    expect(
84
      painter.getOffsetForCaret(const TextPosition(offset: 1), Rect.zero),
Ian Hickson's avatar
Ian Hickson committed
85 86 87 88 89 90 91
      const Offset(240.0, 0.0),
    );
    expect(
      painter.getOffsetForCaret(const TextPosition(offset: 7, affinity: TextAffinity.upstream), Rect.zero),
      const Offset(180.0, 0.0),
    );
    expect(
92
      painter.getOffsetForCaret(const TextPosition(offset: 7), Rect.zero),
Ian Hickson's avatar
Ian Hickson committed
93 94 95 96 97 98 99
      const Offset(180.0, 0.0),
    );
    expect(
      painter.getOffsetForCaret(const TextPosition(offset: 8, affinity: TextAffinity.upstream), Rect.zero),
      const Offset(170.0, 0.0),
    );
    expect(
100
      painter.getOffsetForCaret(const TextPosition(offset: 8), Rect.zero),
Ian Hickson's avatar
Ian Hickson committed
101 102 103 104 105 106 107
      const Offset(170.0, 0.0),
    );
    expect(
      painter.getOffsetForCaret(const TextPosition(offset: 9, affinity: TextAffinity.upstream), Rect.zero),
      const Offset(160.0, 0.0),
    );
    expect(
108
      painter.getOffsetForCaret(const TextPosition(offset: 9), Rect.zero),
Ian Hickson's avatar
Ian Hickson committed
109 110 111 112 113 114 115
      const Offset(160.0, 0.0),
    );
    expect(
      painter.getOffsetForCaret(const TextPosition(offset: 10, affinity: TextAffinity.upstream), Rect.zero),
      const Offset(80.0, 0.0),
    );
    expect(
116
      painter.getOffsetForCaret(const TextPosition(offset: 10), Rect.zero),
Ian Hickson's avatar
Ian Hickson committed
117 118 119 120 121 122
      const Offset(80.0, 0.0),
    );

    expect(
      painter.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: 27)),
      const <TextBox>[
123 124 125
        TextBox.fromLTRBD(160.0, 0.0, 240.0, 10.0, TextDirection.rtl), // HEBREW1
        TextBox.fromLTRBD( 80.0, 0.0, 160.0, 10.0, TextDirection.ltr), // english2
        TextBox.fromLTRBD(  0.0, 0.0,  80.0, 10.0, TextDirection.rtl), // HEBREW3
Ian Hickson's avatar
Ian Hickson committed
126 127 128 129 130
      ],
      // Horizontal offsets are currently one pixel off in places; vertical offsets are good.
      // The list is currently in the wrong order (so selection boxes will paint in the wrong order).
    );

131
    textSpan = painter.text! as TextSpan;
132
    final List<List<TextBox>> list = <List<TextBox>>[
133
      for (int index = 0; index < textSpan.text!.length; index += 1)
134 135
        painter.getBoxesForSelection(TextSelection(baseOffset: index, extentOffset: index + 1)),
    ];
Ian Hickson's avatar
Ian Hickson committed
136
    expect(list, const <List<TextBox>>[
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
      <TextBox>[], // U+202E, non-printing Unicode bidi formatting character
      <TextBox>[TextBox.fromLTRBD(230.0, 0.0, 240.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(220.0, 0.0, 230.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(210.0, 0.0, 220.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(200.0, 0.0, 210.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(190.0, 0.0, 200.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(180.0, 0.0, 190.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(170.0, 0.0, 180.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(160.0, 0.0, 170.0, 10.0, TextDirection.rtl)],
      <TextBox>[], // U+202D, non-printing Unicode bidi formatting character
      <TextBox>[TextBox.fromLTRBD(80.0, 0.0, 90.0, 10.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(90.0, 0.0, 100.0, 10.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(100.0, 0.0, 110.0, 10.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(110.0, 0.0, 120.0, 10.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(120.0, 0.0, 130.0, 10.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(130.0, 0.0, 140.0, 10.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(140.0, 0.0, 150.0, 10.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(150.0, 0.0, 160.0, 10.0, TextDirection.ltr)],
      <TextBox>[], // U+202C, non-printing Unicode bidi formatting character
      <TextBox>[TextBox.fromLTRBD(70.0, 0.0, 80.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(60.0, 0.0, 70.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(50.0, 0.0, 60.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(40.0, 0.0, 50.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(30.0, 0.0, 40.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(20.0, 0.0, 30.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(10.0, 0.0, 20.0, 10.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(0.0, 0.0, 10.0, 10.0, TextDirection.rtl)],
      <TextBox>[], // U+202C, non-printing Unicode bidi formatting character
Ian Hickson's avatar
Ian Hickson committed
165 166
      // The list currently has one extra bogus entry (the last entry, for the
      // trailing U+202C PDF, should be empty but is one-pixel-wide instead).
167
    ], skip: skipExpectsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
168
    painter.dispose();
169
  }, skip: skipTestsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
170 171

  test('TextPainter - bidi overrides in RTL', () {
172
    final TextPainter painter = TextPainter()
Ian Hickson's avatar
Ian Hickson committed
173 174 175 176 177
      ..textDirection = TextDirection.rtl;

    painter.text = const TextSpan(
      text: '${Unicode.RLO}HEBREW1 ${Unicode.LRO}english2${Unicode.PDF} HEBREW3${Unicode.PDF}',
           //      0       12345678      9      101234567       18     90123456       27
178
      style: TextStyle(fontSize: 10.0),
Ian Hickson's avatar
Ian Hickson committed
179
    );
180
    final TextSpan textSpan = painter.text! as TextSpan;
181
    expect(textSpan.text!.length, 28);
Ian Hickson's avatar
Ian Hickson committed
182 183
    painter.layout();

184
    final TextRange hebrew1 = painter.getWordBoundary(const TextPosition(offset: 4));
185
    expect(hebrew1, const TextRange(start: 0, end: 8), skip: skipExpectsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
186
    final TextRange english2 = painter.getWordBoundary(const TextPosition(offset: 14));
187
    expect(english2, const TextRange(start: 9, end: 19), skip: skipExpectsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
188
    final TextRange hebrew3 = painter.getWordBoundary(const TextPosition(offset: 24));
Ian Hickson's avatar
Ian Hickson committed
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
    expect(hebrew3, const TextRange(start: 20, end: 28));

    //                              >>>>>>>>>>>>>>>                       embedding level 2
    //            <==================================================     embedding level 1
    //            2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0
    //            7 6 5 4 3 2 1 0 9 0 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 0  <- index of character in string
    // Paints as:   3 W E R B E H   e n g l i s h 2   1 W E R B E H
    //             0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2   <- pixel offset at boundary
    //             0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
    //             0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

    expect(
      painter.getOffsetForCaret(const TextPosition(offset: 0, affinity: TextAffinity.upstream), Rect.zero),
      const Offset(240.0, 0.0),
    );
    expect(
205
      painter.getOffsetForCaret(const TextPosition(offset: 0), Rect.zero),
Ian Hickson's avatar
Ian Hickson committed
206 207 208 209 210 211 212
      const Offset(240.0, 0.0),
    );
    expect(
      painter.getOffsetForCaret(const TextPosition(offset: 1, affinity: TextAffinity.upstream), Rect.zero),
      const Offset(240.0, 0.0),
    );
    expect(
213
      painter.getOffsetForCaret(const TextPosition(offset: 1), Rect.zero),
Ian Hickson's avatar
Ian Hickson committed
214 215 216 217 218 219 220
      const Offset(240.0, 0.0),
    );
    expect(
      painter.getOffsetForCaret(const TextPosition(offset: 7, affinity: TextAffinity.upstream), Rect.zero),
      const Offset(180.0, 0.0),
    );
    expect(
221
      painter.getOffsetForCaret(const TextPosition(offset: 7), Rect.zero),
Ian Hickson's avatar
Ian Hickson committed
222 223 224 225 226 227 228
      const Offset(180.0, 0.0),
    );
    expect(
      painter.getOffsetForCaret(const TextPosition(offset: 8, affinity: TextAffinity.upstream), Rect.zero),
      const Offset(170.0, 0.0),
    );
    expect(
229
      painter.getOffsetForCaret(const TextPosition(offset: 8), Rect.zero),
Ian Hickson's avatar
Ian Hickson committed
230 231 232 233 234 235 236
      const Offset(170.0, 0.0),
    );
    expect(
      painter.getOffsetForCaret(const TextPosition(offset: 9, affinity: TextAffinity.upstream), Rect.zero),
      const Offset(160.0, 0.0),
    );
    expect(
237
      painter.getOffsetForCaret(const TextPosition(offset: 9), Rect.zero),
Ian Hickson's avatar
Ian Hickson committed
238 239 240 241 242 243 244
      const Offset(160.0, 0.0),
    );
    expect(
      painter.getOffsetForCaret(const TextPosition(offset: 10, affinity: TextAffinity.upstream), Rect.zero),
      const Offset(80.0, 0.0),
    );
    expect(
245
      painter.getOffsetForCaret(const TextPosition(offset: 10), Rect.zero),
Ian Hickson's avatar
Ian Hickson committed
246 247 248 249 250 251
      const Offset(80.0, 0.0),
    );

    expect(
      painter.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: 27)),
      const <TextBox>[
252 253 254
        TextBox.fromLTRBD(160.0, 0.0, 240.0, 10.0, TextDirection.rtl), // HEBREW1
        TextBox.fromLTRBD( 80.0, 0.0, 160.0, 10.0, TextDirection.ltr), // english2
        TextBox.fromLTRBD(  0.0, 0.0,  80.0, 10.0, TextDirection.rtl), // HEBREW3
Ian Hickson's avatar
Ian Hickson committed
255 256 257
      ],
      // Horizontal offsets are currently one pixel off in places; vertical offsets are good.
      // The list is currently in the wrong order (so selection boxes will paint in the wrong order).
258
      skip: skipExpectsWithKnownBugs, // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
259
    );
260
    painter.dispose();
261
  }, skip: skipTestsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
262 263

  test('TextPainter - forced line-wrapping with bidi', () {
264
    final TextPainter painter = TextPainter()
Ian Hickson's avatar
Ian Hickson committed
265 266 267 268
      ..textDirection = TextDirection.ltr;

    painter.text = const TextSpan(
      text: 'A\u05D0', // A, Alef
269
      style: TextStyle(fontSize: 10.0),
Ian Hickson's avatar
Ian Hickson committed
270
    );
271
    final TextSpan textSpan = painter.text! as TextSpan;
272
    expect(textSpan.text!.length, 2);
Ian Hickson's avatar
Ian Hickson committed
273 274 275 276
    painter.layout(maxWidth: 10.0);

    for (int index = 0; index <= 2; index += 1) {
      expect(
277
        painter.getWordBoundary(const TextPosition(offset: 0)),
Ian Hickson's avatar
Ian Hickson committed
278 279 280 281 282 283
        const TextRange(start: 0, end: 2),
      );
    }

    expect( // before the A
      painter.getOffsetForCaret(const TextPosition(offset: 0, affinity: TextAffinity.upstream), Rect.zero),
284
      Offset.zero,
Ian Hickson's avatar
Ian Hickson committed
285 286
    );
    expect( // before the A
287
      painter.getOffsetForCaret(const TextPosition(offset: 0), Rect.zero),
288
      Offset.zero,
Ian Hickson's avatar
Ian Hickson committed
289 290 291 292 293 294 295
    );

    expect( // between A and Alef, after the A
      painter.getOffsetForCaret(const TextPosition(offset: 1, affinity: TextAffinity.upstream), Rect.zero),
      const Offset(10.0, 0.0),
    );
    expect( // between A and Alef, before the Alef
296
      painter.getOffsetForCaret(const TextPosition(offset: 1), Rect.zero),
Ian Hickson's avatar
Ian Hickson committed
297 298 299 300 301 302 303 304
      const Offset(10.0, 10.0),
    );

    expect( // after the Alef
      painter.getOffsetForCaret(const TextPosition(offset: 2, affinity: TextAffinity.upstream), Rect.zero),
      const Offset(0.0, 10.0),
    );
    expect( // after the Alef
305
      painter.getOffsetForCaret(const TextPosition(offset: 2), Rect.zero),
Ian Hickson's avatar
Ian Hickson committed
306 307 308 309 310 311
      const Offset(0.0, 10.0),
    );

    expect(
      painter.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: 2)),
      const <TextBox>[
312 313
        TextBox.fromLTRBD(0.0,  0.0, 10.0, 10.0, TextDirection.ltr), // A
        TextBox.fromLTRBD(0.0, 10.0, 10.0, 20.0, TextDirection.rtl), // Alef
Ian Hickson's avatar
Ian Hickson committed
314 315 316 317 318
      ],
    );
    expect(
      painter.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: 1)),
      const <TextBox>[
319
        TextBox.fromLTRBD(0.0,  0.0, 10.0, 10.0, TextDirection.ltr), // A
Ian Hickson's avatar
Ian Hickson committed
320 321 322 323 324
      ],
    );
    expect(
      painter.getBoxesForSelection(const TextSelection(baseOffset: 1, extentOffset: 2)),
      const <TextBox>[
325
        TextBox.fromLTRBD(0.0, 10.0, 10.0, 20.0, TextDirection.rtl), // Alef
Ian Hickson's avatar
Ian Hickson committed
326 327
      ],
    );
328
    painter.dispose();
329
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/32238
Ian Hickson's avatar
Ian Hickson committed
330 331

  test('TextPainter - line wrap mid-word', () {
332
    final TextPainter painter = TextPainter()
Ian Hickson's avatar
Ian Hickson committed
333 334 335
      ..textDirection = TextDirection.ltr;

    painter.text = const TextSpan(
336
      style: TextStyle(fontSize: 10.0),
337 338
      children: <TextSpan>[
        TextSpan(
Ian Hickson's avatar
Ian Hickson committed
339 340
          text: 'hello', // width 50
        ),
341
        TextSpan(
Ian Hickson's avatar
Ian Hickson committed
342
          text: 'lovely', // width 120
343
          style: TextStyle(fontSize: 20.0),
Ian Hickson's avatar
Ian Hickson committed
344
        ),
345
        TextSpan(
Ian Hickson's avatar
Ian Hickson committed
346 347 348 349 350 351 352 353 354
          text: 'world', // width 50
        ),
      ],
    );
    painter.layout(maxWidth: 110.0); // half-way through "lovely"

    expect(
      painter.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: 16)),
      const <TextBox>[
355 356 357 358
        TextBox.fromLTRBD( 0.0,  8.0,  50.0, 18.0, TextDirection.ltr),
        TextBox.fromLTRBD(50.0,  0.0, 110.0, 20.0, TextDirection.ltr),
        TextBox.fromLTRBD( 0.0, 20.0,  60.0, 40.0, TextDirection.ltr),
        TextBox.fromLTRBD(60.0, 28.0, 110.0, 38.0, TextDirection.ltr),
Ian Hickson's avatar
Ian Hickson committed
359
      ],
360 361
      // horizontal offsets are one pixel off in places; vertical offsets are good
      skip: skipExpectsWithKnownBugs, // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
362
    );
363
    painter.dispose();
364
  }, skip: skipTestsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
365 366

  test('TextPainter - line wrap mid-word, bidi - LTR base', () {
367
    final TextPainter painter = TextPainter()
Ian Hickson's avatar
Ian Hickson committed
368 369 370
      ..textDirection = TextDirection.ltr;

    painter.text = const TextSpan(
371
      style: TextStyle(fontSize: 10.0),
372 373
      children: <TextSpan>[
        TextSpan(
Ian Hickson's avatar
Ian Hickson committed
374 375
          text: 'hello', // width 50
        ),
376
        TextSpan(
Ian Hickson's avatar
Ian Hickson committed
377
          text: '\u062C\u0645\u064A\u0644', // width 80
378
          style: TextStyle(fontSize: 20.0),
Ian Hickson's avatar
Ian Hickson committed
379
        ),
380
        TextSpan(
Ian Hickson's avatar
Ian Hickson committed
381 382 383 384 385 386 387 388 389
          text: 'world', // width 50
        ),
      ],
    );
    painter.layout(maxWidth: 90.0); // half-way through the Arabic word

    expect(
      painter.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: 16)),
      const <TextBox>[
390 391 392 393
        TextBox.fromLTRBD( 0.0,  8.0, 50.0, 18.0, TextDirection.ltr),
        TextBox.fromLTRBD(50.0,  0.0, 90.0, 20.0, TextDirection.rtl),
        TextBox.fromLTRBD( 0.0, 20.0, 40.0, 40.0, TextDirection.rtl),
        TextBox.fromLTRBD(40.0, 28.0, 90.0, 38.0, TextDirection.ltr),
Ian Hickson's avatar
Ian Hickson committed
394
      ],
395 396
      // horizontal offsets are one pixel off in places; vertical offsets are good
      skip: skipExpectsWithKnownBugs, // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
397 398
    );

399 400 401 402 403
    final List<List<TextBox>> list = <List<TextBox>>[
      for (int index = 0; index < 5+4+5; index += 1)
        painter.getBoxesForSelection(TextSelection(baseOffset: index, extentOffset: index + 1)),
    ];

Ian Hickson's avatar
Ian Hickson committed
404
    expect(list, const <List<TextBox>>[
405 406 407 408 409 410 411 412 413 414 415 416 417
      <TextBox>[TextBox.fromLTRBD(0.0, 8.0, 10.0, 18.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(10.0, 8.0, 20.0, 18.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(20.0, 8.0, 30.0, 18.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(30.0, 8.0, 40.0, 18.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(40.0, 8.0, 50.0, 18.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(70.0, 0.0, 90.0, 20.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(50.0, 0.0, 70.0, 20.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(20.0, 20.0, 40.0, 40.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(0.0, 20.0, 20.0, 40.0, TextDirection.rtl)],
      <TextBox>[TextBox.fromLTRBD(40.0, 28.0, 50.0, 38.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(50.0, 28.0, 60.0, 38.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(60.0, 28.0, 70.0, 38.0, TextDirection.ltr)],
      <TextBox>[TextBox.fromLTRBD(70.0, 28.0, 80.0, 38.0, TextDirection.ltr)],
418
      <TextBox>[TextBox.fromLTRBD(80.0, 28.0, 90.0, 38.0, TextDirection.ltr)],
Ian Hickson's avatar
Ian Hickson committed
419
    ]);
420
    painter.dispose();
421
  }, skip: skipTestsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
422 423

  test('TextPainter - line wrap mid-word, bidi - RTL base', () {
424
    final TextPainter painter = TextPainter()
Ian Hickson's avatar
Ian Hickson committed
425 426 427
      ..textDirection = TextDirection.rtl;

    painter.text = const TextSpan(
428
      style: TextStyle(fontSize: 10.0),
429 430
      children: <TextSpan>[
        TextSpan(
Ian Hickson's avatar
Ian Hickson committed
431 432
          text: 'hello', // width 50
        ),
433
        TextSpan(
Ian Hickson's avatar
Ian Hickson committed
434
          text: '\u062C\u0645\u064A\u0644', // width 80
435
          style: TextStyle(fontSize: 20.0),
Ian Hickson's avatar
Ian Hickson committed
436
        ),
437
        TextSpan(
Ian Hickson's avatar
Ian Hickson committed
438 439 440 441 442 443 444 445 446
          text: 'world', // width 50
        ),
      ],
    );
    painter.layout(maxWidth: 90.0); // half-way through the Arabic word

    expect(
      painter.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: 16)),
      const <TextBox>[
447 448 449 450
        TextBox.fromLTRBD(40.0,  8.0, 90.0, 18.0, TextDirection.ltr),
        TextBox.fromLTRBD( 0.0,  0.0, 40.0, 20.0, TextDirection.rtl),
        TextBox.fromLTRBD(50.0, 20.0, 90.0, 40.0, TextDirection.rtl),
        TextBox.fromLTRBD( 0.0, 28.0, 50.0, 38.0, TextDirection.ltr),
Ian Hickson's avatar
Ian Hickson committed
451 452 453
      ],
      // Horizontal offsets are currently one pixel off in places; vertical offsets are good.
      // The list is currently in the wrong order (so selection boxes will paint in the wrong order).
454
      skip: skipExpectsWithKnownBugs, // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
455
    );
456
    painter.dispose();
457
  }, skip: skipTestsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
458 459

  test('TextPainter - multiple levels', () {
460
    final TextPainter painter = TextPainter()
Ian Hickson's avatar
Ian Hickson committed
461 462 463
      ..textDirection = TextDirection.rtl;

    final String pyramid = rlo(lro(rlo(lro(rlo('')))));
464
    painter.text = TextSpan(
Ian Hickson's avatar
Ian Hickson committed
465
      text: pyramid,
466
      style: const TextStyle(fontSize: 10.0),
Ian Hickson's avatar
Ian Hickson committed
467 468 469 470
    );
    painter.layout();

    expect(
471
      painter.getBoxesForSelection(TextSelection(baseOffset: 0, extentOffset: pyramid.length)),
Ian Hickson's avatar
Ian Hickson committed
472
      const <TextBox>[
473 474 475 476 477 478 479 480 481
        TextBox.fromLTRBD(90.0, 0.0, 100.0, 10.0, TextDirection.rtl), // outer R, start (right)
        TextBox.fromLTRBD(10.0, 0.0,  20.0, 10.0, TextDirection.ltr), // level 1 L, start (left)
        TextBox.fromLTRBD(70.0, 0.0,  80.0, 10.0, TextDirection.rtl), // level 2 R, start (right)
        TextBox.fromLTRBD(30.0, 0.0,  40.0, 10.0, TextDirection.ltr), // level 3 L, start (left)
        TextBox.fromLTRBD(40.0, 0.0,  60.0, 10.0, TextDirection.rtl), // inner-most RR
        TextBox.fromLTRBD(60.0, 0.0,  70.0, 10.0, TextDirection.ltr), // lever 3 L, end (right)
        TextBox.fromLTRBD(20.0, 0.0,  30.0, 10.0, TextDirection.rtl), // level 2 R, end (left)
        TextBox.fromLTRBD(80.0, 0.0,  90.0, 10.0, TextDirection.ltr), // level 1 L, end (right)
        TextBox.fromLTRBD( 0.0, 0.0,  10.0, 10.0, TextDirection.rtl), // outer R, end (left)
Ian Hickson's avatar
Ian Hickson committed
482 483 484 485
      ],
      // Horizontal offsets are currently one pixel off in places; vertical offsets are good.
      // The list is currently in the wrong order (so selection boxes will paint in the wrong order).
      // Also currently there's an extraneous box at the start of the list.
486
      skip: skipExpectsWithKnownBugs, // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
487
    );
488
    painter.dispose();
489
  }, skip: skipTestsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
490 491

  test('TextPainter - getPositionForOffset - RTL in LTR', () {
492
    final TextPainter painter = TextPainter()
Ian Hickson's avatar
Ian Hickson committed
493 494 495 496
      ..textDirection = TextDirection.ltr;

    painter.text = const TextSpan(
      text: 'ABC\u05D0\u05D1\u05D2DEF', // A B C Alef Bet Gimel D E F -- but the Hebrew letters are RTL
497
      style: TextStyle(fontSize: 10.0),
Ian Hickson's avatar
Ian Hickson committed
498 499 500 501 502 503 504 505
    );
    painter.layout();

    // TODO(ianh): Remove the toString()s once https://github.com/flutter/engine/pull/4283 lands
    expect(
      //  Aaa  Bbb  Ccc  Gimel  Bet  Alef  Ddd  Eee  Fff
      // ^
      painter.getPositionForOffset(const Offset(0.0, 5.0)).toString(),
506
      const TextPosition(offset: 0).toString(),
Ian Hickson's avatar
Ian Hickson committed
507 508 509 510 511
    );
    expect(
      //                     Aaa  Bbb  Ccc  Gimel  Bet  Alef  Ddd  Eee  Fff
      // ^
      painter.getPositionForOffset(const Offset(-100.0, 5.0)).toString(),
512
      const TextPosition(offset: 0).toString(),
Ian Hickson's avatar
Ian Hickson committed
513 514 515 516 517
    );
    expect(
      //  Aaa  Bbb  Ccc  Gimel  Bet  Alef  Ddd  Eee  Fff
      //  ^
      painter.getPositionForOffset(const Offset(4.0, 5.0)).toString(),
518
      const TextPosition(offset: 0).toString(),
Ian Hickson's avatar
Ian Hickson committed
519 520 521 522 523 524 525 526 527 528 529
    );
    expect(
      //  Aaa  Bbb  Ccc  Gimel  Bet  Alef  Ddd  Eee  Fff
      //    ^
      painter.getPositionForOffset(const Offset(8.0, 5.0)).toString(),
      const TextPosition(offset: 1, affinity: TextAffinity.upstream).toString(),
    );
    expect(
      //  Aaa  Bbb  Ccc  Gimel  Bet  Alef  Ddd  Eee  Fff
      //       ^
      painter.getPositionForOffset(const Offset(12.0, 5.0)).toString(),
530
      const TextPosition(offset: 1).toString(),
531 532
      // currently we say upstream instead of downstream
      skip: skipExpectsWithKnownBugs, // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
    );
    expect(
      //  Aaa  Bbb  Ccc  Gimel  Bet  Alef  Ddd  Eee  Fff
      //              ^
      painter.getPositionForOffset(const Offset(28.0, 5.0)).toString(),
      const TextPosition(offset: 3, affinity: TextAffinity.upstream).toString(),
    );
    expect(
      //  Aaa  Bbb  Ccc  Gimel  Bet  Alef  Ddd  Eee  Fff
      //                 ^
      painter.getPositionForOffset(const Offset(32.0, 5.0)).toString(),
      const TextPosition(offset: 6, affinity: TextAffinity.upstream).toString(),
      skip: skipExpectsWithKnownBugs, // this is part of https://github.com/flutter/flutter/issues/11375
    );
    expect(
      //  Aaa  Bbb  Ccc  Gimel  Bet  Alef  Ddd  Eee  Fff
      //                                ^
      painter.getPositionForOffset(const Offset(58.0, 5.0)).toString(),
551
      const TextPosition(offset: 3).toString(),
Ian Hickson's avatar
Ian Hickson committed
552 553 554 555 556 557
      skip: skipExpectsWithKnownBugs, // this is part of https://github.com/flutter/flutter/issues/11375
    );
    expect(
      //  Aaa  Bbb  Ccc  Gimel  Bet  Alef  Ddd  Eee  Fff
      //                                   ^
      painter.getPositionForOffset(const Offset(62.0, 5.0)).toString(),
558
      const TextPosition(offset: 6).toString(),
Ian Hickson's avatar
Ian Hickson committed
559 560 561 562 563 564 565 566 567 568 569 570 571
    );
    expect(
      //  Aaa  Bbb  Ccc  Gimel  Bet  Alef  Ddd  Eee  Fff
      //                                               ^
      painter.getPositionForOffset(const Offset(88.0, 5.0)).toString(),
      const TextPosition(offset: 9, affinity: TextAffinity.upstream).toString(),
    );
    expect(
      //  Aaa  Bbb  Ccc  Gimel  Bet  Alef  Ddd  Eee  Fff
      //                                                     ^
      painter.getPositionForOffset(const Offset(100.0, 5.0)).toString(),
      const TextPosition(offset: 9, affinity: TextAffinity.upstream).toString(),
    );
572
    painter.dispose();
573
  }, skip: skipTestsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
574 575

  test('TextPainter - getPositionForOffset - LTR in RTL', () {
576
    final TextPainter painter = TextPainter()
Ian Hickson's avatar
Ian Hickson committed
577 578 579 580
      ..textDirection = TextDirection.rtl;

    painter.text = const TextSpan(
      text: '\u05D0\u05D1\u05D2ABC\u05D3\u05D4\u05D5',
581
      style: TextStyle(fontSize: 10.0),
Ian Hickson's avatar
Ian Hickson committed
582 583 584 585 586 587 588 589 590 591 592 593 594 595
    );
    painter.layout();

    // TODO(ianh): Remove the toString()s once https://github.com/flutter/engine/pull/4283 lands
    expect(
      //   Vav He Dalet Aaa Bbb Ccc Gimel Bet Alef
      // ^
      painter.getPositionForOffset(const Offset(-4.0, 5.0)).toString(),
      const TextPosition(offset: 9, affinity: TextAffinity.upstream).toString(),
    );
    expect(
      // Vav He Dalet Aaa Bbb Ccc Gimel Bet Alef
      //            ^
      painter.getPositionForOffset(const Offset(28.0, 5.0)).toString(),
596
      const TextPosition(offset: 6).toString(),
Ian Hickson's avatar
Ian Hickson committed
597 598 599 600 601
    );
    expect(
      // Vav He Dalet Aaa Bbb Ccc Gimel Bet Alef
      //              ^
      painter.getPositionForOffset(const Offset(32.0, 5.0)).toString(),
602
      const TextPosition(offset: 3).toString(),
Ian Hickson's avatar
Ian Hickson committed
603 604 605 606 607 608 609 610 611 612 613 614 615 616 617
      skip: skipExpectsWithKnownBugs, // this is part of https://github.com/flutter/flutter/issues/11375
    );
    expect(
      // Vav He Dalet Aaa Bbb Ccc Gimel Bet Alef
      //                        ^
      painter.getPositionForOffset(const Offset(58.0, 5.0)).toString(),
      const TextPosition(offset: 6, affinity: TextAffinity.upstream).toString(),
      skip: skipExpectsWithKnownBugs, // this is part of https://github.com/flutter/flutter/issues/11375
    );
    expect(
      // Vav He Dalet Aaa Bbb Ccc Gimel Bet Alef
      //                          ^
      painter.getPositionForOffset(const Offset(62.0, 5.0)).toString(),
      const TextPosition(offset: 3, affinity: TextAffinity.upstream).toString(),
    );
618
    painter.dispose();
619
  }, skip: skipTestsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
620 621

  test('TextPainter - Spaces', () {
622
    final TextPainter painter = TextPainter()
Ian Hickson's avatar
Ian Hickson committed
623 624 625 626
      ..textDirection = TextDirection.ltr;

    painter.text = const TextSpan(
      text: ' ',
627
      style: TextStyle(fontSize: 100.0),
628 629
      children: <TextSpan>[
        TextSpan(
Ian Hickson's avatar
Ian Hickson committed
630
          text: ' ',
631
          style: TextStyle(fontSize: 10.0),
Ian Hickson's avatar
Ian Hickson committed
632
        ),
633
        TextSpan(
Ian Hickson's avatar
Ian Hickson committed
634
          text: ' ',
635
          style: TextStyle(fontSize: 200.0),
Ian Hickson's avatar
Ian Hickson committed
636
        ),
637 638
        // Add a non-whitespace character because the renderer's line breaker
        // may strip trailing whitespace on a line.
639
        TextSpan(text: 'A'),
Ian Hickson's avatar
Ian Hickson committed
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664
      ],
    );
    painter.layout();

    // This renders as three (invisible) boxes:
    //
    //                 |<--------200------->|
    //                  ____________________
    //                 |        ^           |
    //                 |        :           |
    //                 |        :           |
    //                 |        :           |
    //                 |        :           |
    //   ___________   |        : 160       |
    //  |  ^        |  |        :           |
    //  |<-+-100--->|10|        :           |
    //  |  :        |__|        :           |
    //  |  : 80     |  |8       :           |
    // _|__v________|__|________v___________| BASELINE
    //  |     ^20   |__|2       ^           |
    //  |_____v_____|  |        |           |
    //                 |        | 40        |
    //                 |        |           |
    //                 |________v___________|

665
    expect(painter.width, 410.0);
Ian Hickson's avatar
Ian Hickson committed
666
    expect(painter.height, 200.0);
667
    expect(painter.computeDistanceToActualBaseline(TextBaseline.alphabetic), moreOrLessEquals(160.0, epsilon: 0.001));
Ian Hickson's avatar
Ian Hickson committed
668 669 670
    expect(painter.preferredLineHeight, 100.0);

    expect(
671
      painter.getBoxesForSelection(const TextSelection(baseOffset: 0, extentOffset: 3)),
Ian Hickson's avatar
Ian Hickson committed
672
      const <TextBox>[
673 674 675
        TextBox.fromLTRBD(  0.0,  80.0, 100.0, 180.0, TextDirection.ltr),
        TextBox.fromLTRBD(100.0, 152.0, 110.0, 162.0, TextDirection.ltr),
        TextBox.fromLTRBD(110.0,   0.0, 310.0, 200.0, TextDirection.ltr),
Ian Hickson's avatar
Ian Hickson committed
676 677
      ],
      // Horizontal offsets are currently one pixel off in places; vertical offsets are good.
678
      skip: skipExpectsWithKnownBugs, // https://github.com/flutter/flutter/issues/87536
Ian Hickson's avatar
Ian Hickson committed
679
    );
680
    painter.dispose();
681
  }, skip: skipTestsWithKnownBugs); // https://github.com/flutter/flutter/issues/87536
682 683

  test('TextPainter - empty text baseline', () {
684
    final TextPainter painter = TextPainter()
685 686 687
      ..textDirection = TextDirection.ltr;
    painter.text = const TextSpan(
      text: '',
688
      style: TextStyle(fontFamily: 'FlutterTest', fontSize: 100.0, height: 1.0),
689 690
    );
    painter.layout();
691
    expect(painter.computeDistanceToActualBaseline(TextBaseline.alphabetic), 75.0);
692
    painter.dispose();
693
  });
Ian Hickson's avatar
Ian Hickson committed
694 695 696 697
}

String lro(String s) => '${Unicode.LRO}L${s}L${Unicode.PDF}';
String rlo(String s) => '${Unicode.RLO}R${s}R${Unicode.PDF}';