未验证 提交 5ba9ed51 编写于 作者: M Mouad Debbar 提交者: GitHub

[web] Clean up legacy Paragraph implementation (#28050)

上级 c388bfab
......@@ -16,7 +16,6 @@ import 'keyboard_binding.dart';
import 'platform_dispatcher.dart';
import 'pointer_binding.dart';
import 'semantics.dart';
import 'text/measurement.dart';
import 'text_editing/text_editing.dart';
import 'util.dart';
import 'window.dart';
......@@ -29,11 +28,6 @@ class DomRenderer {
reset();
TextMeasurementService.initialize(
rulerCacheCapacity: 10,
root: _glassPaneShadow!.node,
);
assert(() {
_setupHotRestart();
return true;
......
......@@ -15,11 +15,13 @@ import 'paint_service.dart';
import 'paragraph.dart';
import 'word_breaker.dart';
const ui.Color _defaultTextColor = ui.Color(0xFFFF0000);
/// A paragraph made up of a flat list of text spans and placeholders.
///
/// As opposed to [DomParagraph], a [CanvasParagraph] doesn't use a DOM element
/// to represent the structure of its spans and styles. Instead it uses a flat
/// list of [ParagraphSpan] objects.
/// [CanvasParagraph] doesn't use a DOM element to represent the structure of
/// its spans and styles. Instead it uses a flat list of [ParagraphSpan]
/// objects.
class CanvasParagraph implements EngineParagraph {
/// This class is created by the engine, and should not be instantiated
/// or extended directly.
......@@ -574,7 +576,7 @@ class RootStyleNode extends StyleNode {
final EngineParagraphStyle paragraphStyle;
@override
final ui.Color _color = defaultTextColor;
final ui.Color _color = _defaultTextColor;
@override
ui.TextDecoration? get _decoration => null;
......
......@@ -12,7 +12,6 @@ import '../assets.dart';
import '../browser_detection.dart';
import '../util.dart';
import 'layout_service.dart';
import 'measurement.dart';
const String ahemFontFamily = 'Ahem';
const String ahemFontUrl = 'packages/ui/assets/ahem.ttf';
......@@ -215,7 +214,6 @@ class FontManager {
// There might be paragraph measurements for this new font before it is
// loaded. They were measured using fallback font, so we should clear the
// cache.
TextMeasurementService.clearCache();
Spanometer.clearRulersCache();
}, onError: (dynamic exception) {
// Failures here will throw an html.DomException which confusingly
......
......@@ -19,53 +19,15 @@ class WebExperiments {
}
static WebExperiments ensureInitialized() {
return WebExperiments.instance ?? (WebExperiments.instance = WebExperiments._());
return WebExperiments.instance ??
(WebExperiments.instance = WebExperiments._());
}
static WebExperiments? instance;
/// Experiment flag for using canvas-based text measurement.
bool get useCanvasText => _useCanvasText;
set useCanvasText(bool? enabled) {
_useCanvasText = enabled ?? _defaultUseCanvasText;
}
static const bool _defaultUseCanvasText = bool.fromEnvironment(
'FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_TEXT',
defaultValue: true,
);
bool _useCanvasText = _defaultUseCanvasText;
// TODO(mdebbar): Clean up https://github.com/flutter/flutter/issues/71952
/// Experiment flag for using canvas-based measurement for rich text.
bool get useCanvasRichText => _useCanvasRichText;
set useCanvasRichText(bool? enabled) {
_useCanvasRichText = enabled ?? _defaultUseCanvasRichText;
}
static const bool _defaultUseCanvasRichText = bool.fromEnvironment(
'FLUTTER_WEB_USE_EXPERIMENTAL_CANVAS_RICH_TEXT',
defaultValue: true,
);
bool _useCanvasRichText = _defaultUseCanvasRichText;
/// Reset all experimental flags to their default values.
void reset() {
_useCanvasText = _defaultUseCanvasText;
_useCanvasRichText = _defaultUseCanvasRichText;
}
void reset() {}
/// Used to enable/disable experimental flags in the web engine.
void updateExperiment(String name, bool? enabled) {
switch (name) {
case 'useCanvasText':
useCanvasText = enabled;
break;
case 'useCanvasRichText':
useCanvasRichText = enabled;
break;
}
}
void updateExperiment(String name, bool? enabled) {}
}
......@@ -706,12 +706,8 @@ abstract class ParagraphBuilder {
factory ParagraphBuilder(ParagraphStyle style) {
if (engine.useCanvasKit) {
return engine.CkParagraphBuilder(style);
} else if (engine.WebExperiments.instance!.useCanvasRichText) {
return engine.CanvasParagraphBuilder(
style as engine.EngineParagraphStyle);
} else {
return engine.DomParagraphBuilder(style as engine.EngineParagraphStyle);
}
return engine.CanvasParagraphBuilder(style as engine.EngineParagraphStyle);
}
void pushStyle(TextStyle style);
void pop();
......
......@@ -468,7 +468,10 @@ void testMain() {
final bool useOffset = int.tryParse(char) == null;
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(const ui.Rect.fromLTRB(0, 0, 400, 400));
final DomParagraph paragraph = (DomParagraphBuilder(EngineParagraphStyle())..addText(char)).build() as DomParagraph;
final ui.Paragraph paragraph = (ui.ParagraphBuilder(ui.ParagraphStyle())
..pushStyle(ui.TextStyle(decoration: ui.TextDecoration.lineThrough))
..addText(char))
.build();
paragraph.layout(const ui.ParagraphConstraints(width: 1000));
canvas.drawParagraph(paragraph, ui.Offset.zero);
final ui.EngineLayer newLayer = useOffset
......
......@@ -9,9 +9,6 @@ import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/web_experiments.dart';
const bool _defaultUseCanvasText = true;
const bool _defaultUseCanvasRichText = true;
void main() {
internalBootstrapBrowserTest(() => testMain);
}
......@@ -25,70 +22,6 @@ void testMain() {
WebExperiments.instance!.reset();
});
test('default web experiment values', () {
expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText);
expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText);
});
test('can turn on/off web experiments', () {
WebExperiments.instance!.updateExperiment('useCanvasText', true);
WebExperiments.instance!.updateExperiment('useCanvasRichText', true);
expect(WebExperiments.instance!.useCanvasText, isTrue);
expect(WebExperiments.instance!.useCanvasRichText, isTrue);
WebExperiments.instance!.updateExperiment('useCanvasText', false);
WebExperiments.instance!.updateExperiment('useCanvasRichText', false);
expect(WebExperiments.instance!.useCanvasText, isFalse);
expect(WebExperiments.instance!.useCanvasRichText, isFalse);
WebExperiments.instance!.updateExperiment('useCanvasText', null);
WebExperiments.instance!.updateExperiment('useCanvasRichText', null);
// Goes back to default value.
expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText);
expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText);
});
test('ignores unknown experiments', () {
expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText);
expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText);
WebExperiments.instance!.updateExperiment('foobarbazqux', true);
expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText);
expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText);
WebExperiments.instance!.updateExperiment('foobarbazqux', false);
expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText);
expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText);
});
test('can reset web experiments', () {
WebExperiments.instance!.updateExperiment('useCanvasText', false);
WebExperiments.instance!.updateExperiment('useCanvasRichText', false);
WebExperiments.instance!.reset();
expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText);
expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText);
WebExperiments.instance!.updateExperiment('useCanvasText', false);
WebExperiments.instance!.updateExperiment('useCanvasRichText', false);
WebExperiments.instance!.updateExperiment('foobarbazqux', true);
WebExperiments.instance!.reset();
expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText);
expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText);
});
test('js interop also works', () {
expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText);
expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText);
expect(() => jsUpdateExperiment('useCanvasText', true), returnsNormally);
expect(() => jsUpdateExperiment('useCanvasRichText', true), returnsNormally);
expect(WebExperiments.instance!.useCanvasText, isTrue);
expect(WebExperiments.instance!.useCanvasRichText, isTrue);
expect(() => jsUpdateExperiment('useCanvasText', null), returnsNormally);
expect(() => jsUpdateExperiment('useCanvasRichText', null), returnsNormally);
expect(WebExperiments.instance!.useCanvasText, _defaultUseCanvasText);
expect(WebExperiments.instance!.useCanvasRichText, _defaultUseCanvasRichText);
});
test('js interop throws on wrong type', () {
expect(() => jsUpdateExperiment(123, true), throwsA(anything));
expect(() => jsUpdateExperiment('foo', 123), throwsA(anything));
......
......@@ -818,10 +818,12 @@ void _testCullRectComputation() {
'renders clipped text with high quality',
() async {
// To reproduce blurriness we need real clipping.
final DomParagraph paragraph =
(DomParagraphBuilder(EngineParagraphStyle(fontFamily: 'Roboto'))
final CanvasParagraph paragraph =
(ui.ParagraphBuilder(ui.ParagraphStyle(fontFamily: 'Roboto'))
// Use a decoration to force rendering in DOM mode.
..pushStyle(ui.TextStyle(decoration: ui.TextDecoration.lineThrough, decorationColor: const ui.Color(0x00000000)))
..addText('Am I blurry?'))
.build() as DomParagraph;
.build() as CanvasParagraph;
paragraph.layout(const ui.ParagraphConstraints(width: 1000));
final ui.Rect canvasSize = ui.Rect.fromLTRB(
......@@ -843,7 +845,7 @@ void _testCullRectComputation() {
final RecordingCanvas canvas = recorder.beginRecording(outerClip);
canvas.drawParagraph(paragraph, const ui.Offset(8.5, 8.5));
final ui.Picture picture = recorder.endRecording();
expect(canvas.renderStrategy.hasArbitraryPaint, isFalse);
expect(paragraph.drawOnCanvas, isFalse);
builder.addPicture(
ui.Offset.zero,
......@@ -857,7 +859,7 @@ void _testCullRectComputation() {
final RecordingCanvas canvas = recorder.beginRecording(innerClip);
canvas.drawParagraph(paragraph, ui.Offset(8.5, 8.5 + innerClip.top));
final ui.Picture picture = recorder.endRecording();
expect(canvas.renderStrategy.hasArbitraryPaint, isFalse);
expect(paragraph.drawOnCanvas, isFalse);
builder.addPicture(
ui.Offset.zero,
......
......@@ -13,10 +13,6 @@ import 'text_scuba.dart';
typedef CanvasTest = FutureOr<void> Function(EngineCanvas canvas);
const String threeLines = 'First\nSecond\nThird';
const String veryLongWithShortPrefix =
'Lorem ipsum dolor\nsit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
const String veryLongWithShortSuffix =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et\ndolore magna aliqua.';
const String veryLong =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
const String longUnbreakable = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
......@@ -30,18 +26,8 @@ Future<void> testMain() async {
viewportSize: const Size(800, 800),
);
final TextStyle warningStyle = TextStyle(
color: const Color(0xFFFF0000),
fontFamily: 'Roboto',
fontSize: 10,
);
setUpStableTestFonts();
EngineParagraph warning(String text) {
return paragraph(text, textStyle: warningStyle);
}
testEachCanvas('maxLines clipping', (EngineCanvas canvas) {
Offset offset = Offset.zero;
EngineParagraph p;
......@@ -69,93 +55,4 @@ Future<void> testMain() async {
return scuba.diffCanvasScreenshot(canvas, 'text_max_lines');
});
testEachCanvas('maxLines with overflow', (EngineCanvas canvas) {
Offset offset = Offset.zero;
EngineParagraph p;
// Only the first line is rendered with no ellipsis because the first line
// doesn't overflow.
p = paragraph(
threeLines,
paragraphStyle: ParagraphStyle(ellipsis: '...'),
);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// The first two lines are rendered with an ellipsis on the 2nd line.
p = paragraph(
veryLongWithShortPrefix,
paragraphStyle: ParagraphStyle(ellipsis: '...'),
maxWidth: 200,
);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// Only the first line is rendered with an ellipsis.
if (!WebExperiments.instance!.useCanvasText) {
// This is now correct with the canvas-based measurement, so we shouldn't
// print the "(wrong)" warning.
p = warning('(wrong)');
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height);
}
p = paragraph(
veryLongWithShortSuffix,
paragraphStyle: ParagraphStyle(ellipsis: '...'),
maxWidth: 200,
);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// Only the first two lines are rendered and the ellipsis appears on the 2nd
// line.
if (!WebExperiments.instance!.useCanvasText) {
// This is now correct with the canvas-based measurement, so we shouldn't
// print the "(wrong)" warning.
p = warning('(wrong)');
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height);
}
p = paragraph(
veryLong,
paragraphStyle: ParagraphStyle(maxLines: 2, ellipsis: '...'),
maxWidth: 200,
);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
return scuba.diffCanvasScreenshot(canvas, 'text_max_lines_with_ellipsis');
});
testEachCanvas('long unbreakable text', (EngineCanvas canvas) {
Offset offset = Offset.zero;
EngineParagraph p;
// The whole line is rendered unbroken when there are no constraints.
p = paragraph(longUnbreakable);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// The whole line is rendered with an ellipsis.
p = paragraph(
longUnbreakable,
paragraphStyle: ParagraphStyle(ellipsis: '...'),
maxWidth: 200,
);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// The text is broken into multiple lines.
p = paragraph(longUnbreakable, maxWidth: 200);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
// Very narrow constraint (less than one character's width).
p = paragraph('AA', maxWidth: 7);
canvas.drawParagraph(p, offset);
offset = offset.translate(0, p.height + 10);
return scuba.diffCanvasScreenshot(canvas, 'text_long_unbreakable');
});
}
......@@ -71,7 +71,7 @@ class EngineScubaTester {
sceneElement.append(canvas.rootElement);
html.document.body!.append(sceneElement);
String screenshotName = '${fileName}_${canvas.runtimeType}';
if (WebExperiments.instance!.useCanvasText) {
if (canvas is BitmapCanvas) {
screenshotName += '+canvas_measurement';
}
await diffScreenshot(
......@@ -94,41 +94,11 @@ typedef CanvasTest = FutureOr<void> Function(EngineCanvas canvas);
void testEachCanvas(String description, CanvasTest body,
{double? maxDiffRate}) {
const ui.Rect bounds = ui.Rect.fromLTWH(0, 0, 600, 800);
test('$description (bitmap)', () {
try {
TextMeasurementService.initialize(rulerCacheCapacity: 2);
WebExperiments.instance!.useCanvasText = false;
WebExperiments.instance!.useCanvasRichText = false;
return body(BitmapCanvas(bounds, RenderStrategy()));
} finally {
WebExperiments.instance!.useCanvasText = null;
WebExperiments.instance!.useCanvasRichText = null;
TextMeasurementService.clearCache();
}
});
test('$description (bitmap + canvas measurement)', () async {
try {
TextMeasurementService.initialize(rulerCacheCapacity: 2);
WebExperiments.instance!.useCanvasText = true;
WebExperiments.instance!.useCanvasRichText = false;
await body(BitmapCanvas(bounds, RenderStrategy()));
} finally {
WebExperiments.instance!.useCanvasText = null;
WebExperiments.instance!.useCanvasRichText = null;
TextMeasurementService.clearCache();
}
return body(BitmapCanvas(bounds, RenderStrategy()));
});
test('$description (dom)', () {
try {
TextMeasurementService.initialize(rulerCacheCapacity: 2);
WebExperiments.instance!.useCanvasText = false;
WebExperiments.instance!.useCanvasRichText = false;
return body(DomCanvas(domRenderer.createElement('flt-picture')));
} finally {
WebExperiments.instance!.useCanvasText = null;
WebExperiments.instance!.useCanvasRichText = null;
TextMeasurementService.clearCache();
}
return body(DomCanvas(domRenderer.createElement('flt-picture')));
});
}
......
// 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 'package:test/bootstrap/browser.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'text_scuba.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
final EngineScubaTester scuba = await EngineScubaTester.initialize(
viewportSize: const Size(800, 800),
);
setUpStableTestFonts();
void drawLetterAndWordSpacing(EngineCanvas canvas) {
Offset offset = Offset.zero;
for (double spacing = 0; spacing < 15; spacing += 5) {
canvas.drawParagraph(
paragraph('HelloWorld',
textStyle: TextStyle(
color: const Color(0xFF000000),
decoration: TextDecoration.none,
fontFamily: 'Roboto',
fontSize: 30,
letterSpacing: spacing)),
offset,
);
offset = offset.translate(0, 40);
}
for (double spacing = 0; spacing < 30; spacing += 10) {
final TextStyle textStyle = TextStyle(
color: const Color(0xFF00FF00),
decoration: TextDecoration.none,
fontFamily: 'Roboto',
fontSize: 30,
wordSpacing: spacing);
canvas.drawParagraph(
paragraph('Hello World', textStyle: textStyle, maxWidth: 600),
offset,
);
offset = offset.translate(0, 40);
}
}
testEachCanvas('draws text with letter/word spacing', (EngineCanvas canvas) {
drawLetterAndWordSpacing(canvas);
return scuba.diffCanvasScreenshot(
canvas, 'paint_bounds_for_text_style_letter_spacing');
});
void drawTextDecorationStyle(EngineCanvas canvas) {
final List<TextDecorationStyle> decorationStyles = <TextDecorationStyle>[
TextDecorationStyle.solid,
TextDecorationStyle.double,
TextDecorationStyle.dotted,
TextDecorationStyle.dashed,
TextDecorationStyle.wavy,
];
Offset offset = Offset.zero;
for (final TextDecorationStyle decorationStyle in decorationStyles) {
final TextStyle textStyle = TextStyle(
color: const Color.fromRGBO(50, 50, 255, 1.0),
decoration: TextDecoration.underline,
decorationStyle: decorationStyle,
decorationColor: const Color.fromRGBO(50, 50, 50, 1.0),
fontFamily: 'Roboto',
fontSize: 30,
);
canvas.drawParagraph(
paragraph('Hello World', textStyle: textStyle, maxWidth: 600),
offset,
);
offset = offset.translate(0, 40);
}
}
testEachCanvas('draws text decoration style', (EngineCanvas canvas) {
drawTextDecorationStyle(canvas);
return scuba.diffCanvasScreenshot(
canvas, 'paint_bounds_for_text_decorationStyle');
});
void drawTextDecoration(EngineCanvas canvas) {
final List<TextDecoration> decorations = <TextDecoration>[
TextDecoration.overline,
TextDecoration.underline,
TextDecoration.combine(<TextDecoration>[
TextDecoration.underline,
TextDecoration.lineThrough
]),
TextDecoration.combine(
<TextDecoration>[TextDecoration.underline, TextDecoration.overline]),
TextDecoration.combine(
<TextDecoration>[TextDecoration.overline, TextDecoration.lineThrough])
];
Offset offset = Offset.zero;
for (final TextDecoration decoration in decorations) {
final TextStyle textStyle = TextStyle(
color: const Color.fromRGBO(50, 50, 255, 1.0),
decoration: decoration,
decorationStyle: TextDecorationStyle.solid,
decorationColor: const Color.fromRGBO(255, 160, 0, 1.0),
fontFamily: 'Roboto',
fontSize: 20,
);
canvas.drawParagraph(
paragraph(
'Hello World $decoration',
textStyle: textStyle,
maxWidth: 600,
),
offset,
);
offset = offset.translate(0, 40);
}
}
testEachCanvas('draws text decoration', (EngineCanvas canvas) {
drawTextDecoration(canvas);
return scuba.diffCanvasScreenshot(
canvas, 'paint_bounds_for_text_decoration');
});
void drawTextWithBackground(EngineCanvas canvas) {
// Single-line text.
canvas.drawParagraph(
paragraph(
'Hello World',
maxWidth: 600,
textStyle: TextStyle(
color: const Color.fromRGBO(0, 0, 0, 1.0),
background: Paint()..color = const Color.fromRGBO(255, 50, 50, 1.0),
fontFamily: 'Roboto',
fontSize: 30,
),
),
Offset.zero,
);
// Multi-line text.
canvas.drawParagraph(
paragraph(
'Multi line Hello World paragraph',
maxWidth: 200,
textStyle: TextStyle(
color: const Color.fromRGBO(0, 0, 0, 1.0),
background: Paint()..color = const Color.fromRGBO(50, 50, 255, 1.0),
fontFamily: 'Roboto',
fontSize: 30,
),
),
const Offset(0, 40),
);
}
void drawTextWithShadow(EngineCanvas canvas) {
// Single-line text.
canvas.drawParagraph(
paragraph(
'Hello World',
maxWidth: 600,
textStyle: TextStyle(
color: const Color.fromRGBO(0, 0, 0, 1.0),
background: Paint()..color = const Color.fromRGBO(255, 50, 50, 1.0),
fontFamily: 'Roboto',
fontSize: 30,
shadows: <Shadow>[
const Shadow(
blurRadius: 0,
color: Color.fromRGBO(255, 0, 255, 1.0),
offset: Offset(10, 5),
),
],
),
),
Offset.zero,
);
// Multi-line text.
canvas.drawParagraph(
paragraph(
'Multi line Hello World paragraph',
maxWidth: 200,
textStyle: TextStyle(
color: const Color.fromRGBO(0, 0, 0, 1.0),
background: Paint()..color = const Color.fromRGBO(50, 50, 255, 1.0),
fontFamily: 'Roboto',
fontSize: 30,
shadows: <Shadow>[
const Shadow(
blurRadius: 0,
color: Color.fromRGBO(255, 0, 255, 1.0),
offset: Offset(10, 5),
),
const Shadow(
blurRadius: 0,
color: Color.fromRGBO(0, 255, 255, 1.0),
offset: Offset(-10, -5),
),
],
),
),
const Offset(0, 40),
);
}
testEachCanvas('draws text with a background', (EngineCanvas canvas) {
drawTextWithBackground(canvas);
return scuba.diffCanvasScreenshot(canvas, 'text_background');
});
testEachCanvas('draws text with a shadow', (EngineCanvas canvas) {
drawTextWithShadow(canvas);
return scuba.diffCanvasScreenshot(canvas, 'text_shadow', maxDiffRatePercent: 0.2);
});
testEachCanvas('Handles disabled strut style', (EngineCanvas canvas) {
// Flutter uses [StrutStyle.disabled] for the [SelectableText] widget. This
// translates into a strut style with a [height] of 0, which wasn't being
// handled correctly by the web engine.
final StrutStyle disabled = StrutStyle(height: 0, leading: 0);
canvas.drawParagraph(
paragraph(
'Hello\nWorld',
paragraphStyle: ParagraphStyle(strutStyle: disabled),
),
Offset.zero,
);
return scuba.diffCanvasScreenshot(
canvas,
'text_strut_style_disabled',
region: const Rect.fromLTRB(0, 0, 100, 100),
maxDiffRatePercent: 0.0,
);
});
}
此差异已折叠。
......@@ -7,6 +7,8 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import '../html/paragraph/helper.dart';
const ui.Color white = ui.Color(0xFFFFFFFF);
const ui.Color black = ui.Color(0xFF000000);
const ui.Color red = ui.Color(0xFFFF0000);
......@@ -22,15 +24,6 @@ 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);
}
......@@ -686,6 +679,54 @@ Future<void> testMain() async {
}
});
});
test('$CanvasParagraph.getWordBoundary', () {
final ui.Paragraph paragraph = plain(ahemStyle, 'Lorem ipsum dolor');
const ui.TextRange loremRange = ui.TextRange(start: 0, end: 5);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 0)), loremRange);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 1)), loremRange);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 2)), loremRange);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 3)), loremRange);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 4)), loremRange);
const ui.TextRange firstSpace = ui.TextRange(start: 5, end: 6);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 5)), firstSpace);
const ui.TextRange ipsumRange = ui.TextRange(start: 6, end: 11);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 6)), ipsumRange);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 7)), ipsumRange);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 8)), ipsumRange);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 9)), ipsumRange);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 10)), ipsumRange);
const ui.TextRange secondSpace = ui.TextRange(start: 11, end: 12);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 11)), secondSpace);
const ui.TextRange dolorRange = ui.TextRange(start: 12, end: 17);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 12)), dolorRange);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 13)), dolorRange);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 14)), dolorRange);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 15)), dolorRange);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 16)), dolorRange);
const ui.TextRange endRange = ui.TextRange(start: 17, end: 17);
expect(paragraph.getWordBoundary(const ui.TextPosition(offset: 17)), endRange);
});
test('$CanvasParagraph.longestLine', () {
final ui.Paragraph paragraph = plain(ahemStyle, 'abcd\nabcde abc');
paragraph.layout(const ui.ParagraphConstraints(width: 80.0));
expect(paragraph.longestLine, 50.0);
});
test('$CanvasParagraph.width should be a whole integer', () {
final ui.Paragraph paragraph = plain(ahemStyle, 'abc');
paragraph.layout(const ui.ParagraphConstraints(width: 30.8));
expect(paragraph.width, 30);
expect(paragraph.height, 10);
});
}
/// Shortcut to create a [ui.TextBox] with an optional [ui.TextDirection].
......
......@@ -56,12 +56,6 @@ Future<void> testMain() async {
const ui.ParagraphConstraints constraints =
ui.ParagraphConstraints(width: 30.0);
final DomParagraphBuilder domBuilder = DomParagraphBuilder(style);
domBuilder.addText('test');
// Triggers the measuring and verifies the result has been cached.
domBuilder.build().layout(constraints);
expect(TextMeasurementService.rulerManager!.rulers.length, 1);
final CanvasParagraphBuilder canvasBuilder = CanvasParagraphBuilder(style);
canvasBuilder.addText('test');
// Triggers the measuring and verifies the ruler cache has been populated.
......@@ -78,7 +72,6 @@ Future<void> testMain() async {
// Verifies the font is loaded, and the cache is cleaned.
expect(_containsFontFamily('Blehm'), isTrue);
expect(TextMeasurementService.rulerManager!.rulers.length, 0);
expect(Spanometer.rulers.length, 0);
},
// TODO(hterkelsen): https://github.com/flutter/flutter/issues/56702
......
此差异已折叠。
......@@ -10,6 +10,7 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'html/paragraph/helper.dart';
import 'matchers.dart';
void main() {
......@@ -88,16 +89,13 @@ Future<void> testMain() async {
});
test('lay out unattached paragraph', () {
final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle(
final CanvasParagraph paragraph = plain(EngineParagraphStyle(
fontFamily: 'sans-serif',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
fontSize: 14.0,
));
builder.addText('How do you do this fine morning?');
final DomParagraph paragraph = builder.build() as DomParagraph;
), 'How do you do this fine morning?');
expect(paragraph.paragraphElement.parent, isNull);
expect(paragraph.height, 0.0);
expect(paragraph.width, -1.0);
expect(paragraph.minIntrinsicWidth, 0.0);
......@@ -107,7 +105,6 @@ Future<void> testMain() async {
paragraph.layout(const ParagraphConstraints(width: 60.0));
expect(paragraph.paragraphElement.parent, isNull);
expect(paragraph.height, greaterThan(0.0));
expect(paragraph.width, greaterThan(0.0));
expect(paragraph.minIntrinsicWidth, greaterThan(0.0));
......@@ -154,18 +151,18 @@ Future<void> testMain() async {
});
test('$ParagraphBuilder detects plain text', () {
DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle(
ParagraphBuilder builder = ParagraphBuilder(EngineParagraphStyle(
fontFamily: 'sans-serif',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
fontSize: 15.0,
));
builder.addText('hi');
DomParagraph paragraph = builder.build() as DomParagraph;
CanvasParagraph paragraph = builder.build() as CanvasParagraph;
expect(paragraph.plainText, isNotNull);
expect(paragraph.geometricStyle.fontWeight, FontWeight.normal);
expect(paragraph.paragraphStyle.fontWeight, FontWeight.normal);
builder = DomParagraphBuilder(EngineParagraphStyle(
builder = ParagraphBuilder(EngineParagraphStyle(
fontFamily: 'sans-serif',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
......@@ -173,13 +170,12 @@ Future<void> testMain() async {
));
builder.pushStyle(TextStyle(fontWeight: FontWeight.bold));
builder.addText('hi');
paragraph = builder.build() as DomParagraph;
paragraph = builder.build() as CanvasParagraph;
expect(paragraph.plainText, isNotNull);
expect(paragraph.geometricStyle.fontWeight, FontWeight.bold);
});
test('$ParagraphBuilder detects rich text', () {
final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle(
final ParagraphBuilder builder = ParagraphBuilder(EngineParagraphStyle(
fontFamily: 'sans-serif',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
......@@ -188,176 +184,27 @@ Future<void> testMain() async {
builder.addText('h');
builder.pushStyle(TextStyle(fontWeight: FontWeight.bold));
builder.addText('i');
final DomParagraph paragraph = builder.build() as DomParagraph;
expect(paragraph.plainText, isNull);
expect(paragraph.geometricStyle.fontWeight, FontWeight.normal);
final CanvasParagraph paragraph = builder.build() as CanvasParagraph;
expect(paragraph.plainText, 'hi');
});
test('$ParagraphBuilder treats empty text as plain', () {
final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle(
final ParagraphBuilder builder = ParagraphBuilder(EngineParagraphStyle(
fontFamily: 'sans-serif',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
fontSize: 15.0,
));
builder.pushStyle(TextStyle(fontWeight: FontWeight.bold));
final DomParagraph paragraph = builder.build() as DomParagraph;
final CanvasParagraph paragraph = builder.build() as CanvasParagraph;
expect(paragraph.plainText, '');
expect(paragraph.geometricStyle.fontWeight, FontWeight.bold);
});
// Regression test for https://github.com/flutter/flutter/issues/34931.
test('hit test on styled text returns correct span offset', () {
final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle(
fontFamily: 'sans-serif',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
fontSize: 15.0,
));
builder.pushStyle(TextStyle(fontWeight: FontWeight.bold));
const String firstSpanText = 'XYZ';
builder.addText(firstSpanText);
builder.pushStyle(TextStyle(fontWeight: FontWeight.normal));
const String secondSpanText = '1234';
builder.addText(secondSpanText);
builder.pushStyle(TextStyle(fontStyle: FontStyle.italic));
builder.addText('followed by a link');
final DomParagraph paragraph = builder.build() as DomParagraph;
paragraph.layout(const ParagraphConstraints(width: 800.0));
expect(paragraph.plainText, isNull);
const int secondSpanStartPosition = firstSpanText.length;
const int thirdSpanStartPosition =
firstSpanText.length + secondSpanText.length;
expect(paragraph.getPositionForOffset(const Offset(50, 0)).offset,
secondSpanStartPosition);
expect(paragraph.getPositionForOffset(const Offset(150, 0)).offset,
thirdSpanStartPosition);
});
test('hit test on the nested text span and returns correct span offset', () {
const String fontFamily = 'sans-serif';
const double fontSize = 20.0;
final TextStyle style = TextStyle(fontFamily: fontFamily, fontSize: fontSize);
final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle(
fontFamily: fontFamily,
fontSize: fontSize,
));
const String text00 = 'test test test test test te00 ';
const String text010 = 'test010 ';
const String text02 = 'test test test test te02 ';
const String text030 = 'test030 ';
const String text04 = 'test test test test test test test test test test te04 ';
const String text050 = 'test050 ';
/* Logical arrangement: Tree
Root TextSpan: 0
*/
builder.pushStyle(style);
{
// 1st child TextSpan of Root: 0.0
builder.pushStyle(style);
builder.addText(text00);
builder.pop();
// 2nd child TextSpan of Root: 0.1
builder.pushStyle(style);
{
// 1st child TextSpan of 0.1: 0.1.0
builder.pushStyle(style);
builder.addText(text010);
builder.pop();
}
builder.pop();
// 3rd child TextSpan of Root: 0.2
builder.pushStyle(style);
builder.addText(text02);
builder.pop();
// 4th child TextSpan of Root: 0.3
builder.pushStyle(style);
{
// 1st child TextSpan of 0.3: 0.3.0
builder.pushStyle(style);
builder.addText(text030);
builder.pop();
}
builder.pop();
// 5th child TextSpan of Root: 0.4
builder.pushStyle(style);
builder.addText(text04);
builder.pop();
// 6th child TextSpan of Root: 0.5
builder.pushStyle(style);
{
// 1st child TextSpan of 0.5: 0.5.0
builder.pushStyle(style);
builder.addText(text050);
builder.pop();
}
builder.pop();
}
builder.pop();
/* Display arrangement: Visible texts
Because `const fontSize = 20.0`, the width of each character is 20 and the
height is 20. `Display arrangement` squashes `Logical arrangement` to the
(x, y) plane. That means `Display arrangement` only shows the visible texts.
The order of texts is text00 --> text010 --> text02 --> text030 --> text04
--> text050.
The output is like that.
|------------ 600 ------------| Begin of test010
|--------------- 760 ----------------| End of test010
|---------- 500 ---------| Begin of test030
|------------- 660 -------------| End of test030
|-- 180 --| Begin of test050
|------ 360 -----| End of test050
'test test test test test te00 test010 '
'test test test test te02 test030 test '
'test test test test test test test test '
'test te04 test050 '
*/
final DomParagraph paragraph = builder.build() as DomParagraph;
paragraph.layout(const ParagraphConstraints(width: 800));
// Reference the offsets with the output of `Display arrangement`.
const int offset010 = text00.length;
const int offset030 = offset010 + text010.length + text02.length;
const int offset04 = offset030 + text030.length;
const int offset050 = offset04 + text04.length;
// Tap text010.
expect(paragraph.getPositionForOffset(const Offset(700, 10)).offset, offset010);
// Tap text030
expect(paragraph.getPositionForOffset(const Offset(600, 30)).offset, offset030);
// Tap text050
expect(paragraph.getPositionForOffset(const Offset(220, 70)).offset, offset050);
// Tap the left neighbor of text050
expect(paragraph.getPositionForOffset(const Offset(199, 70)).offset, offset04);
// Tap the right neighbor of text050. No matter who the right neighbor of
// text0505 is, it must not be text050 itself.
expect(paragraph.getPositionForOffset(const Offset(360, 70)).offset,
isNot(offset050));
// Tap the neighbor above text050
expect(paragraph.getPositionForOffset(const Offset(220, 59)).offset, offset04);
// Tap the neighbor below text050. No matter who the neighbor above text050,
// it must not be text050 itself.
expect(paragraph.getPositionForOffset(const Offset(220, 80)).offset,
isNot(offset050));
});
// Regression test for https://github.com/flutter/flutter/issues/38972
test(
'should not set fontFamily to effectiveFontFamily for spans in rich text',
() {
final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle(
final ParagraphBuilder builder = ParagraphBuilder(EngineParagraphStyle(
fontFamily: 'Roboto',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
......@@ -370,11 +217,11 @@ Future<void> testMain() async {
builder.pushStyle(TextStyle(fontSize: 30.0, fontWeight: FontWeight.normal));
const String secondSpanText = 'def';
builder.addText(secondSpanText);
final DomParagraph paragraph = builder.build() as DomParagraph;
final CanvasParagraph paragraph = builder.build() as CanvasParagraph;
paragraph.layout(const ParagraphConstraints(width: 800.0));
expect(paragraph.plainText, isNull);
expect(paragraph.plainText, 'abcdef');
final List<SpanElement> spans =
paragraph.paragraphElement.querySelectorAll('span');
paragraph.toDomElement().querySelectorAll('span');
expect(spans[0].style.fontFamily, 'Ahem, $fallback, sans-serif');
// The nested span here should not set it's family to default sans-serif.
expect(spans[1].style.fontFamily, 'Ahem, $fallback, sans-serif');
......@@ -388,15 +235,13 @@ Future<void> testMain() async {
// Set this to false so it doesn't default to 'Ahem' font.
debugEmulateFlutterTesterEnvironment = false;
final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle(
final CanvasParagraph paragraph = plain(EngineParagraphStyle(
fontFamily: 'SomeFont',
fontSize: 12.0,
));
builder.addText('Hello');
), 'Hello');
final DomParagraph paragraph = builder.build() as DomParagraph;
expect(paragraph.paragraphElement.style.fontFamily,
paragraph.layout(constrain(double.infinity));
expect(paragraph.toDomElement().style.fontFamily,
'SomeFont, $fallback, sans-serif');
debugEmulateFlutterTesterEnvironment = true;
......@@ -410,15 +255,13 @@ Future<void> testMain() async {
// Set this to false so it doesn't default to 'Ahem' font.
debugEmulateFlutterTesterEnvironment = false;
final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle(
final CanvasParagraph paragraph = plain(EngineParagraphStyle(
fontFamily: 'serif',
fontSize: 12.0,
));
), 'Hello');
builder.addText('Hello');
final DomParagraph paragraph = builder.build() as DomParagraph;
expect(paragraph.paragraphElement.style.fontFamily, 'serif');
paragraph.layout(constrain(double.infinity));
expect(paragraph.toDomElement().style.fontFamily, 'serif');
debugEmulateFlutterTesterEnvironment = true;
});
......@@ -427,15 +270,13 @@ Future<void> testMain() async {
// Set this to false so it doesn't default to 'Ahem' font.
debugEmulateFlutterTesterEnvironment = false;
final DomParagraphBuilder builder = DomParagraphBuilder(EngineParagraphStyle(
final CanvasParagraph paragraph = plain(EngineParagraphStyle(
fontFamily: 'MyFont 2000',
fontSize: 12.0,
));
builder.addText('Hello');
), 'Hello');
final DomParagraph paragraph = builder.build() as DomParagraph;
expect(paragraph.paragraphElement.style.fontFamily,
paragraph.layout(constrain(double.infinity));
expect(paragraph.toDomElement().style.fontFamily,
'"MyFont 2000", $fallback, sans-serif');
debugEmulateFlutterTesterEnvironment = true;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册