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

[web] Rich text painting on bitmap canvas (#23136)

上级 a8c360dc
......@@ -1184,6 +1184,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/layout_service.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/line_break_properties.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/line_breaker.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/measurement.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/paint_service.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/paragraph.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/ruler.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/unicode_range.dart
......
repository: https://github.com/flutter/goldens.git
revision: c808c28c81b6c3143ae969e8c49bed4a6d49aabb
revision: 4946ab2de031c14d30502efcaf51220e0be4d1f1
......@@ -129,6 +129,7 @@ part 'engine/text/layout_service.dart';
part 'engine/text/line_break_properties.dart';
part 'engine/text/line_breaker.dart';
part 'engine/text/measurement.dart';
part 'engine/text/paint_service.dart';
part 'engine/text/paragraph.dart';
part 'engine/text/canvas_paragraph.dart';
part 'engine/text/ruler.dart';
......
......@@ -58,9 +58,13 @@ class CanvasParagraph implements EngineParagraph {
@override
bool get didExceedMaxLines => _layoutService.didExceedMaxLines;
@override
bool isLaidOut = false;
ui.ParagraphConstraints? _lastUsedConstraints;
late final TextLayoutService _layoutService = TextLayoutService(this);
late final TextPaintService _paintService = TextPaintService(this);
@override
void layout(ui.ParagraphConstraints constraints) {
......@@ -90,6 +94,7 @@ class CanvasParagraph implements EngineParagraph {
.benchmark('text_layout', stopwatch.elapsedMicroseconds.toDouble());
}
isLaidOut = true;
_lastUsedConstraints = constraints;
}
......@@ -100,10 +105,7 @@ class CanvasParagraph implements EngineParagraph {
@override
void paint(BitmapCanvas canvas, ui.Offset offset) {
// TODO(mdebbar): Loop through the spans and for each box in the span:
// 1. Paint the background rect.
// 2. Paint the text shadows?
// 3. Paint the text.
_paintService.paint(canvas, offset);
}
@override
......@@ -182,9 +184,6 @@ class CanvasParagraph implements EngineParagraph {
@override
final bool drawOnCanvas = true;
@override
bool isLaidOut = false;
@override
List<ui.TextBox> getBoxesForRange(
int start,
......@@ -217,7 +216,7 @@ class CanvasParagraph implements EngineParagraph {
}
@override
List<ui.LineMetrics> computeLineMetrics() {
List<EngineLineMetrics> computeLineMetrics() {
return _layoutService.lines;
}
}
......
......@@ -370,6 +370,14 @@ class RangeBox {
return startIndex < this.end.index && this.start.index < endIndex;
}
/// Returns a [ui.TextBox] representing this range box in the given [line].
///
/// The coordinates of the resulting [ui.TextBox] are relative to the
/// paragraph, not to the line.
ui.TextBox toTextBox(EngineLineMetrics line) {
return intersect(line, start.index, end.index);
}
/// Performs the intersection of this box with the range given by [start] and
/// [end] indices, and returns a [ui.TextBox] representing that intersection.
///
......@@ -772,6 +780,12 @@ class LineBuilder {
);
extendTo(
LineBreakResult.sameIndex(breakingPoint, LineBreakType.prohibited));
// There's a possibility that the end of line has moved backwards, so we
// need to remove some boxes in that case.
while (_boxes.length > 0 && _boxes.last.end.index > breakingPoint) {
_boxes.removeLast();
}
}
LineBreakResult get _boxStart {
......
// 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
part of engine;
/// Responsible for painting a [CanvasParagraph] on a [BitmapCanvas].
class TextPaintService {
TextPaintService(this.paragraph);
final CanvasParagraph paragraph;
void paint(BitmapCanvas canvas, ui.Offset offset) {
// Loop through all the lines, for each line, loop through all the boxes and
// paint them. The boxes have enough information so they can be painted
// individually.
final List<EngineLineMetrics> lines = paragraph.computeLineMetrics();
for (final EngineLineMetrics line in lines) {
for (final RangeBox box in line.boxes!) {
_paintBox(canvas, offset, line, box);
}
}
}
void _paintBox(
BitmapCanvas canvas,
ui.Offset offset,
EngineLineMetrics line,
RangeBox box,
) {
final ParagraphSpan span = box.span;
// Placeholder spans don't need any painting. Their boxes should remain
// empty so that their underlying widgets do their own painting.
if (span is FlatTextSpan) {
// Paint the background of the box, if the span has a background.
final SurfacePaint? background = span.style._background as SurfacePaint?;
if (background != null) {
canvas.drawRect(
box.toTextBox(line).toRect().shift(offset),
background.paintData,
);
}
// Paint the actual text.
_applySpanStyleToCanvas(span, canvas);
final double x = offset.dx + line.left + box.left;
final double y = offset.dy + line.baseline;
final String text = paragraph.toPlainText().substring(
box.start.index,
box.end.indexWithoutTrailingNewlines,
);
canvas.fillText(text, x, y);
// Paint the ellipsis using the same span styles.
final String? ellipsis = line.ellipsis;
if (ellipsis != null && box == line.boxes!.last) {
final double x = offset.dx + line.left + box.right;
canvas.fillText(ellipsis, x, y);
}
canvas._tearDownPaint();
}
}
void _applySpanStyleToCanvas(FlatTextSpan span, BitmapCanvas canvas) {
final SurfacePaint? paint;
final ui.Paint? foreground = span.style._foreground;
if (foreground != null) {
paint = foreground as SurfacePaint;
} else {
paint = (ui.Paint()..color = span.style._color!) as SurfacePaint;
}
canvas.setCssFont(span.style.cssFontString);
canvas._setUpPaint(paint.paintData, null);
}
}
// 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.6
import 'dart:async';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/ui.dart' hide window;
import 'package:ui/src/engine.dart';
import '../scuba.dart';
import 'helper.dart';
typedef CanvasTest = FutureOr<void> Function(EngineCanvas canvas);
const Rect bounds = Rect.fromLTWH(0, 0, 800, 600);
const Color white = Color(0xFFFFFFFF);
const Color black = Color(0xFF000000);
const Color red = Color(0xFFFF0000);
const Color green = Color(0xFF00FF00);
const Color blue = Color(0xFF0000FF);
ParagraphConstraints constrain(double width) {
return 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 {
setUpStableTestFonts();
test('paints spans and lines correctly', () {
final canvas = BitmapCanvas(bounds, RenderStrategy());
Offset offset = Offset.zero;
CanvasParagraph paragraph;
// Single-line multi-span.
paragraph = rich(ParagraphStyle(fontFamily: 'Roboto'), (builder) {
builder.pushStyle(EngineTextStyle.only(color: blue));
builder.addText('Lorem ');
builder.pushStyle(EngineTextStyle.only(
color: green,
background: Paint()..color = red,
));
builder.addText('ipsum ');
builder.pop();
builder.addText('.');
})
..layout(constrain(double.infinity));
canvas.drawParagraph(paragraph, offset);
offset = offset.translate(0, paragraph.height + 10);
// Multi-line single-span.
paragraph = rich(ParagraphStyle(fontFamily: 'Roboto'), (builder) {
builder.addText('Lorem ipsum dolor sit');
})
..layout(constrain(90.0));
canvas.drawParagraph(paragraph, offset);
offset = offset.translate(0, paragraph.height + 10);
// Multi-line multi-span.
paragraph = rich(ParagraphStyle(fontFamily: 'Roboto'), (builder) {
builder.pushStyle(EngineTextStyle.only(color: blue));
builder.addText('Lorem ipsum ');
builder.pushStyle(EngineTextStyle.only(background: Paint()..color = red));
builder.pushStyle(EngineTextStyle.only(color: green));
builder.addText('dolor ');
builder.pop();
builder.addText('sit');
})
..layout(constrain(90.0));
canvas.drawParagraph(paragraph, offset);
offset = offset.translate(0, paragraph.height + 10);
return takeScreenshot(canvas, bounds, 'canvas_paragraph_general');
});
test('respects alignment', () {
final canvas = BitmapCanvas(bounds, RenderStrategy());
Offset offset = Offset.zero;
CanvasParagraph paragraph;
void build(CanvasParagraphBuilder builder) {
builder.pushStyle(EngineTextStyle.only(color: black));
builder.addText('Lorem ');
builder.pushStyle(EngineTextStyle.only(color: blue));
builder.addText('ipsum ');
builder.pushStyle(EngineTextStyle.only(color: green));
builder.addText('dolor ');
builder.pushStyle(EngineTextStyle.only(color: red));
builder.addText('sit');
}
paragraph = rich(
ParagraphStyle(fontFamily: 'Roboto', textAlign: TextAlign.left),
build,
)..layout(constrain(100.0));
canvas.drawParagraph(paragraph, offset);
offset = offset.translate(0, paragraph.height + 10);
paragraph = rich(
ParagraphStyle(fontFamily: 'Roboto', textAlign: TextAlign.center),
build,
)..layout(constrain(100.0));
canvas.drawParagraph(paragraph, offset);
offset = offset.translate(0, paragraph.height + 10);
paragraph = rich(
ParagraphStyle(fontFamily: 'Roboto', textAlign: TextAlign.right),
build,
)..layout(constrain(100.0));
canvas.drawParagraph(paragraph, offset);
offset = offset.translate(0, paragraph.height + 10);
return takeScreenshot(canvas, bounds, 'canvas_paragraph_align');
});
test('paints spans with varying heights/baselines', () {
final canvas = BitmapCanvas(bounds, RenderStrategy());
final CanvasParagraph paragraph = rich(
ParagraphStyle(fontFamily: 'Roboto'),
(builder) {
builder.pushStyle(EngineTextStyle.only(fontSize: 20.0));
builder.addText('Lorem ');
builder.pushStyle(EngineTextStyle.only(
fontSize: 40.0,
background: Paint()..color = green,
));
builder.addText('ipsum ');
builder.pushStyle(EngineTextStyle.only(
fontSize: 10.0,
color: white,
background: Paint()..color = black,
));
builder.addText('dolor ');
builder.pushStyle(EngineTextStyle.only(fontSize: 30.0));
builder.addText('sit ');
builder.pop();
builder.pop();
builder.pushStyle(EngineTextStyle.only(
fontSize: 20.0,
background: Paint()..color = blue,
));
builder.addText('amet');
},
)..layout(constrain(220.0));
canvas.drawParagraph(paragraph, Offset.zero);
return takeScreenshot(canvas, bounds, 'canvas_paragraph_varying_heights');
});
}
// 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 'dart:html' as html;
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'package:web_engine_tester/golden_tester.dart';
Future<void> takeScreenshot(
EngineCanvas canvas,
Rect region,
String fileName, {
bool write = false,
double? maxDiffRatePercent,
}) async {
final html.Element sceneElement = html.Element.tag('flt-scene');
try {
sceneElement.append(canvas.rootElement);
html.document.body!.append(sceneElement);
await matchGoldenFile(
'$fileName.png',
region: region,
maxDiffRatePercent: maxDiffRatePercent,
write: write,
);
} finally {
// The page is reused across tests, so remove the element after taking the
// Scuba screenshot.
sceneElement.remove();
}
}
// 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.6
import 'dart:async';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/ui.dart' hide window;
import 'package:ui/src/engine.dart';
import '../scuba.dart';
import 'helper.dart';
typedef CanvasTest = FutureOr<void> Function(EngineCanvas canvas);
const Rect bounds = Rect.fromLTWH(0, 0, 800, 600);
const Color white = Color(0xFFFFFFFF);
const Color black = Color(0xFF000000);
const Color red = Color(0xFFFF0000);
const Color green = Color(0xFF00FF00);
const Color blue = Color(0xFF0000FF);
ParagraphConstraints constrain(double width) {
return 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 {
setUpStableTestFonts();
test('ellipsis', () {
final canvas = BitmapCanvas(bounds, RenderStrategy());
Offset offset = Offset.zero;
CanvasParagraph paragraph;
paragraph = rich(
ParagraphStyle(fontFamily: 'Roboto', ellipsis: '...'),
(builder) {
builder.pushStyle(EngineTextStyle.only(color: blue));
builder.addText('Lorem ');
builder.pushStyle(EngineTextStyle.only(color: green));
builder.addText('ipsum');
},
)..layout(constrain(80.0));
canvas.drawParagraph(paragraph, offset);
offset = offset.translate(0, paragraph.height + 10);
paragraph = rich(
ParagraphStyle(fontFamily: 'Roboto', ellipsis: '...'),
(builder) {
builder.pushStyle(EngineTextStyle.only(color: blue));
builder.addText('Lorem\n');
builder.pushStyle(EngineTextStyle.only(color: green));
builder.addText('ipsum ');
builder.pushStyle(EngineTextStyle.only(color: red));
builder.addText('dolor sit');
},
)..layout(constrain(80.0));
canvas.drawParagraph(paragraph, offset);
offset = offset.translate(0, paragraph.height + 10);
paragraph = rich(
ParagraphStyle(fontFamily: 'Roboto', ellipsis: '...'),
(builder) {
builder.pushStyle(EngineTextStyle.only(color: blue));
builder.addText('Lorem\n');
builder.pushStyle(EngineTextStyle.only(color: green));
builder.addText('ipsum ');
builder.pushStyle(EngineTextStyle.only(color: red));
builder.addText('d');
builder.pushStyle(EngineTextStyle.only(color: black));
builder.addText('o');
builder.pushStyle(EngineTextStyle.only(color: blue));
builder.addText('l');
builder.pushStyle(EngineTextStyle.only(color: green));
builder.addText('o');
builder.pushStyle(EngineTextStyle.only(color: red));
builder.addText('r');
builder.pushStyle(EngineTextStyle.only(color: black));
builder.addText(' ');
builder.pushStyle(EngineTextStyle.only(color: blue));
builder.addText('s');
builder.pushStyle(EngineTextStyle.only(color: green));
builder.addText('i');
builder.pushStyle(EngineTextStyle.only(color: red));
builder.addText('t');
},
)..layout(constrain(80.0));
canvas.drawParagraph(paragraph, offset);
offset = offset.translate(0, paragraph.height + 10);
paragraph = rich(
ParagraphStyle(fontFamily: 'Roboto', maxLines: 2, ellipsis: '...'),
(builder) {
builder.pushStyle(EngineTextStyle.only(color: blue));
builder.addText('Lorem');
builder.pushStyle(EngineTextStyle.only(color: green));
builder.addText('ipsum');
builder.pushStyle(EngineTextStyle.only(color: red));
builder.addText('dolor');
builder.pushStyle(EngineTextStyle.only(color: black));
builder.addText('sit');
builder.pushStyle(EngineTextStyle.only(color: blue));
builder.addText('amet');
builder.pushStyle(EngineTextStyle.only(color: blue));
builder.addText('consectetur');
},
)..layout(constrain(80.0));
canvas.drawParagraph(paragraph, offset);
offset = offset.translate(0, paragraph.height + 10);
return takeScreenshot(canvas, bounds, 'canvas_paragraph_ellipsis');
});
}
......@@ -35,8 +35,7 @@ TestLine l(
void expectLines(CanvasParagraph paragraph, List<TestLine> expectedLines) {
final String text = paragraph.toPlainText();
final List<EngineLineMetrics> lines =
paragraph.computeLineMetrics() as List<EngineLineMetrics>;
final List<EngineLineMetrics> lines = paragraph.computeLineMetrics();
expect(lines, hasLength(expectedLines.length));
for (int i = 0; i < lines.length; i++) {
final EngineLineMetrics line = lines[i];
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册