| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // @dart = 2.12 |
| import 'package:test/bootstrap/browser.dart'; |
| import 'package:test/test.dart'; |
| import 'package:ui/src/engine.dart'; |
| import 'package:ui/ui.dart' as ui; |
| |
| const ui.Color white = ui.Color(0xFFFFFFFF); |
| const ui.Color black = ui.Color(0xFF000000); |
| const ui.Color red = ui.Color(0xFFFF0000); |
| const ui.Color green = ui.Color(0xFF00FF00); |
| const ui.Color blue = ui.Color(0xFF0000FF); |
| |
| final EngineParagraphStyle ahemStyle = EngineParagraphStyle( |
| fontFamily: 'ahem', |
| fontSize: 10, |
| ); |
| |
| ui.ParagraphConstraints constrain(double width) { |
| return ui.ParagraphConstraints(width: width); |
| } |
| |
| CanvasParagraph rich( |
| EngineParagraphStyle style, |
| void Function(CanvasParagraphBuilder) callback, |
| ) { |
| final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style); |
| callback(builder); |
| return builder.build(); |
| } |
| |
| void main() { |
| internalBootstrapBrowserTest(() => testMain); |
| } |
| |
| void testMain() async { |
| await ui.webOnlyInitializeTestDomRenderer(); |
| |
| group('$CanvasParagraph.getBoxesForRange', () { |
| test('return empty list for invalid ranges', () { |
| final CanvasParagraph paragraph = rich(ahemStyle, (builder) { |
| builder.addText('Lorem ipsum'); |
| }) |
| ..layout(constrain(double.infinity)); |
| |
| expect(paragraph.getBoxesForRange(-1, 0), <ui.TextBox>[]); |
| expect(paragraph.getBoxesForRange(0, 0), <ui.TextBox>[]); |
| expect(paragraph.getBoxesForRange(11, 11), <ui.TextBox>[]); |
| expect(paragraph.getBoxesForRange(11, 12), <ui.TextBox>[]); |
| expect(paragraph.getBoxesForRange(4, 3), <ui.TextBox>[]); |
| }); |
| |
| test('handles single-line multi-span paragraphs', () { |
| final CanvasParagraph paragraph = rich(ahemStyle, (builder) { |
| builder.pushStyle(EngineTextStyle.only(color: blue)); |
| builder.addText('Lorem '); |
| builder.pushStyle(EngineTextStyle.only(color: green)); |
| builder.addText('ipsum '); |
| builder.pop(); |
| builder.addText('.'); |
| }) |
| ..layout(constrain(double.infinity)); |
| |
| // Within the first span "Lorem ". |
| |
| expect( |
| // "or" |
| paragraph.getBoxesForRange(1, 3), |
| <ui.TextBox>[ |
| box(10, 0, 30, 10), |
| ], |
| ); |
| expect( |
| // "Lorem" |
| paragraph.getBoxesForRange(0, 5), |
| <ui.TextBox>[ |
| box(0, 0, 50, 10), |
| ], |
| ); |
| // Make sure the trailing space is also included in the box. |
| expect( |
| // "Lorem " |
| paragraph.getBoxesForRange(0, 6), |
| <ui.TextBox>[ |
| box(0, 0, 60, 10), |
| ], |
| ); |
| |
| // Within the second span "ipsum ". |
| |
| expect( |
| // "psum" |
| paragraph.getBoxesForRange(7, 11), |
| <ui.TextBox>[ |
| box(70, 0, 110, 10), |
| ], |
| ); |
| expect( |
| // "um " |
| paragraph.getBoxesForRange(9, 12), |
| <ui.TextBox>[ |
| box(90, 0, 120, 10), |
| ], |
| ); |
| |
| // Across the two spans "Lorem " and "ipsum ". |
| |
| expect( |
| // "rem ipsum" |
| paragraph.getBoxesForRange(2, 11), |
| <ui.TextBox>[ |
| box(20, 0, 60, 10), |
| box(60, 0, 110, 10), |
| ], |
| ); |
| |
| // Across all spans "Lorem ", "ipsum ", ".". |
| |
| expect( |
| // "Lorem ipsum." |
| paragraph.getBoxesForRange(0, 13), |
| <ui.TextBox>[ |
| box(0, 0, 60, 10), |
| box(60, 0, 120, 10), |
| box(120, 0, 130, 10), |
| ], |
| ); |
| }); |
| |
| test('handles multi-line single-span paragraphs', () { |
| final CanvasParagraph paragraph = rich(ahemStyle, (builder) { |
| builder.addText('Lorem ipsum dolor sit'); |
| }) |
| ..layout(constrain(90.0)); |
| |
| // Lines: |
| // "Lorem " |
| // "ipsum " |
| // "dolor sit" |
| |
| // Within the first line "Lorem ". |
| |
| expect( |
| // "or" |
| paragraph.getBoxesForRange(1, 3), |
| <ui.TextBox>[ |
| box(10, 0, 30, 10), |
| ], |
| ); |
| // Make sure the trailing space at the end of line is also included in the |
| // box. |
| expect( |
| // "Lorem " |
| paragraph.getBoxesForRange(0, 6), |
| <ui.TextBox>[ |
| box(0, 0, 60, 10), |
| ], |
| ); |
| |
| // Within the second line "ipsum ". |
| |
| expect( |
| // "psum " |
| paragraph.getBoxesForRange(7, 12), |
| <ui.TextBox>[ |
| box(10, 10, 60, 20), |
| ], |
| ); |
| |
| // Across all lines. |
| |
| expect( |
| // "em " |
| // "ipsum " |
| // "dolor s" |
| paragraph.getBoxesForRange(3, 19), |
| <ui.TextBox>[ |
| box(30, 0, 60, 10), |
| box(0, 10, 60, 20), |
| box(0, 20, 70, 30), |
| ], |
| ); |
| }); |
| |
| test('handles multi-line multi-span paragraphs', () { |
| final CanvasParagraph paragraph = rich(ahemStyle, (builder) { |
| builder.pushStyle(EngineTextStyle.only(color: blue)); |
| builder.addText('Lorem ipsum '); |
| builder.pushStyle(EngineTextStyle.only(color: green)); |
| builder.addText('dolor '); |
| builder.pop(); |
| builder.addText('sit'); |
| }) |
| ..layout(constrain(90.0)); |
| |
| // Lines: |
| // "Lorem " |
| // "ipsum " |
| // "dolor sit" |
| |
| // Within the first line "Lorem ". |
| |
| expect( |
| // "ore" |
| paragraph.getBoxesForRange(1, 4), |
| <ui.TextBox>[ |
| box(10, 0, 40, 10), |
| ], |
| ); |
| expect( |
| // "Lorem " |
| paragraph.getBoxesForRange(0, 6), |
| <ui.TextBox>[ |
| box(0, 0, 60, 10), |
| ], |
| ); |
| |
| // Within the second line "ipsum ". |
| |
| expect( |
| // "psum " |
| paragraph.getBoxesForRange(7, 12), |
| <ui.TextBox>[ |
| box(10, 10, 60, 20), |
| ], |
| ); |
| |
| // Within the third line "dolor sit" which is made of 2 spans. |
| |
| expect( |
| // "lor sit" |
| paragraph.getBoxesForRange(14, 21), |
| <ui.TextBox>[ |
| box(20, 20, 60, 30), |
| box(60, 20, 90, 30), |
| ], |
| ); |
| |
| // Across all lines. |
| |
| expect( |
| // "em " |
| // "ipsum " |
| // "dolor s" |
| paragraph.getBoxesForRange(3, 19), |
| <ui.TextBox>[ |
| box(30, 0, 60, 10), |
| box(0, 10, 60, 20), |
| box(0, 20, 60, 30), |
| box(60, 20, 70, 30), |
| ], |
| ); |
| }); |
| |
| test('handles spans with varying heights/baselines', () { |
| final CanvasParagraph paragraph = rich(ahemStyle, (builder) { |
| builder.pushStyle(EngineTextStyle.only(fontSize: 20.0)); |
| // width = 20.0 * 6 = 120.0 |
| // baseline = 20.0 * 80% = 16.0 |
| builder.addText('Lorem '); |
| builder.pushStyle(EngineTextStyle.only(fontSize: 40.0)); |
| // width = 40.0 * 6 = 240.0 |
| // baseline = 40.0 * 80% = 32.0 |
| builder.addText('ipsum '); |
| builder.pushStyle(EngineTextStyle.only(fontSize: 10.0)); |
| // width = 10.0 * 6 = 60.0 |
| // baseline = 10.0 * 80% = 8.0 |
| builder.addText('dolor '); |
| builder.pushStyle(EngineTextStyle.only(fontSize: 30.0)); |
| // width = 30.0 * 4 = 120.0 |
| // baseline = 30.0 * 80% = 24.0 |
| builder.addText('sit '); |
| builder.pushStyle(EngineTextStyle.only(fontSize: 20.0)); |
| // width = 20.0 * 4 = 80.0 |
| // baseline = 20.0 * 80% = 16.0 |
| builder.addText('amet'); |
| }) |
| ..layout(constrain(420.0)); |
| |
| // Lines: |
| // "Lorem ipsum dolor " (width: 420, height: 40, baseline: 32) |
| // "sit amet" (width: 200, height: 30, baseline: 24) |
| |
| expect( |
| // "em ipsum dol" |
| paragraph.getBoxesForRange(3, 15), |
| <ui.TextBox>[ |
| box(60, 16, 120, 36), |
| box(120, 0, 360, 40), |
| box(360, 24, 390, 34), |
| ], |
| ); |
| |
| expect( |
| // "sum dolor " |
| // "sit amet" |
| paragraph.getBoxesForRange(8, 26), |
| <ui.TextBox>[ |
| box(200, 0, 360, 40), |
| box(360, 24, 420, 34), |
| box(0, 40, 120, 70), |
| box(120, 48, 200, 68), |
| ], |
| ); |
| }); |
| }); |
| |
| group('$CanvasParagraph.getPositionForOffset', () { |
| test('handles single-line multi-span paragraphs', () { |
| final CanvasParagraph paragraph = rich(ahemStyle, (builder) { |
| builder.pushStyle(EngineTextStyle.only(color: blue)); |
| builder.addText('Lorem '); |
| builder.pushStyle(EngineTextStyle.only(color: green)); |
| builder.addText('ipsum '); |
| builder.pop(); |
| builder.addText('.'); |
| }) |
| ..layout(constrain(double.infinity)); |
| |
| // Above the line, at the beginning. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(0, -5)), |
| pos(0, ui.TextAffinity.downstream), |
| ); |
| // At the top left corner of the line. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(0, 0)), |
| pos(0, ui.TextAffinity.downstream), |
| ); |
| // At the beginning of the line. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(0, 5)), |
| pos(0, ui.TextAffinity.downstream), |
| ); |
| // Below the line, at the end. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(130, 12)), |
| pos(13, ui.TextAffinity.upstream), |
| ); |
| // At the end of the line. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(130, 5)), |
| pos(13, ui.TextAffinity.upstream), |
| ); |
| // On the left half of "p" in "ipsum". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(74, 5)), |
| pos(7, ui.TextAffinity.downstream), |
| ); |
| // On the left half of "p" in "ipsum" (above the line). |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(74, -5)), |
| pos(7, ui.TextAffinity.downstream), |
| ); |
| // On the left half of "p" in "ipsum" (below the line). |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(74, 15)), |
| pos(7, ui.TextAffinity.downstream), |
| ); |
| // On the right half of "p" in "ipsum". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(76, 5)), |
| pos(8, ui.TextAffinity.upstream), |
| ); |
| // At the top of the line, on the left half of "p" in "ipsum". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(74, 0)), |
| pos(7, ui.TextAffinity.downstream), |
| ); |
| // At the top of the line, on the right half of "p" in "ipsum". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(76, 0)), |
| pos(8, ui.TextAffinity.upstream), |
| ); |
| }); |
| |
| test('handles multi-line single-span paragraphs', () { |
| final CanvasParagraph paragraph = rich(ahemStyle, (builder) { |
| builder.addText('Lorem ipsum dolor sit'); |
| }) |
| ..layout(constrain(90.0)); |
| |
| // Lines: |
| // "Lorem " |
| // "ipsum " |
| // "dolor sit" |
| |
| // Above the first line, at the beginning. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(0, -5)), |
| pos(0, ui.TextAffinity.downstream), |
| ); |
| // At the top left corner of the line. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(0, 0)), |
| pos(0, ui.TextAffinity.downstream), |
| ); |
| // At the beginning of the first line. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(0, 5)), |
| pos(0, ui.TextAffinity.downstream), |
| ); |
| // At the end of the first line. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(60, 5)), |
| pos(6, ui.TextAffinity.upstream), |
| ); |
| // At the end of the first line (above the line). |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(60, -5)), |
| pos(6, ui.TextAffinity.upstream), |
| ); |
| // After the end of the first line to the right. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(70, 5)), |
| pos(6, ui.TextAffinity.upstream), |
| ); |
| // On the left half of " " in "Lorem ". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(54, 5)), |
| pos(5, ui.TextAffinity.downstream), |
| ); |
| // On the right half of " " in "Lorem ". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(56, 5)), |
| pos(6, ui.TextAffinity.upstream), |
| ); |
| |
| // At the beginning of the second line "ipsum ". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(0, 15)), |
| pos(6, ui.TextAffinity.downstream), |
| ); |
| // At the end of the second line. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(60, 15)), |
| pos(12, ui.TextAffinity.upstream), |
| ); |
| // After the end of the second line to the right. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(70, 15)), |
| pos(12, ui.TextAffinity.upstream), |
| ); |
| |
| // Below the third line "dolor sit", at the end. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(90, 40)), |
| pos(21, ui.TextAffinity.upstream), |
| ); |
| // At the end of the third line. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(90, 25)), |
| pos(21, ui.TextAffinity.upstream), |
| ); |
| // After the end of the third line to the right. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(100, 25)), |
| pos(21, ui.TextAffinity.upstream), |
| ); |
| // On the left half of " " in "dolor sit". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(54, 25)), |
| pos(17, ui.TextAffinity.downstream), |
| ); |
| // On the right half of " " in "dolor sit". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(56, 25)), |
| pos(18, ui.TextAffinity.upstream), |
| ); |
| }); |
| |
| test('handles multi-line multi-span paragraphs', () { |
| final CanvasParagraph paragraph = rich(ahemStyle, (builder) { |
| builder.pushStyle(EngineTextStyle.only(color: blue)); |
| builder.addText('Lorem ipsum '); |
| builder.pushStyle(EngineTextStyle.only(color: green)); |
| builder.addText('dolor '); |
| builder.pop(); |
| builder.addText('sit'); |
| }) |
| ..layout(constrain(90.0)); |
| |
| // Lines: |
| // "Lorem " |
| // "ipsum " |
| // "dolor sit" |
| |
| // Above the first line, at the beginning. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(0, -5)), |
| pos(0, ui.TextAffinity.downstream), |
| ); |
| // At the beginning of the first line. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(0, 5)), |
| pos(0, ui.TextAffinity.downstream), |
| ); |
| // At the end of the first line. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(60, 5)), |
| pos(6, ui.TextAffinity.upstream), |
| ); |
| // After the end of the first line to the right. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(70, 5)), |
| pos(6, ui.TextAffinity.upstream), |
| ); |
| // On the left half of " " in "Lorem ". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(54, 5)), |
| pos(5, ui.TextAffinity.downstream), |
| ); |
| // On the right half of " " in "Lorem ". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(56, 5)), |
| pos(6, ui.TextAffinity.upstream), |
| ); |
| |
| // At the beginning of the second line "ipsum ". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(0, 15)), |
| pos(6, ui.TextAffinity.downstream), |
| ); |
| // At the end of the second line. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(60, 15)), |
| pos(12, ui.TextAffinity.upstream), |
| ); |
| // After the end of the second line to the right. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(70, 15)), |
| pos(12, ui.TextAffinity.upstream), |
| ); |
| |
| // Below the third line "dolor sit", at the end. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(90, 40)), |
| pos(21, ui.TextAffinity.upstream), |
| ); |
| // At the end of the third line. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(90, 25)), |
| pos(21, ui.TextAffinity.upstream), |
| ); |
| // After the end of the third line to the right. |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(100, 25)), |
| pos(21, ui.TextAffinity.upstream), |
| ); |
| // On the left half of " " in "dolor sit". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(54, 25)), |
| pos(17, ui.TextAffinity.downstream), |
| ); |
| // On the right half of " " in "dolor sit". |
| expect( |
| paragraph.getPositionForOffset(ui.Offset(56, 25)), |
| pos(18, ui.TextAffinity.upstream), |
| ); |
| }); |
| }); |
| |
| group('$CanvasParagraph.getLineBoundary', () { |
| test('single-line', () { |
| final CanvasParagraph paragraph = rich(ahemStyle, (builder) { |
| builder.addText('One single line'); |
| }) |
| ..layout(constrain(400.0)); |
| |
| // "One single line".length == 15 |
| for (int i = 0; i < 15; i++) { |
| expect( |
| paragraph.getLineBoundary(ui.TextPosition(offset: i)), |
| ui.TextRange(start: 0, end: 15), |
| reason: 'failed at offset $i', |
| ); |
| } |
| }); |
| |
| test('multi-line', () { |
| final CanvasParagraph paragraph = rich(ahemStyle, (builder) { |
| builder.addText('First line\n'); |
| builder.addText('Second line\n'); |
| builder.addText('Third line'); |
| }) |
| ..layout(constrain(400.0)); |
| |
| // "First line\n".length == 11 |
| for (int i = 0; i < 11; i++) { |
| expect( |
| paragraph.getLineBoundary(ui.TextPosition(offset: i)), |
| ui.TextRange(start: 0, end: 11), |
| reason: 'failed at offset $i', |
| ); |
| } |
| |
| // "Second line\n".length == 12 |
| for (int i = 11; i < 23; i++) { |
| expect( |
| paragraph.getLineBoundary(ui.TextPosition(offset: i)), |
| ui.TextRange(start: 11, end: 23), |
| reason: 'failed at offset $i', |
| ); |
| } |
| |
| // "Third line".length == 10 |
| for (int i = 23; i < 33; i++) { |
| expect( |
| paragraph.getLineBoundary(ui.TextPosition(offset: i)), |
| ui.TextRange(start: 23, end: 33), |
| reason: 'failed at offset $i', |
| ); |
| } |
| }); |
| }); |
| } |
| |
| /// Shortcut to create a [ui.TextBox] with an optional [ui.TextDirection]. |
| ui.TextBox box( |
| double left, |
| double top, |
| double right, |
| double bottom, [ |
| ui.TextDirection direction = ui.TextDirection.ltr, |
| ]) { |
| return ui.TextBox.fromLTRBD(left, top, right, bottom, direction); |
| } |
| |
| /// Shortcut to create a [ui.TextPosition]. |
| ui.TextPosition pos(int offset, ui.TextAffinity affinity) { |
| return ui.TextPosition(offset: offset, affinity: affinity); |
| } |