Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/dev/goldens_lock.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
repository: https://github.com/flutter/goldens.git
revision: c808c28c81b6c3143ae969e8c49bed4a6d49aabb
revision: 4946ab2de031c14d30502efcaf51220e0be4d1f1
1 change: 1 addition & 0 deletions lib/web_ui/lib/src/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
15 changes: 7 additions & 8 deletions lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -90,6 +94,7 @@ class CanvasParagraph implements EngineParagraph {
.benchmark('text_layout', stopwatch.elapsedMicroseconds.toDouble());
}

isLaidOut = true;
_lastUsedConstraints = constraints;
}

Expand All @@ -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
Expand Down Expand Up @@ -182,9 +184,6 @@ class CanvasParagraph implements EngineParagraph {
@override
final bool drawOnCanvas = true;

@override
bool isLaidOut = false;

@override
List<ui.TextBox> getBoxesForRange(
int start,
Expand Down Expand Up @@ -220,7 +219,7 @@ class CanvasParagraph implements EngineParagraph {
}

@override
List<ui.LineMetrics> computeLineMetrics() {
List<EngineLineMetrics> computeLineMetrics() {
return _layoutService.lines;
}
}
Expand Down
14 changes: 14 additions & 0 deletions lib/web_ui/lib/src/engine/text/layout_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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.
///
Expand Down Expand Up @@ -659,6 +667,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 {
Expand Down
80 changes: 80 additions & 0 deletions lib/web_ui/lib/src/engine/text/paint_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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);
}
}
168 changes: 168 additions & 0 deletions lib/web_ui/test/golden_tests/engine/canvas_paragraph/general_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// 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');
});
}
34 changes: 34 additions & 0 deletions lib/web_ui/test/golden_tests/engine/canvas_paragraph/helper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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();
}
}
Loading