mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Implement TextAlign.justify (flutter/engine#29158)
This commit is contained in:
parent
cd18e95d73
commit
dedd0625e3
@ -1,2 +1,2 @@
|
||||
repository: https://github.com/flutter/goldens.git
|
||||
revision: 9c36f57f1a673a7ab444f4f20df16601dde15335
|
||||
revision: 8e169f3ca5cf4af283a2ff4cfd98a564fb173adf
|
||||
|
||||
@ -185,7 +185,7 @@ class CanvasParagraph implements EngineParagraph {
|
||||
}
|
||||
|
||||
final EngineLineMetrics line = lines[i];
|
||||
final List<RangeBox> boxes = line.boxes!;
|
||||
final List<RangeBox> boxes = line.boxes;
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
|
||||
int j = 0;
|
||||
|
||||
@ -273,7 +273,7 @@ class TextLayoutService {
|
||||
List<ui.TextBox> getBoxesForPlaceholders() {
|
||||
final List<ui.TextBox> boxes = <ui.TextBox>[];
|
||||
for (final EngineLineMetrics line in lines) {
|
||||
for (final RangeBox box in line.boxes!) {
|
||||
for (final RangeBox box in line.boxes) {
|
||||
if (box is PlaceholderBox) {
|
||||
boxes.add(box.toTextBox(line));
|
||||
}
|
||||
@ -303,7 +303,7 @@ class TextLayoutService {
|
||||
|
||||
for (final EngineLineMetrics line in lines) {
|
||||
if (line.overlapsWith(start, end)) {
|
||||
for (final RangeBox box in line.boxes!) {
|
||||
for (final RangeBox box in line.boxes) {
|
||||
if (box is SpanBox && box.overlapsWith(start, end)) {
|
||||
boxes.add(box.intersect(line, start, end));
|
||||
}
|
||||
@ -336,7 +336,7 @@ class TextLayoutService {
|
||||
}
|
||||
|
||||
final double dx = offset.dx - line.left;
|
||||
for (final RangeBox box in line.boxes!) {
|
||||
for (final RangeBox box in line.boxes) {
|
||||
if (box.left <= dx && dx <= box.right) {
|
||||
return box.getPositionForX(dx);
|
||||
}
|
||||
@ -892,6 +892,8 @@ class LineBuilder {
|
||||
/// Whether the end of this line is a prohibited break.
|
||||
bool get isEndProhibited => end.type == LineBreakType.prohibited;
|
||||
|
||||
int _spaceBoxCount = 0;
|
||||
|
||||
bool get isEmpty => _segments.isEmpty;
|
||||
bool get isNotEmpty => _segments.isNotEmpty;
|
||||
|
||||
@ -1131,6 +1133,9 @@ class LineBuilder {
|
||||
if (_currentBoxStart.index > poppedSegment.start.index) {
|
||||
final RangeBox poppedBox = _boxes.removeLast();
|
||||
_currentBoxStartOffset -= poppedBox.width;
|
||||
if (poppedBox is SpanBox && poppedBox.isSpaceOnly) {
|
||||
_spaceBoxCount--;
|
||||
}
|
||||
}
|
||||
|
||||
return poppedSegment;
|
||||
@ -1274,6 +1279,10 @@ class LineBuilder {
|
||||
isSpaceOnly: isSpaceOnly,
|
||||
));
|
||||
|
||||
if (isSpaceOnly) {
|
||||
_spaceBoxCount++;
|
||||
}
|
||||
|
||||
_currentBoxStartOffset = widthIncludingSpace;
|
||||
}
|
||||
|
||||
@ -1308,6 +1317,7 @@ class LineBuilder {
|
||||
ascent: ascent,
|
||||
descent: descent,
|
||||
boxes: _boxes,
|
||||
spaceBoxCount: _spaceBoxCount,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -22,60 +22,105 @@ class TextPaintService {
|
||||
// individually.
|
||||
final List<EngineLineMetrics> lines = paragraph.computeLineMetrics();
|
||||
|
||||
if (lines.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final EngineLineMetrics lastLine = lines.last;
|
||||
for (final EngineLineMetrics line in lines) {
|
||||
for (final RangeBox box in line.boxes!) {
|
||||
_paintBox(canvas, offset, line, box);
|
||||
if (line.boxes.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final RangeBox lastBox = line.boxes.last;
|
||||
final double justifyPerSpaceBox =
|
||||
_calculateJustifyPerSpaceBox(paragraph, line, lastLine, lastBox);
|
||||
|
||||
ui.Offset justifiedOffset = offset;
|
||||
|
||||
for (final RangeBox box in line.boxes) {
|
||||
final bool isTrailingSpaceBox =
|
||||
box == lastBox && box is SpanBox && box.isSpaceOnly;
|
||||
|
||||
// Don't paint background for the trailing space in the line.
|
||||
if (!isTrailingSpaceBox) {
|
||||
_paintBackground(canvas, justifiedOffset, line, box, justifyPerSpaceBox);
|
||||
}
|
||||
_paintText(canvas, justifiedOffset, line, box);
|
||||
|
||||
if (box is SpanBox && box.isSpaceOnly && justifyPerSpaceBox != 0.0) {
|
||||
justifiedOffset = justifiedOffset.translate(justifyPerSpaceBox, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _paintBox(
|
||||
void _paintBackground(
|
||||
BitmapCanvas canvas,
|
||||
ui.Offset offset,
|
||||
EngineLineMetrics line,
|
||||
RangeBox box,
|
||||
double justifyPerSpaceBox,
|
||||
) {
|
||||
// Placeholder spans don't need any painting. Their boxes should remain
|
||||
// empty so that their underlying widgets do their own painting.
|
||||
if (box is SpanBox) {
|
||||
final FlatTextSpan span = box.span;
|
||||
|
||||
// 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,
|
||||
);
|
||||
ui.Rect rect = box.toTextBox(line).toRect().shift(offset);
|
||||
if (box.isSpaceOnly) {
|
||||
rect = ui.Rect.fromPoints(
|
||||
rect.topLeft,
|
||||
rect.bottomRight.translate(justifyPerSpaceBox, 0.0),
|
||||
);
|
||||
}
|
||||
canvas.drawRect(rect, background.paintData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _paintText(
|
||||
BitmapCanvas canvas,
|
||||
ui.Offset offset,
|
||||
EngineLineMetrics line,
|
||||
RangeBox box,
|
||||
) {
|
||||
// There's no text to paint in placeholder spans.
|
||||
if (box is SpanBox) {
|
||||
final FlatTextSpan span = box.span;
|
||||
|
||||
// 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,
|
||||
);
|
||||
final double? letterSpacing = span.style.letterSpacing;
|
||||
if (letterSpacing == null || letterSpacing == 0.0) {
|
||||
canvas.fillText(text, x, y, shadows: span.style.shadows);
|
||||
} else {
|
||||
// TODO(mdebbar): Implement letter-spacing on canvas more efficiently:
|
||||
// https://github.com/flutter/flutter/issues/51234
|
||||
double charX = x;
|
||||
final int len = text.length;
|
||||
for (int i = 0; i < len; i++) {
|
||||
final String char = text[i];
|
||||
canvas.fillText(char, charX.roundToDouble(), y,
|
||||
shadows: span.style.shadows);
|
||||
charX += letterSpacing + canvas.measureText(char).width!;
|
||||
|
||||
// Don't paint the text for space-only boxes. This is just an
|
||||
// optimization, it doesn't have any effect on the output.
|
||||
if (!box.isSpaceOnly) {
|
||||
final String text = paragraph.toPlainText().substring(
|
||||
box.start.index,
|
||||
box.end.indexWithoutTrailingNewlines,
|
||||
);
|
||||
final double? letterSpacing = span.style.letterSpacing;
|
||||
if (letterSpacing == null || letterSpacing == 0.0) {
|
||||
canvas.fillText(text, x, y, shadows: span.style.shadows);
|
||||
} else {
|
||||
// TODO(mdebbar): Implement letter-spacing on canvas more efficiently:
|
||||
// https://github.com/flutter/flutter/issues/51234
|
||||
double charX = x;
|
||||
final int len = text.length;
|
||||
for (int i = 0; i < len; i++) {
|
||||
final String char = text[i];
|
||||
canvas.fillText(char, charX.roundToDouble(), y,
|
||||
shadows: span.style.shadows);
|
||||
charX += letterSpacing + canvas.measureText(char).width!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Paint the ellipsis using the same span styles.
|
||||
final String? ellipsis = line.ellipsis;
|
||||
if (ellipsis != null && box == line.boxes!.last) {
|
||||
if (ellipsis != null && box == line.boxes.last) {
|
||||
final double x = offset.dx + line.left + box.right;
|
||||
canvas.fillText(ellipsis, x, y);
|
||||
}
|
||||
@ -97,3 +142,31 @@ class TextPaintService {
|
||||
canvas.setUpPaint(paint.paintData, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates for the given [line], the amount of extra width that needs to be
|
||||
/// added to each space box in order to align the line with the rest of the
|
||||
/// paragraph.
|
||||
double _calculateJustifyPerSpaceBox(
|
||||
CanvasParagraph paragraph,
|
||||
EngineLineMetrics line,
|
||||
EngineLineMetrics lastLine,
|
||||
RangeBox lastBox,
|
||||
) {
|
||||
// Don't apply any justification on the last line.
|
||||
if (line != lastLine &&
|
||||
paragraph.width.isFinite &&
|
||||
paragraph.paragraphStyle.textAlign == ui.TextAlign.justify) {
|
||||
final double justifyTotal = paragraph.width - line.width;
|
||||
|
||||
int spaceBoxesToJustify = line.spaceBoxCount;
|
||||
// If the last box is a space box, we can't use it to justify text.
|
||||
if (lastBox is SpanBox && lastBox.isSpaceOnly) {
|
||||
spaceBoxesToJustify--;
|
||||
}
|
||||
if (spaceBoxesToJustify > 0) {
|
||||
return justifyTotal / spaceBoxesToJustify;
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@ -31,33 +31,8 @@ class EngineLineMetrics implements ui.LineMetrics {
|
||||
endIndex = -1,
|
||||
endIndexWithoutNewlines = -1,
|
||||
widthWithTrailingSpaces = width,
|
||||
boxes = null;
|
||||
|
||||
EngineLineMetrics.withText(
|
||||
String this.displayText, {
|
||||
required this.startIndex,
|
||||
required this.endIndex,
|
||||
required this.endIndexWithoutNewlines,
|
||||
required this.hardBreak,
|
||||
required this.width,
|
||||
required this.widthWithTrailingSpaces,
|
||||
required this.left,
|
||||
required this.lineNumber,
|
||||
}) : assert(displayText != null), // ignore: unnecessary_null_comparison,
|
||||
assert(startIndex != null), // ignore: unnecessary_null_comparison
|
||||
assert(endIndex != null), // ignore: unnecessary_null_comparison
|
||||
assert(endIndexWithoutNewlines != null), // ignore: unnecessary_null_comparison
|
||||
assert(hardBreak != null), // ignore: unnecessary_null_comparison
|
||||
assert(width != null), // ignore: unnecessary_null_comparison
|
||||
assert(left != null), // ignore: unnecessary_null_comparison
|
||||
assert(lineNumber != null && lineNumber >= 0), // ignore: unnecessary_null_comparison
|
||||
ellipsis = null,
|
||||
ascent = double.infinity,
|
||||
descent = double.infinity,
|
||||
unscaledAscent = double.infinity,
|
||||
height = double.infinity,
|
||||
baseline = double.infinity,
|
||||
boxes = null;
|
||||
boxes = <RangeBox>[],
|
||||
spaceBoxCount = 0;
|
||||
|
||||
EngineLineMetrics.rich(
|
||||
this.lineNumber, {
|
||||
@ -74,6 +49,7 @@ class EngineLineMetrics implements ui.LineMetrics {
|
||||
required this.ascent,
|
||||
required this.descent,
|
||||
required this.boxes,
|
||||
required this.spaceBoxCount,
|
||||
}) : displayText = null,
|
||||
unscaledAscent = double.infinity;
|
||||
|
||||
@ -101,7 +77,10 @@ class EngineLineMetrics implements ui.LineMetrics {
|
||||
|
||||
/// The list of boxes representing the entire line, possibly across multiple
|
||||
/// spans.
|
||||
final List<RangeBox>? boxes;
|
||||
final List<RangeBox> boxes;
|
||||
|
||||
/// The number of boxes that are space-only.
|
||||
final int spaceBoxCount;
|
||||
|
||||
@override
|
||||
final bool hardBreak;
|
||||
|
||||
@ -0,0 +1,164 @@
|
||||
// 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:async';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' hide window;
|
||||
|
||||
import 'helper.dart';
|
||||
import 'text_scuba.dart';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
Future<void> testMain() async {
|
||||
setUpStableTestFonts();
|
||||
|
||||
test('TextAlign.justify with multiple spans', () {
|
||||
const Rect bounds = Rect.fromLTWH(0, 0, 400, 400);
|
||||
final BitmapCanvas canvas = BitmapCanvas(bounds, RenderStrategy());
|
||||
|
||||
void build(CanvasParagraphBuilder builder) {
|
||||
builder.pushStyle(EngineTextStyle.only(color: black));
|
||||
builder.addText('Lorem ipsum dolor sit ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: blue));
|
||||
builder.addText('amet, consectetur ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: green));
|
||||
builder.addText('adipiscing elit, sed do eiusmod tempor incididunt ut ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: red));
|
||||
builder
|
||||
.addText('labore et dolore magna aliqua. Ut enim ad minim veniam, ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: lightPurple));
|
||||
builder.addText('quis nostrud exercitation ullamco ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: blue));
|
||||
builder.addText('laboris nisi ut aliquip ex ea commodo consequat.');
|
||||
}
|
||||
|
||||
final CanvasParagraph paragraph = rich(
|
||||
EngineParagraphStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 20.0,
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
build,
|
||||
);
|
||||
paragraph.layout(constrain(250.0));
|
||||
canvas.drawParagraph(paragraph, Offset.zero);
|
||||
|
||||
return takeScreenshot(canvas, bounds, 'canvas_paragraph_justify');
|
||||
});
|
||||
|
||||
test('TextAlign.justify with single space and empty line', () {
|
||||
const Rect bounds = Rect.fromLTWH(0, 0, 400, 400);
|
||||
final BitmapCanvas canvas = BitmapCanvas(bounds, RenderStrategy());
|
||||
|
||||
void build(CanvasParagraphBuilder builder) {
|
||||
builder.pushStyle(bg(yellow));
|
||||
builder.pushStyle(EngineTextStyle.only(color: black));
|
||||
builder.addText('Loremipsumdolorsit');
|
||||
builder.pushStyle(EngineTextStyle.only(color: blue));
|
||||
builder.addText('amet, consectetur\n\n');
|
||||
builder.pushStyle(EngineTextStyle.only(color: lightPurple));
|
||||
builder.addText('adipiscing elit, sed do eiusmod tempor incididunt ut ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: red));
|
||||
builder.addText('labore et dolore magna aliqua.');
|
||||
}
|
||||
|
||||
final CanvasParagraph paragraph = rich(
|
||||
EngineParagraphStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 20.0,
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
build,
|
||||
);
|
||||
paragraph.layout(constrain(250.0));
|
||||
canvas.drawParagraph(paragraph, Offset.zero);
|
||||
|
||||
return takeScreenshot(
|
||||
canvas, bounds, 'canvas_paragraph_justify_empty_line');
|
||||
});
|
||||
|
||||
test('TextAlign.justify with ellipsis', () {
|
||||
const Rect bounds = Rect.fromLTWH(0, 0, 400, 300);
|
||||
final BitmapCanvas canvas = BitmapCanvas(bounds, RenderStrategy());
|
||||
|
||||
final EngineParagraphStyle paragraphStyle = EngineParagraphStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 20.0,
|
||||
textAlign: TextAlign.justify,
|
||||
maxLines: 4,
|
||||
ellipsis: '...',
|
||||
);
|
||||
final CanvasParagraph paragraph = rich(
|
||||
paragraphStyle,
|
||||
(CanvasParagraphBuilder builder) {
|
||||
builder.pushStyle(EngineTextStyle.only(color: black));
|
||||
builder.addText('Lorem ipsum dolor sit ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: blue));
|
||||
builder.addText('amet, consectetur ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: green));
|
||||
builder
|
||||
.addText('adipiscing elit, sed do eiusmod tempor incididunt ut ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: red));
|
||||
builder.addText(
|
||||
'labore et dolore magna aliqua. Ut enim ad minim veniam, ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: lightPurple));
|
||||
builder.addText('quis nostrud exercitation ullamco ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: blue));
|
||||
builder.addText('laboris nisi ut aliquip ex ea commodo consequat.');
|
||||
},
|
||||
);
|
||||
paragraph.layout(constrain(250));
|
||||
canvas.drawParagraph(paragraph, Offset.zero);
|
||||
|
||||
return takeScreenshot(canvas, bounds, 'canvas_paragraph_justify_ellipsis');
|
||||
});
|
||||
|
||||
test('TextAlign.justify with background', () {
|
||||
const Rect bounds = Rect.fromLTWH(0, 0, 400, 400);
|
||||
final BitmapCanvas canvas = BitmapCanvas(bounds, RenderStrategy());
|
||||
|
||||
final EngineParagraphStyle paragraphStyle = EngineParagraphStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 20.0,
|
||||
textAlign: TextAlign.justify,
|
||||
);
|
||||
final CanvasParagraph paragraph = rich(
|
||||
paragraphStyle,
|
||||
(CanvasParagraphBuilder builder) {
|
||||
builder.pushStyle(EngineTextStyle.only(color: black));
|
||||
builder.pushStyle(bg(blue));
|
||||
builder.addText('Lorem ipsum dolor sit ');
|
||||
builder.pushStyle(bg(black));
|
||||
builder.pushStyle(EngineTextStyle.only(color: white));
|
||||
builder.addText('amet, consectetur ');
|
||||
builder.pop();
|
||||
builder.pushStyle(bg(green));
|
||||
builder
|
||||
.addText('adipiscing elit, sed do eiusmod tempor incididunt ut ');
|
||||
builder.pushStyle(bg(yellow));
|
||||
builder.addText(
|
||||
'labore et dolore magna aliqua. Ut enim ad minim veniam, ');
|
||||
builder.pushStyle(bg(red));
|
||||
builder.addText('quis nostrud exercitation ullamco ');
|
||||
builder.pushStyle(bg(green));
|
||||
builder.addText('laboris nisi ut aliquip ex ea commodo consequat.');
|
||||
},
|
||||
);
|
||||
paragraph.layout(constrain(250));
|
||||
canvas.drawParagraph(paragraph, Offset.zero);
|
||||
|
||||
return takeScreenshot(
|
||||
canvas, bounds, 'canvas_paragraph_justify_background');
|
||||
});
|
||||
}
|
||||
|
||||
EngineTextStyle bg(Color color) {
|
||||
return EngineTextStyle.only(background: Paint()..color = color);
|
||||
}
|
||||
@ -401,26 +401,26 @@ Future<void> testMain() async {
|
||||
// There should be no "B" in the first line's boxes.
|
||||
expect(firstLine.boxes, hasLength(2));
|
||||
|
||||
expect((firstLine.boxes![0] as SpanBox).toText(), 'AAA');
|
||||
expect((firstLine.boxes![0] as SpanBox).left, 0.0);
|
||||
expect((firstLine.boxes[0] as SpanBox).toText(), 'AAA');
|
||||
expect((firstLine.boxes[0] as SpanBox).left, 0.0);
|
||||
|
||||
expect((firstLine.boxes![1] as SpanBox).toText(), ' ');
|
||||
expect((firstLine.boxes![1] as SpanBox).left, 30.0);
|
||||
expect((firstLine.boxes[1] as SpanBox).toText(), ' ');
|
||||
expect((firstLine.boxes[1] as SpanBox).left, 30.0);
|
||||
|
||||
// Make sure the second line isn't missing any boxes.
|
||||
expect(secondLine.boxes, hasLength(4));
|
||||
|
||||
expect((secondLine.boxes![0] as SpanBox).toText(), 'B');
|
||||
expect((secondLine.boxes![0] as SpanBox).left, 0.0);
|
||||
expect((secondLine.boxes[0] as SpanBox).toText(), 'B');
|
||||
expect((secondLine.boxes[0] as SpanBox).left, 0.0);
|
||||
|
||||
expect((secondLine.boxes![1] as SpanBox).toText(), '_C');
|
||||
expect((secondLine.boxes![1] as SpanBox).left, 10.0);
|
||||
expect((secondLine.boxes[1] as SpanBox).toText(), '_C');
|
||||
expect((secondLine.boxes[1] as SpanBox).left, 10.0);
|
||||
|
||||
expect((secondLine.boxes![2] as SpanBox).toText(), ' ');
|
||||
expect((secondLine.boxes![2] as SpanBox).left, 30.0);
|
||||
expect((secondLine.boxes[2] as SpanBox).toText(), ' ');
|
||||
expect((secondLine.boxes[2] as SpanBox).left, 30.0);
|
||||
|
||||
expect((secondLine.boxes![3] as SpanBox).toText(), 'DD');
|
||||
expect((secondLine.boxes![3] as SpanBox).left, 40.0);
|
||||
expect((secondLine.boxes[3] as SpanBox).toText(), 'DD');
|
||||
expect((secondLine.boxes[3] as SpanBox).left, 40.0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user