| // 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. |
| |
| import 'dart:ui'; |
| |
| import 'package:litetest/litetest.dart'; |
| |
| void main() { |
| // Ahem font uses a constant ideographic/alphabetic baseline ratio. |
| const double kAhemBaselineRatio = 1.25; |
| |
| test('predictably lays out a single-line paragraph - Ahem', () { |
| for (final double fontSize in <double>[10.0, 20.0, 30.0, 40.0]) { |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( |
| fontFamily: 'Ahem', |
| fontStyle: FontStyle.normal, |
| fontWeight: FontWeight.normal, |
| fontSize: fontSize, |
| )); |
| builder.addText('Test'); |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(const ParagraphConstraints(width: 400.0)); |
| |
| expect(paragraph.height, closeTo(fontSize, 0.001)); |
| expect(paragraph.width, closeTo(400.0, 0.001)); |
| expect(paragraph.minIntrinsicWidth, closeTo(fontSize * 4.0, 0.001)); |
| expect(paragraph.maxIntrinsicWidth, closeTo(fontSize * 4.0, 0.001)); |
| expect(paragraph.alphabeticBaseline, closeTo(fontSize * .8, 0.001)); |
| expect( |
| paragraph.ideographicBaseline, |
| closeTo(paragraph.alphabeticBaseline * kAhemBaselineRatio, 0.001), |
| ); |
| } |
| }); |
| |
| test('predictably lays out a single-line paragraph - FlutterTest', () { |
| for (final double fontSize in <double>[10.0, 20.0, 30.0, 40.0]) { |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( |
| fontFamily: 'FlutterTest', |
| fontStyle: FontStyle.normal, |
| fontWeight: FontWeight.normal, |
| fontSize: fontSize, |
| )); |
| builder.addText('Test'); |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(const ParagraphConstraints(width: 400.0)); |
| |
| expect(paragraph.height, fontSize); |
| expect(paragraph.width, 400.0); |
| expect(paragraph.minIntrinsicWidth, fontSize * 4.0); |
| expect(paragraph.maxIntrinsicWidth, fontSize * 4.0); |
| expect(paragraph.alphabeticBaseline, fontSize * 0.75); |
| expect(paragraph.ideographicBaseline, fontSize); |
| } |
| }); |
| |
| test('predictably lays out a multi-line paragraph', () { |
| for (final double fontSize in <double>[10.0, 20.0, 30.0, 40.0]) { |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( |
| fontFamily: 'Ahem', |
| fontStyle: FontStyle.normal, |
| fontWeight: FontWeight.normal, |
| fontSize: fontSize, |
| )); |
| builder.addText('Test Ahem'); |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(ParagraphConstraints(width: fontSize * 5.0)); |
| |
| expect(paragraph.height, closeTo(fontSize * 2.0, 0.001)); // because it wraps |
| expect(paragraph.width, closeTo(fontSize * 5.0, 0.001)); |
| expect(paragraph.minIntrinsicWidth, closeTo(fontSize * 4.0, 0.001)); |
| |
| // TODO(yjbanov): see https://github.com/flutter/flutter/issues/21965 |
| expect(paragraph.maxIntrinsicWidth, closeTo(fontSize * 9.0, 0.001)); |
| expect(paragraph.alphabeticBaseline, closeTo(fontSize * .8, 0.001)); |
| expect( |
| paragraph.ideographicBaseline, |
| closeTo(paragraph.alphabeticBaseline * kAhemBaselineRatio, 0.001), |
| ); |
| } |
| }); |
| |
| test('getLineBoundary', () { |
| const double fontSize = 10.0; |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( |
| fontFamily: 'Ahem', |
| fontStyle: FontStyle.normal, |
| fontWeight: FontWeight.normal, |
| fontSize: fontSize, |
| )); |
| builder.addText('Test Ahem'); |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(const ParagraphConstraints(width: fontSize * 5.0)); |
| |
| // Wraps to two lines. |
| expect(paragraph.height, closeTo(fontSize * 2.0, 0.001)); |
| |
| const TextPosition wrapPositionDown = TextPosition( |
| offset: 5, |
| ); |
| TextRange line = paragraph.getLineBoundary(wrapPositionDown); |
| expect(line.start, 5); |
| expect(line.end, 9); |
| |
| const TextPosition wrapPositionUp = TextPosition( |
| offset: 5, |
| affinity: TextAffinity.upstream, |
| ); |
| line = paragraph.getLineBoundary(wrapPositionUp); |
| expect(line.start, 0); |
| expect(line.end, 5); |
| |
| const TextPosition wrapPositionStart = TextPosition( |
| offset: 0, |
| ); |
| line = paragraph.getLineBoundary(wrapPositionStart); |
| expect(line.start, 0); |
| expect(line.end, 5); |
| |
| const TextPosition wrapPositionEnd = TextPosition( |
| offset: 9, |
| ); |
| line = paragraph.getLineBoundary(wrapPositionEnd); |
| expect(line.start, 5); |
| expect(line.end, 9); |
| }); |
| |
| test('getLineBoundary RTL', () { |
| const double fontSize = 10.0; |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( |
| fontFamily: 'Ahem', |
| fontStyle: FontStyle.normal, |
| fontWeight: FontWeight.normal, |
| fontSize: fontSize, |
| textDirection: TextDirection.rtl, |
| )); |
| builder.addText('القاهرةالقاهرة'); |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(const ParagraphConstraints(width: fontSize * 5.0)); |
| |
| // Wraps to three lines. |
| expect(paragraph.height, closeTo(fontSize * 3.0, 0.001)); |
| |
| const TextPosition wrapPositionDown = TextPosition( |
| offset: 5, |
| ); |
| TextRange line = paragraph.getLineBoundary(wrapPositionDown); |
| expect(line.start, 5); |
| expect(line.end, 10); |
| |
| const TextPosition wrapPositionUp = TextPosition( |
| offset: 5, |
| affinity: TextAffinity.upstream, |
| ); |
| line = paragraph.getLineBoundary(wrapPositionUp); |
| expect(line.start, 0); |
| expect(line.end, 5); |
| |
| const TextPosition wrapPositionStart = TextPosition( |
| offset: 0, |
| ); |
| line = paragraph.getLineBoundary(wrapPositionStart); |
| expect(line.start, 0); |
| expect(line.end, 5); |
| |
| const TextPosition wrapPositionEnd = TextPosition( |
| offset: 9, |
| ); |
| line = paragraph.getLineBoundary(wrapPositionEnd); |
| expect(line.start, 5); |
| expect(line.end, 10); |
| }); |
| |
| test('getLineBoundary empty line', () { |
| const double fontSize = 10.0; |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( |
| fontFamily: 'Ahem', |
| fontStyle: FontStyle.normal, |
| fontWeight: FontWeight.normal, |
| fontSize: fontSize, |
| textDirection: TextDirection.rtl, |
| )); |
| builder.addText('Test\n\nAhem'); |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(const ParagraphConstraints(width: fontSize * 5.0)); |
| |
| // Three lines due to line breaks, with the middle line being empty. |
| expect(paragraph.height, closeTo(fontSize * 3.0, 0.001)); |
| |
| const TextPosition emptyLinePosition = TextPosition( |
| offset: 5, |
| ); |
| TextRange line = paragraph.getLineBoundary(emptyLinePosition); |
| expect(line.start, 5); |
| expect(line.end, 5); |
| |
| // Since these are hard newlines, TextAffinity has no effect here. |
| const TextPosition emptyLinePositionUpstream = TextPosition( |
| offset: 5, |
| affinity: TextAffinity.upstream, |
| ); |
| line = paragraph.getLineBoundary(emptyLinePositionUpstream); |
| expect(line.start, 5); |
| expect(line.end, 5); |
| |
| const TextPosition endOfFirstLinePosition = TextPosition( |
| offset: 4, |
| ); |
| line = paragraph.getLineBoundary(endOfFirstLinePosition); |
| expect(line.start, 0); |
| expect(line.end, 4); |
| |
| const TextPosition startOfLastLinePosition = TextPosition( |
| offset: 6, |
| ); |
| line = paragraph.getLineBoundary(startOfLastLinePosition); |
| expect(line.start, 6); |
| expect(line.end, 10); |
| }); |
| |
| test('getLineMetricsAt', () { |
| const double fontSize = 10.0; |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( |
| fontSize: fontSize, |
| textDirection: TextDirection.rtl, |
| height: 2.0, |
| )); |
| builder.addText('Test\npppp'); |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(const ParagraphConstraints(width: 100.0)); |
| final LineMetrics? line = paragraph.getLineMetricsAt(1); |
| expect(line?.hardBreak, isTrue); |
| expect(line?.ascent, 15.0); |
| expect(line?.descent, 5.0); |
| expect(line?.height, 20.0); |
| expect(line?.width, 4 * 10.0); |
| expect(line?.left, 100.0 - 40.0); |
| expect(line?.baseline, 20.0 + 15.0); |
| expect(line?.lineNumber, 1); |
| }); |
| |
| test('line number', () { |
| const double fontSize = 10.0; |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(fontSize: fontSize)); |
| builder.addText('Test\n\nTest'); |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(const ParagraphConstraints(width: 100.0)); |
| expect(paragraph.numberOfLines, 3); |
| expect(paragraph.getLineNumberAt(4), 0); // first LF |
| expect(paragraph.getLineNumberAt(5), 1); // second LF |
| expect(paragraph.getLineNumberAt(6), 2); // "T" in the second "Test" |
| }); |
| |
| test('empty paragraph', () { |
| const double fontSize = 10.0; |
| final Paragraph paragraph = ParagraphBuilder(ParagraphStyle( |
| fontSize: fontSize, |
| )).build(); |
| paragraph.layout(const ParagraphConstraints(width: double.infinity)); |
| |
| expect(paragraph.getClosestGlyphInfoForOffset(Offset.zero), isNull); |
| expect(paragraph.getGlyphInfoAt(0), isNull); |
| |
| expect(paragraph.getLineMetricsAt(0), isNull); |
| expect(paragraph.numberOfLines, 0); |
| expect(paragraph.getLineNumberAt(0), isNull); |
| |
| expect(paragraph.getGlyphInfoAt(0), isNull); |
| expect(paragraph.getClosestGlyphInfoForOffset(Offset.zero), isNull); |
| }); |
| |
| test('OOB indices as input', () { |
| const double fontSize = 10.0; |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( |
| fontSize: fontSize, |
| maxLines: 1, |
| ellipsis: 'BBB', |
| ))..addText('A' * 100); |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(const ParagraphConstraints(width: 100)); |
| |
| expect(paragraph.numberOfLines, 1); |
| |
| expect(paragraph.getLineMetricsAt(-1), isNull); |
| expect(paragraph.getLineMetricsAt(0)?.lineNumber, 0); |
| expect(paragraph.getLineMetricsAt(1), isNull); |
| |
| expect(paragraph.getLineNumberAt(-1), isNull); |
| expect(paragraph.getLineNumberAt(0), 0); |
| expect(paragraph.getLineNumberAt(6), 0); |
| // The last 3 characters on the first line are ellipsized with BBB. |
| expect(paragraph.getLineMetricsAt(7), isNull); |
| |
| expect(paragraph.getGlyphInfoAt(-1), isNull); |
| expect(paragraph.getGlyphInfoAt(0)?.graphemeClusterCodeUnitRange, const TextRange(start: 0, end: 1)); |
| expect(paragraph.getGlyphInfoAt(6)?.graphemeClusterCodeUnitRange, const TextRange(start: 6, end: 7)); |
| expect(paragraph.getGlyphInfoAt(7), isNull); |
| expect(paragraph.getGlyphInfoAt(200), isNull); |
| }); |
| |
| test('querying glyph info', () { |
| const double fontSize = 10.0; |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( |
| fontSize: fontSize, |
| )); |
| builder.addText('Test\nTest'); |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(const ParagraphConstraints(width: double.infinity)); |
| |
| final GlyphInfo? bottomRight = paragraph.getClosestGlyphInfoForOffset(const Offset(99.0, 99.0)); |
| final GlyphInfo? last = paragraph.getGlyphInfoAt(8); |
| expect(bottomRight, equals(last)); |
| expect(bottomRight, notEquals(paragraph.getGlyphInfoAt(0))); |
| |
| expect(bottomRight?.graphemeClusterLayoutBounds, const Rect.fromLTWH(30, 10, 10, 10)); |
| expect(bottomRight?.graphemeClusterCodeUnitRange, const TextRange(start: 8, end: 9)); |
| expect(bottomRight?.writingDirection, TextDirection.ltr); |
| }); |
| |
| test('painting a disposed paragraph does not crash', () { |
| final Paragraph paragraph = ParagraphBuilder(ParagraphStyle()).build(); |
| paragraph.dispose(); |
| |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| |
| void callback() { canvas.drawParagraph(paragraph, Offset.zero); } |
| if (assertStatementsEnabled) { |
| expectAssertion(callback); |
| } else { |
| expect(callback, throwsStateError); |
| } |
| }); |
| |
| test('rounding hack disabled', () { |
| const double fontSize = 1.25; |
| const String text = '12345'; |
| assert((fontSize * text.length).truncate() != fontSize * text.length); |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(fontSize: fontSize)); |
| builder.addText(text); |
| final Paragraph paragraph = builder.build() |
| ..layout(const ParagraphConstraints(width: text.length * fontSize)); |
| expect(paragraph.maxIntrinsicWidth, text.length * fontSize); |
| switch (paragraph.computeLineMetrics()) { |
| case [LineMetrics(width: final double width)]: |
| expect(width, text.length * fontSize); |
| case final List<LineMetrics> metrics: |
| expect(metrics, hasLength(1)); |
| } |
| }); |
| } |