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

[web] Placeholders for rich paragraphs (#23160)

上级 d38aac7a
repository: https://github.com/flutter/goldens.git
revision: 4946ab2de031c14d30502efcaf51220e0be4d1f1
revision: 7529e9018b11c79334b99d1e7343fcd500c77b08
......@@ -170,9 +170,7 @@ class CanvasParagraph implements EngineParagraph {
@override
List<ui.TextBox> getBoxesForPlaceholders() {
// TODO(mdebbar): After layout, placeholders positions should've been
// determined and can be used to compute their boxes.
return <ui.TextBox>[];
return _layoutService.getBoxesForPlaceholders();
}
// TODO(mdebbar): Check for child spans if any has styles that can't be drawn
......
......@@ -104,21 +104,16 @@ class TextLayoutService {
// ********************************* //
if (span is PlaceholderSpan) {
spanometer.currentSpan = null;
final double lineWidth = currentLine.width + span.width;
if (lineWidth <= constraints.width) {
if (currentLine.widthIncludingSpace + span.width <= constraints.width) {
// The placeholder fits on the current line.
// TODO(mdebbar):
// (1) adjust the current line's height to fit the placeholder.
// (2) update accumulated line width.
// (3) add placeholder box to line.
currentLine.addPlaceholder(span);
} else {
// The placeholder can't fit on the current line.
// TODO(mdebbar):
// (1) create a line.
// (2) adjust the new line's height to fit the placeholder.
// (3) update `lineStart`, etc.
// (4) add placeholder box to line.
if (currentLine.isNotEmpty) {
lines.add(currentLine.build());
currentLine = currentLine.nextLine();
}
currentLine.addPlaceholder(span);
}
} else if (span is FlatTextSpan) {
spanometer.currentSpan = span;
......@@ -203,7 +198,7 @@ class TextLayoutService {
while (currentLine.end.type != LineBreakType.endOfText) {
if (span is PlaceholderSpan) {
// TODO(mdebbar): Do placeholders affect min/max intrinsic width?
currentLine.addPlaceholder(span);
} else if (span is FlatTextSpan) {
spanometer.currentSpan = span;
final LineBreakResult nextBreak = currentLine.findNextBreak(span.end);
......@@ -211,26 +206,38 @@ class TextLayoutService {
// For the purpose of max intrinsic width, we don't care if the line
// fits within the constraints or not. So we always extend it.
currentLine.extendTo(nextBreak);
}
final double widthOfLastSegment = currentLine.lastSegment.width;
if (minIntrinsicWidth < widthOfLastSegment) {
minIntrinsicWidth = widthOfLastSegment;
}
final double widthOfLastSegment = currentLine.lastSegment.width;
if (minIntrinsicWidth < widthOfLastSegment) {
minIntrinsicWidth = widthOfLastSegment;
}
if (currentLine.end.isHard) {
// Max intrinsic width includes the width of trailing spaces.
if (maxIntrinsicWidth < currentLine.widthIncludingSpace) {
maxIntrinsicWidth = currentLine.widthIncludingSpace;
}
currentLine = currentLine.nextLine();
if (currentLine.end.isHard) {
// Max intrinsic width includes the width of trailing spaces.
if (maxIntrinsicWidth < currentLine.widthIncludingSpace) {
maxIntrinsicWidth = currentLine.widthIncludingSpace;
}
currentLine = currentLine.nextLine();
}
// Only go to the next span if we've reached the end of this span.
if (currentLine.end.index >= span.end && spanIndex < spanCount - 1) {
span = paragraph.spans[++spanIndex];
// Only go to the next span if we've reached the end of this span.
if (currentLine.end.index >= span.end && spanIndex < spanCount - 1) {
span = paragraph.spans[++spanIndex];
}
}
}
List<ui.TextBox> getBoxesForPlaceholders() {
final List<ui.TextBox> boxes = <ui.TextBox>[];
for (final EngineLineMetrics line in lines) {
for (final RangeBox box in line.boxes!) {
if (box is PlaceholderBox) {
boxes.add(box.toTextBox(line));
}
}
}
return boxes;
}
List<ui.TextBox> getBoxesForRange(
......@@ -255,7 +262,7 @@ class TextLayoutService {
for (final EngineLineMetrics line in lines) {
if (line.overlapsWith(start, end)) {
for (final RangeBox box in line.boxes!) {
if (box.overlapsWith(start, end)) {
if (box is SpanBox && box.overlapsWith(start, end)) {
boxes.add(box.intersect(line, start, end));
}
}
......@@ -328,23 +335,133 @@ class TextLayoutService {
/// The box's coordinates are all relative to the line it belongs to. For
/// example, [left] is the distance from the left edge of the line to the left
/// edge of the box.
class RangeBox {
RangeBox.fromSpanometer(
this.spanometer, {
abstract class RangeBox {
LineBreakResult get start;
LineBreakResult get end;
/// The distance from the left edge of the line to the left edge of the box.
double get left;
/// The distance from the left edge of the line to the right edge of the box.
double get right;
/// The direction in which text inside this box flows.
ui.TextDirection get direction;
/// 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);
/// Returns the text position within this box's range that's closest to the
/// given [x] offset.
///
/// The [x] offset is expected to be relative to the left edge of the line,
/// just like the coordinates of this box.
ui.TextPosition getPositionForX(double x);
}
/// Represents a box for a [PlaceholderSpan].
class PlaceholderBox extends RangeBox {
PlaceholderBox(
this.placeholder, {
required LineBreakResult index,
required this.left,
required this.direction,
}) : start = index, end = index;
final PlaceholderSpan placeholder;
@override
final LineBreakResult start;
@override
final LineBreakResult end;
@override
final double left;
@override
double get right => left + placeholder.width;
@override
final ui.TextDirection direction;
ui.TextBox toTextBox(EngineLineMetrics line) {
final double left = line.left + this.left;
final double right = line.left + this.right;
final double lineTop = line.baseline - line.ascent;
final double top;
switch (placeholder.alignment) {
case ui.PlaceholderAlignment.top:
top = lineTop;
break;
case ui.PlaceholderAlignment.middle:
top = lineTop + (line.height - placeholder.height) / 2;
break;
case ui.PlaceholderAlignment.bottom:
top = lineTop + line.height - placeholder.height;
break;
case ui.PlaceholderAlignment.aboveBaseline:
top = line.baseline - placeholder.height;
break;
case ui.PlaceholderAlignment.belowBaseline:
top = line.baseline;
break;
case ui.PlaceholderAlignment.baseline:
top = line.baseline - placeholder.baselineOffset;
break;
}
return ui.TextBox.fromLTRBD(
left,
top,
right,
top + placeholder.height,
direction,
);
}
@override
ui.TextPosition getPositionForX(double x) {
// See if `x` is closer to the left edge or the right edge of the box.
final bool closerToLeft = x - left < right - x;
return ui.TextPosition(
offset: start.index,
affinity: closerToLeft ? ui.TextAffinity.upstream : ui.TextAffinity.downstream,
);
}
}
/// Represents a box in a [FlatTextSpan].
class SpanBox extends RangeBox {
SpanBox(
Spanometer spanometer, {
required this.start,
required this.end,
required this.left,
}) : span = spanometer.currentSpan,
required this.direction,
}) : this.spanometer = spanometer,
span = spanometer.currentSpan,
height = spanometer.height,
baseline = spanometer.alphabeticBaseline,
baseline = spanometer.ascent,
width = spanometer.measureIncludingSpace(start, end);
final Spanometer spanometer;
final ParagraphSpan span;
final FlatTextSpan span;
final LineBreakResult start;
final LineBreakResult end;
/// The distance from the left edge of the line to the left edge of the box.
@override
final double left;
/// The distance from the left edge to the right edge of the box.
......@@ -357,11 +474,10 @@ class RangeBox {
/// the box.
final double baseline;
/// The direction in which text inside this box flows.
ui.TextDirection get direction =>
spanometer.paragraph.paragraphStyle._effectiveTextDirection;
@override
final ui.TextDirection direction;
/// The distance from the left edge of the line to the right edge of the box.
@override
double get right => left + width;
/// Whether this box's range overlaps with the range from [startIndex] to
......@@ -390,14 +506,14 @@ class RangeBox {
if (start <= this.start.index) {
left = this.left;
} else {
spanometer.currentSpan = span as FlatTextSpan;
spanometer.currentSpan = span;
left = this.left + spanometer._measure(this.start.index, start);
}
if (end >= this.end.indexWithoutTrailingNewlines) {
right = this.right;
} else {
spanometer.currentSpan = span as FlatTextSpan;
spanometer.currentSpan = span;
right = this.right -
spanometer._measure(end, this.end.indexWithoutTrailingNewlines);
}
......@@ -414,13 +530,9 @@ class RangeBox {
);
}
/// Returns the text position within this box's range that's closest to the
/// given [x] offset.
///
/// The [x] offset is expected to be relative to the left edge of the line,
/// just like the coordinates of this box.
@override
ui.TextPosition getPositionForX(double x) {
spanometer.currentSpan = span as FlatTextSpan;
spanometer.currentSpan = span;
// Make `x` relative to this box.
x -= left;
......@@ -572,11 +684,14 @@ class LineBuilder {
/// The width of trailing white space in the line.
double get widthOfTrailingSpace => widthIncludingSpace - width;
/// The alphabetic baseline of the line so far.
double alphabeticBaseline = 0.0;
/// The distance from the top of the line to the alphabetic baseline.
double ascent = 0.0;
/// The distance from the bottom of the line to the alphabetic baseline.
double descent = 0.0;
/// The height of the line so far.
double height = 0.0;
double get height => ascent + descent;
/// The last segment in this line.
LineSegment get lastSegment => _segments.last;
......@@ -626,13 +741,75 @@ class LineBuilder {
'Cannot extend a line that ends with a hard break.',
);
alphabeticBaseline =
math.max(alphabeticBaseline, spanometer.alphabeticBaseline);
height = math.max(height, spanometer.height);
ascent = math.max(ascent, spanometer.ascent);
descent = math.max(descent, spanometer.descent);
_addSegment(_createSegment(newEnd));
}
void addPlaceholder(PlaceholderSpan placeholder) {
// Increase the line's height to fit the placeholder, if necessary.
final double ascent, descent;
switch (placeholder.alignment) {
case ui.PlaceholderAlignment.top:
// The placeholder is aligned to the top of text, which means it has the
// same `ascent` as the remaining text. We only need to extend the
// `descent` enough to fit the placeholder.
ascent = this.ascent;
descent = placeholder.height - this.ascent;
break;
case ui.PlaceholderAlignment.bottom:
// The opposite of `top`. The `descent` is the same, but we extend the
// `ascent`.
ascent = placeholder.height - this.descent;
descent = this.descent;
break;
case ui.PlaceholderAlignment.middle:
final double textMidPoint = this.height / 2;
final double placeholderMidPoint = placeholder.height / 2;
final double diff = placeholderMidPoint - textMidPoint;
ascent = this.ascent + diff;
descent = this.descent + diff;
break;
case ui.PlaceholderAlignment.aboveBaseline:
ascent = placeholder.height;
descent = 0.0;
break;
case ui.PlaceholderAlignment.belowBaseline:
ascent = 0.0;
descent = placeholder.height;
break;
case ui.PlaceholderAlignment.baseline:
ascent = placeholder.baselineOffset;
descent = placeholder.height - ascent;
break;
}
this.ascent = math.max(this.ascent, ascent);
this.descent = math.max(this.descent, descent);
_addSegment(LineSegment(
span: placeholder,
start: end,
end: end,
width: placeholder.width,
widthIncludingSpace: placeholder.width,
));
// Add the placeholder box.
_boxes.add(PlaceholderBox(
placeholder,
index: _boxStart,
left: _boxLeft,
direction: paragraph.paragraphStyle._effectiveTextDirection,
));
}
/// Creates a new segment to be appended to the end of this line.
LineSegment _createSegment(LineBreakResult segmentEnd) {
// The segment starts at the end of the line.
......@@ -822,11 +999,12 @@ class LineBuilder {
return;
}
_boxes.add(RangeBox.fromSpanometer(
_boxes.add(SpanBox(
spanometer,
start: boxStart,
end: boxEnd,
left: _boxLeft,
direction: paragraph.paragraphStyle._effectiveTextDirection,
));
}
......@@ -849,7 +1027,9 @@ class LineBuilder {
widthWithTrailingSpaces: widthIncludingSpace + ellipsisWidth,
left: alignOffset,
height: height,
baseline: accumulatedHeight + alphabeticBaseline,
baseline: accumulatedHeight + ascent,
ascent: ascent,
descent: descent,
boxes: _boxes,
);
}
......@@ -929,8 +1109,11 @@ class Spanometer {
}
}
/// The alphabetic baseline for the current span.
double get alphabeticBaseline => _currentRuler!.alphabeticBaseline;
/// The distance from the top of the current span to the alphabetic baseline.
double get ascent => _currentRuler!.alphabeticBaseline;
/// The distance from the bottom of the current span to the alphabetic baseline.
double get descent => height - ascent;
/// The line height of the current span.
double get height => _currentRuler!.height;
......
......@@ -30,11 +30,11 @@ class TextPaintService {
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) {
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) {
......
......@@ -66,12 +66,12 @@ class EngineLineMetrics implements ui.LineMetrics {
required this.left,
required this.height,
required this.baseline,
required this.ascent,
required this.descent,
// Didn't use `this.boxes` because we want it to be non-null in this
// constructor.
required List<RangeBox> boxes,
}) : displayText = null,
ascent = double.infinity,
descent = double.infinity,
unscaledAscent = double.infinity,
this.boxes = boxes;
......
// 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('draws paragraphs with placeholders', () {
final canvas = BitmapCanvas(bounds, RenderStrategy());
Offset offset = Offset.zero;
for (PlaceholderAlignment placeholderAlignment in PlaceholderAlignment.values) {
final CanvasParagraph paragraph = rich(
ParagraphStyle(fontFamily: 'Roboto', fontSize: 14.0),
(builder) {
builder.pushStyle(TextStyle(color: black));
builder.addText('Lorem ipsum');
builder.addPlaceholder(
80.0,
50.0,
placeholderAlignment,
baselineOffset: 40.0,
baseline: TextBaseline.alphabetic,
);
builder.pushStyle(TextStyle(color: blue));
builder.addText('dolor sit amet, consecteur.');
},
)..layout(constrain(200.0));
// Draw the paragraph.
canvas.drawParagraph(paragraph, offset);
// Then fill the placeholders.
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
final SurfacePaint redPaint = Paint()..color = red;
canvas.drawRect(placeholderBox.toRect().shift(offset), redPaint.paintData);
offset = offset.translate(0.0, paragraph.height + 30.0);
}
return takeScreenshot(canvas, bounds, 'canvas_paragraph_placeholders');
});
test('draws paragraphs with placeholders and text align', () {
final canvas = BitmapCanvas(bounds, RenderStrategy());
const List<TextAlign> aligns = <TextAlign>[
TextAlign.left,
TextAlign.center,
TextAlign.right,
];
Offset offset = Offset.zero;
for (TextAlign align in aligns) {
final CanvasParagraph paragraph = rich(
ParagraphStyle(fontFamily: 'Roboto', fontSize: 14.0, textAlign: align),
(builder) {
builder.pushStyle(TextStyle(color: black));
builder.addText('Lorem');
builder.addPlaceholder(80.0, 50.0, PlaceholderAlignment.bottom);
builder.pushStyle(TextStyle(color: blue));
builder.addText('ipsum.');
},
)..layout(constrain(200.0));
// Draw the paragraph.
canvas.drawParagraph(paragraph, offset);
// Then fill the placeholders.
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
final SurfacePaint redPaint = Paint()..color = red;
canvas.drawRect(placeholderBox.toRect().shift(offset), redPaint.paintData);
offset = offset.translate(0.0, paragraph.height + 30.0);
}
return takeScreenshot(canvas, bounds, 'canvas_paragraph_placeholders_align');
});
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册