mirror of
https://github.com/alibaba/flutter-go.git
synced 2025-07-15 03:04:25 +08:00
446 lines
16 KiB
Dart
446 lines
16 KiB
Dart
// Copyright 2018 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.
|
|
|
|
import 'package:flutter_web_ui/ui.dart' as ui;
|
|
import 'package:flutter_web_ui/src/engine.dart';
|
|
import 'package:flutter_web_test/flutter_web_test.dart';
|
|
|
|
final ui.ParagraphStyle ahemStyle = ui.ParagraphStyle(
|
|
fontFamily: 'ahem',
|
|
fontSize: 10,
|
|
);
|
|
const ui.ParagraphConstraints constraints = ui.ParagraphConstraints(width: 50);
|
|
const ui.ParagraphConstraints infiniteConstraints =
|
|
ui.ParagraphConstraints(width: double.infinity);
|
|
|
|
ui.Paragraph build(ui.ParagraphStyle style, String text) {
|
|
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(style);
|
|
builder.addText(text);
|
|
return builder.build();
|
|
}
|
|
|
|
typedef MeasurementTestBody = void Function(TextMeasurementService instance);
|
|
|
|
/// Runs the same test twice - once with dom measurement and once with canvas
|
|
/// measurement.
|
|
void testMeasurements(String description, MeasurementTestBody body) {
|
|
test(
|
|
'$description (dom)',
|
|
() => body(TextMeasurementService.domInstance),
|
|
);
|
|
test(
|
|
'$description (canvas)',
|
|
() => body(TextMeasurementService.canvasInstance),
|
|
);
|
|
}
|
|
|
|
void main() {
|
|
group('$RulerManager', () {
|
|
final ui.ParagraphStyle s1 = ui.ParagraphStyle(fontFamily: 'sans-serif');
|
|
final ui.ParagraphStyle s2 = ui.ParagraphStyle(
|
|
fontWeight: ui.FontWeight.bold,
|
|
);
|
|
final ui.ParagraphStyle s3 = ui.ParagraphStyle(fontSize: 22.0);
|
|
|
|
ParagraphGeometricStyle style1, style2, style3;
|
|
ui.Paragraph style1Text1, style1Text2; // two paragraphs sharing style
|
|
ui.Paragraph style2Text1, style3Text3;
|
|
|
|
setUp(() {
|
|
style1Text1 = build(s1, '1');
|
|
style1Text2 = build(s1, '2');
|
|
style2Text1 = build(s2, '1');
|
|
style3Text3 = build(s3, '3');
|
|
|
|
style1 = style1Text1.webOnlyGetParagraphGeometricStyle();
|
|
style2 = style2Text1.webOnlyGetParagraphGeometricStyle();
|
|
style3 = style3Text3.webOnlyGetParagraphGeometricStyle();
|
|
|
|
final ParagraphGeometricStyle style1_2 =
|
|
style1Text2.webOnlyGetParagraphGeometricStyle();
|
|
expect(style1_2, style1); // styles must be equal despite different text
|
|
});
|
|
|
|
test('caches rulers', () {
|
|
final RulerManager rulerManager = RulerManager(rulerCacheCapacity: 2);
|
|
ParagraphRuler ruler1, ruler2, ruler3;
|
|
|
|
expect(rulerManager.rulerCacheCapacity, 2);
|
|
expect(rulerManager.rulers.length, 0);
|
|
|
|
// First ruler cached
|
|
ruler1 = rulerManager.findOrCreateRuler(style1);
|
|
expect(rulerManager.rulers.length, 1);
|
|
expect(ruler1.hitCount, 1);
|
|
|
|
// Increase hit count for style 1
|
|
ruler1 = rulerManager.findOrCreateRuler(style1);
|
|
expect(rulerManager.rulers.length, 1);
|
|
expect(ruler1.hitCount, 2);
|
|
|
|
// Previous ruler reused
|
|
rulerManager.findOrCreateRuler(style1);
|
|
expect(rulerManager.rulers.length, 1);
|
|
expect(ruler1.hitCount, 3);
|
|
|
|
// Second ruler created and cached
|
|
ruler2 = rulerManager.findOrCreateRuler(style2);
|
|
expect(rulerManager.rulers.length, 2);
|
|
expect(ruler1.hitCount, 3);
|
|
expect(ruler2.hitCount, 1);
|
|
|
|
// Increase hit count for style 2
|
|
rulerManager.findOrCreateRuler(style2);
|
|
rulerManager.findOrCreateRuler(style2);
|
|
rulerManager.findOrCreateRuler(style2);
|
|
expect(rulerManager.rulers.length, 2);
|
|
expect(ruler2.hitCount, 4);
|
|
|
|
// Third ruler cached: it is ok to store more rulers that the cache
|
|
// capacity because the cache is cleaned-up at the next microtask.
|
|
ruler3 = rulerManager.findOrCreateRuler(style3);
|
|
|
|
// Final ruler states
|
|
expect(rulerManager.rulers.length, 3);
|
|
expect(ruler1.hitCount, 3);
|
|
expect(ruler2.hitCount, 4);
|
|
expect(ruler3.hitCount, 1);
|
|
// The least hit ruler isn't disposed yet.
|
|
expect(ruler3.debugIsDisposed, isFalse);
|
|
|
|
// Cleaning up the cache should bring its size down to capacity limit.
|
|
rulerManager.cleanUpRulerCache();
|
|
expect(rulerManager.rulers.length, 2);
|
|
expect(rulerManager.rulers, containsValue(ruler1)); // retained
|
|
expect(rulerManager.rulers, containsValue(ruler2)); // retained
|
|
expect(rulerManager.rulers, isNot(containsValue(ruler3))); // evicted
|
|
expect(ruler1.debugIsDisposed, isFalse);
|
|
expect(ruler2.debugIsDisposed, isFalse);
|
|
expect(ruler3.debugIsDisposed, isTrue);
|
|
|
|
ruler1 = rulerManager.rulers[style1];
|
|
expect(ruler1.style, style1);
|
|
expect(ruler1.hitCount, 0); // hit counts are reset
|
|
|
|
ruler2 = rulerManager.rulers[style2];
|
|
expect(ruler2.style, style2);
|
|
expect(ruler2.hitCount, 0); // hit counts are reset
|
|
});
|
|
});
|
|
|
|
group('$TextMeasurementService', () {
|
|
setUp(() {
|
|
TextMeasurementService.initialize(rulerCacheCapacity: 2);
|
|
});
|
|
tearDown(() {
|
|
TextMeasurementService.clearCache();
|
|
});
|
|
|
|
testMeasurements(
|
|
'preserves whitespace when measuring',
|
|
(TextMeasurementService instance) {
|
|
ui.Paragraph text;
|
|
MeasurementResult result;
|
|
|
|
// leading whitespaces
|
|
text = build(ahemStyle, ' abc');
|
|
result = instance.measure(text, infiniteConstraints);
|
|
expect(result.maxIntrinsicWidth, 60);
|
|
expect(result.minIntrinsicWidth, 30);
|
|
expect(result.height, 10);
|
|
|
|
// trailing whitespaces
|
|
text = build(ahemStyle, 'abc ');
|
|
result = instance.measure(text, infiniteConstraints);
|
|
expect(result.maxIntrinsicWidth, 60);
|
|
expect(result.minIntrinsicWidth, 30);
|
|
expect(result.height, 10);
|
|
|
|
// mixed whitespaces
|
|
text = build(ahemStyle, ' ab c ');
|
|
result = instance.measure(text, infiniteConstraints);
|
|
expect(result.maxIntrinsicWidth, 100);
|
|
expect(result.minIntrinsicWidth, 20);
|
|
expect(result.height, 10);
|
|
|
|
// single whitespace
|
|
text = build(ahemStyle, ' ');
|
|
result = instance.measure(text, infiniteConstraints);
|
|
expect(result.maxIntrinsicWidth, 10);
|
|
expect(result.minIntrinsicWidth, 0);
|
|
expect(result.height, 10);
|
|
|
|
// whitespace only
|
|
text = build(ahemStyle, ' ');
|
|
result = instance.measure(text, infiniteConstraints);
|
|
expect(result.maxIntrinsicWidth, 50);
|
|
expect(result.minIntrinsicWidth, 0);
|
|
expect(result.height, 10);
|
|
},
|
|
);
|
|
|
|
testMeasurements(
|
|
'uses single-line when text can fit without wrapping',
|
|
(TextMeasurementService instance) {
|
|
final MeasurementResult result =
|
|
instance.measure(build(ahemStyle, '12345'), constraints);
|
|
|
|
// Should fit on a single line.
|
|
expect(result.isSingleLine, true);
|
|
expect(result.maxIntrinsicWidth, 50);
|
|
expect(result.minIntrinsicWidth, 50);
|
|
expect(result.width, 50);
|
|
expect(result.height, 10);
|
|
},
|
|
);
|
|
|
|
testMeasurements(
|
|
'uses multi-line for long text',
|
|
(TextMeasurementService instance) {
|
|
final MeasurementResult result =
|
|
instance.measure(build(ahemStyle, '1234567890'), constraints);
|
|
|
|
// The long text doesn't fit in 50px of width, so it needs to wrap.
|
|
expect(result.isSingleLine, false);
|
|
expect(result.maxIntrinsicWidth, 100);
|
|
expect(result.minIntrinsicWidth, 100);
|
|
expect(result.width, 50);
|
|
expect(result.height, 20);
|
|
},
|
|
);
|
|
|
|
testMeasurements(
|
|
'uses multi-line for text that contains new-line',
|
|
(TextMeasurementService instance) {
|
|
final MeasurementResult result =
|
|
instance.measure(build(ahemStyle, '12\n34'), constraints);
|
|
|
|
// Text containing newlines should always be drawn in multi-line mode.
|
|
expect(result.isSingleLine, false);
|
|
expect(result.maxIntrinsicWidth, 20);
|
|
expect(result.minIntrinsicWidth, 20);
|
|
expect(result.width, 50);
|
|
expect(result.height, 20);
|
|
},
|
|
);
|
|
|
|
testMeasurements('empty lines', (TextMeasurementService instance) {
|
|
MeasurementResult result;
|
|
|
|
// Empty lines in the beginning.
|
|
result = instance.measure(build(ahemStyle, '\n\n1234'), constraints);
|
|
expect(result.maxIntrinsicWidth, 40);
|
|
expect(result.minIntrinsicWidth, 40);
|
|
expect(result.height, 30);
|
|
|
|
// Empty lines in the middle.
|
|
result = instance.measure(build(ahemStyle, '12\n\n345'), constraints);
|
|
expect(result.maxIntrinsicWidth, 30);
|
|
expect(result.minIntrinsicWidth, 30);
|
|
expect(result.height, 30);
|
|
|
|
// This can only be done correctly in the canvas-based implementation.
|
|
if (instance is CanvasTextMeasurementService) {
|
|
// Empty lines in the end.
|
|
result = instance.measure(build(ahemStyle, '1234\n\n'), constraints);
|
|
expect(result.maxIntrinsicWidth, 40);
|
|
expect(result.minIntrinsicWidth, 40);
|
|
expect(result.height, 30);
|
|
}
|
|
});
|
|
|
|
test('takes letter spacing into account', () {
|
|
const ui.ParagraphConstraints constraints =
|
|
ui.ParagraphConstraints(width: 100);
|
|
|
|
final ui.ParagraphBuilder normalBuilder = ui.ParagraphBuilder(ahemStyle);
|
|
normalBuilder.addText('abc');
|
|
final ui.Paragraph normalText = normalBuilder.build();
|
|
|
|
final ui.ParagraphBuilder spacedBuilder = ui.ParagraphBuilder(ahemStyle);
|
|
spacedBuilder.pushStyle(ui.TextStyle(letterSpacing: 1.5));
|
|
spacedBuilder.addText('abc');
|
|
final ui.Paragraph spacedText = spacedBuilder.build();
|
|
|
|
// Letter spacing is only supported via DOM measurement.
|
|
final TextMeasurementService instance =
|
|
TextMeasurementService.forParagraph(spacedText);
|
|
expect(instance, isInstanceOf<DomTextMeasurementService>());
|
|
|
|
final MeasurementResult normalResult =
|
|
instance.measure(normalText, constraints);
|
|
final MeasurementResult spacedResult =
|
|
instance.measure(spacedText, constraints);
|
|
|
|
expect(
|
|
normalResult.maxIntrinsicWidth < spacedResult.maxIntrinsicWidth,
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test('takes word spacing into account', () {
|
|
const ui.ParagraphConstraints constraints =
|
|
ui.ParagraphConstraints(width: 100);
|
|
|
|
final ui.ParagraphBuilder normalBuilder = ui.ParagraphBuilder(ahemStyle);
|
|
normalBuilder.addText('a b c');
|
|
final ui.Paragraph normalText = normalBuilder.build();
|
|
|
|
final ui.ParagraphBuilder spacedBuilder = ui.ParagraphBuilder(ahemStyle);
|
|
spacedBuilder.pushStyle(ui.TextStyle(wordSpacing: 1.5));
|
|
spacedBuilder.addText('a b c');
|
|
final ui.Paragraph spacedText = spacedBuilder.build();
|
|
|
|
// Word spacing is only supported via DOM measurement.
|
|
final TextMeasurementService instance =
|
|
TextMeasurementService.forParagraph(spacedText);
|
|
expect(instance, isInstanceOf<DomTextMeasurementService>());
|
|
|
|
final MeasurementResult normalResult =
|
|
instance.measure(normalText, constraints);
|
|
final MeasurementResult spacedResult =
|
|
instance.measure(spacedText, constraints);
|
|
|
|
expect(
|
|
normalResult.maxIntrinsicWidth < spacedResult.maxIntrinsicWidth,
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
testMeasurements('minIntrinsicWidth', (TextMeasurementService instance) {
|
|
MeasurementResult result;
|
|
|
|
// Simple case.
|
|
result = instance.measure(build(ahemStyle, 'abc de fghi'), constraints);
|
|
expect(result.minIntrinsicWidth, 40);
|
|
|
|
// With new lines.
|
|
result = instance.measure(build(ahemStyle, 'abcd\nef\nghi'), constraints);
|
|
expect(result.minIntrinsicWidth, 40);
|
|
|
|
// With trailing whitespace.
|
|
result = instance.measure(build(ahemStyle, 'abcd efg'), constraints);
|
|
expect(result.minIntrinsicWidth, 40);
|
|
|
|
// With trailing whitespace and new lines.
|
|
result = instance.measure(build(ahemStyle, 'abc \ndefg'), constraints);
|
|
expect(result.minIntrinsicWidth, 40);
|
|
|
|
// Very long text.
|
|
result = instance.measure(build(ahemStyle, 'AAAAAAAAAAAA'), constraints);
|
|
expect(result.minIntrinsicWidth, 120);
|
|
});
|
|
|
|
testMeasurements('maxIntrinsicWidth', (TextMeasurementService instance) {
|
|
MeasurementResult result;
|
|
|
|
// Simple case.
|
|
result = instance.measure(build(ahemStyle, 'abc de fghi'), constraints);
|
|
expect(result.maxIntrinsicWidth, 110);
|
|
|
|
// With new lines.
|
|
result = instance.measure(build(ahemStyle, 'abcd\nef\nghi'), constraints);
|
|
expect(result.maxIntrinsicWidth, 40);
|
|
|
|
// With long whitespace.
|
|
result = instance.measure(build(ahemStyle, 'abcd efg'), constraints);
|
|
expect(result.maxIntrinsicWidth, 100);
|
|
|
|
// With trailing whitespace.
|
|
result = instance.measure(build(ahemStyle, 'abc def '), constraints);
|
|
expect(result.maxIntrinsicWidth, 100);
|
|
|
|
// With trailing whitespace and new lines.
|
|
result = instance.measure(build(ahemStyle, 'abc \ndef '), constraints);
|
|
expect(result.maxIntrinsicWidth, 60);
|
|
|
|
// Very long text.
|
|
result = instance.measure(build(ahemStyle, 'AAAAAAAAAAAA'), constraints);
|
|
expect(result.maxIntrinsicWidth, 120);
|
|
});
|
|
|
|
testMeasurements(
|
|
'respects text overflow',
|
|
(TextMeasurementService instance) {
|
|
final ui.ParagraphStyle overflowStyle = ui.ParagraphStyle(
|
|
fontFamily: 'ahem',
|
|
fontSize: 10,
|
|
ellipsis: '...',
|
|
);
|
|
|
|
MeasurementResult result;
|
|
|
|
// The text shouldn't be broken into multiple lines, so the height should
|
|
// be equal to a height of a single line.
|
|
final ui.Paragraph longText = build(
|
|
overflowStyle,
|
|
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
);
|
|
result = instance.measure(longText, constraints);
|
|
expect(result.minIntrinsicWidth, 480);
|
|
expect(result.maxIntrinsicWidth, 480);
|
|
expect(result.height, 10);
|
|
|
|
// The short prefix should make the text break into two lines, but the
|
|
// second line should remain unbroken.
|
|
final ui.Paragraph longTextShortPrefix = build(
|
|
overflowStyle,
|
|
'AAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
);
|
|
result = instance.measure(longTextShortPrefix, constraints);
|
|
expect(result.minIntrinsicWidth, 450);
|
|
expect(result.maxIntrinsicWidth, 450);
|
|
expect(result.height, 20);
|
|
|
|
// The first line is overflowing so we should stop the measurement there
|
|
// and there should be no second line (the short suffix shouldn't be rendered).
|
|
final ui.Paragraph longTextShortSuffix = build(
|
|
overflowStyle,
|
|
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAA',
|
|
);
|
|
result = instance.measure(longTextShortSuffix, constraints);
|
|
expect(result.minIntrinsicWidth, 450);
|
|
expect(result.maxIntrinsicWidth, 450);
|
|
// This can only be done correctly in the canvas-based implementation.
|
|
if (instance is CanvasTextMeasurementService) {
|
|
expect(result.height, 10);
|
|
}
|
|
},
|
|
);
|
|
|
|
testMeasurements('respects max lines', (TextMeasurementService instance) {
|
|
final ui.ParagraphStyle maxlinesStyle = ui.ParagraphStyle(
|
|
fontFamily: 'ahem',
|
|
fontSize: 10,
|
|
maxLines: 2,
|
|
);
|
|
|
|
MeasurementResult result;
|
|
|
|
// The height should be that of a single line.
|
|
final ui.Paragraph oneline = build(maxlinesStyle, 'One line');
|
|
result = instance.measure(oneline, infiniteConstraints);
|
|
expect(result.height, 10);
|
|
|
|
// The height should respect max lines and be limited to two lines here.
|
|
final ui.Paragraph threelines =
|
|
build(maxlinesStyle, 'First\nSecond\nThird');
|
|
result = instance.measure(threelines, infiniteConstraints);
|
|
expect(result.height, 20);
|
|
});
|
|
|
|
test('canvas line breaks', () {
|
|
// TODO(mdebbar): Add tests and make sure to cover the following edge cases:
|
|
// 1. First chunk already overflows the width constraint.
|
|
// 2. maxIntrinsicWidth in the presence of mandatory line breaks.
|
|
// 3. minIntrinsicWidth in the presence of optional line breaks.
|
|
// 4. empty lines in the middle.
|
|
// 5. long text with a short prefix.
|
|
// 6. long text with a short suffix.
|
|
// 7. whitespace at end of line (shouldn't count towards minIntWidth, but counts towards max).
|
|
// 8. long whitespace should still cause line break if not enough space for it.
|
|
});
|
|
});
|
|
}
|