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

[web] Implement support for WidgetSpan (aka paragraph placeholders) (#20276)

上级 ece90428
......@@ -448,6 +448,7 @@ class DomTextMeasurementService extends TextMeasurementService {
alphabeticBaseline: alphabeticBaseline,
ideographicBaseline: ideographicBaseline,
lines: lines,
placeholderBoxes: ruler.measurePlaceholderBoxes(),
textAlign: paragraph._textAlign,
textDirection: paragraph._textDirection,
);
......@@ -498,6 +499,7 @@ class DomTextMeasurementService extends TextMeasurementService {
alphabeticBaseline: alphabeticBaseline,
ideographicBaseline: ideographicBaseline,
lines: null,
placeholderBoxes: ruler.measurePlaceholderBoxes(),
textAlign: paragraph._textAlign,
textDirection: paragraph._textDirection,
);
......@@ -609,6 +611,7 @@ class CanvasTextMeasurementService extends TextMeasurementService {
maxIntrinsicWidth: maxIntrinsicCalculator.value,
width: constraints.width,
lines: linesCalculator.lines,
placeholderBoxes: <ui.TextBox>[],
textAlign: paragraph._textAlign,
textDirection: paragraph._textDirection,
);
......
......@@ -7,6 +7,8 @@ part of engine;
const ui.Color _defaultTextColor = ui.Color(0xFFFF0000);
const String _placeholderClass = 'paragraph-placeholder';
class EngineLineMetrics implements ui.LineMetrics {
EngineLineMetrics({
required this.hardBreak,
......@@ -174,6 +176,7 @@ class EngineParagraph implements ui.Paragraph {
required ui.TextAlign textAlign,
required ui.TextDirection textDirection,
required ui.Paint? background,
required this.placeholderCount,
}) : assert((plainText == null && paint == null) ||
(plainText != null && paint != null)),
_paragraphElement = paragraphElement,
......@@ -192,6 +195,8 @@ class EngineParagraph implements ui.Paragraph {
final ui.TextDirection _textDirection;
final SurfacePaint? _background;
final int placeholderCount;
@visibleForTesting
String? get plainText => _plainText;
......@@ -332,7 +337,8 @@ class EngineParagraph implements ui.Paragraph {
@override
List<ui.TextBox> getBoxesForPlaceholders() {
return const <ui.TextBox>[];
assert(_isLaidOut);
return _measurementResult!.placeholderBoxes;
}
/// Returns `true` if this paragraph can be directly painted to the canvas.
......@@ -482,6 +488,7 @@ class EngineParagraph implements ui.Paragraph {
textAlign: _textAlign,
textDirection: _textDirection,
background: _background,
placeholderCount: placeholderCount,
);
}
......@@ -1058,11 +1065,11 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
@override
int get placeholderCount => _placeholderCount;
late int _placeholderCount;
int _placeholderCount = 0;
@override
List<double> get placeholderScales => _placeholderScales;
List<double> _placeholderScales = <double>[];
final List<double> _placeholderScales = <double>[];
@override
void addPlaceholder(
......@@ -1073,8 +1080,20 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
double? baselineOffset,
ui.TextBaseline? baseline,
}) {
// TODO(garyq): Implement stub_ui version of this.
throw UnimplementedError();
// Require a baseline to be specified if using a baseline-based alignment.
assert((alignment == ui.PlaceholderAlignment.aboveBaseline ||
alignment == ui.PlaceholderAlignment.belowBaseline ||
alignment == ui.PlaceholderAlignment.baseline) ? baseline != null : true);
_placeholderCount++;
_placeholderScales.add(scale);
_ops.add(ParagraphPlaceholder(
width * scale,
height * scale,
alignment,
baselineOffset: (baselineOffset ?? height) * scale,
baseline: baseline ?? ui.TextBaseline.alphabetic,
));
}
// TODO(yjbanov): do we need to do this?
......@@ -1253,6 +1272,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
textAlign: textAlign,
textDirection: textDirection,
background: cumulativeStyle._background,
placeholderCount: placeholderCount,
);
}
......@@ -1307,6 +1327,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
textAlign: textAlign,
textDirection: textDirection,
background: cumulativeStyle._background,
placeholderCount: placeholderCount,
);
}
......@@ -1315,6 +1336,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
final List<dynamic> elementStack = <dynamic>[];
dynamic currentElement() =>
elementStack.isNotEmpty ? elementStack.last : _paragraphElement;
for (int i = 0; i < _ops.length; i++) {
final dynamic op = _ops[i];
if (op is EngineTextStyle) {
......@@ -1327,6 +1349,11 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
elementStack.add(span);
} else if (op is String) {
domRenderer.appendText(currentElement(), op);
} else if (op is ParagraphPlaceholder) {
domRenderer.append(
currentElement(),
_createPlaceholderElement(placeholder: op),
);
} else if (identical(op, _paragraphBuilderPop)) {
elementStack.removeLast();
} else {
......@@ -1350,10 +1377,42 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
textAlign: _paragraphStyle._effectiveTextAlign,
textDirection: _paragraphStyle._effectiveTextDirection,
background: null,
placeholderCount: placeholderCount,
);
}
}
/// Holds information for a placeholder in a paragraph.
///
/// [width], [height] and [baselineOffset] are expected to be already scaled.
class ParagraphPlaceholder {
ParagraphPlaceholder(
this.width,
this.height,
this.alignment, {
required this.baselineOffset,
required this.baseline,
});
/// The scaled width of the placeholder.
final double width;
/// The scaled height of the placeholder.
final double height;
/// Specifies how the placeholder rectangle will be vertically aligned with
/// the surrounding text.
final ui.PlaceholderAlignment alignment;
/// When the [alignment] value is [ui.PlaceholderAlignment.baseline], the
/// [baselineOffset] indicates the distance from the baseline to the top of
/// the placeholder rectangle.
final double baselineOffset;
/// Dictates whether to use alphabetic or ideographic baseline.
final ui.TextBaseline baseline;
}
/// Converts [fontWeight] to its CSS equivalent value.
String? fontWeightToCss(ui.FontWeight? fontWeight) {
if (fontWeight == null) {
......@@ -1568,6 +1627,52 @@ void _applyTextStyleToElement({
}
}
html.Element _createPlaceholderElement({
required ParagraphPlaceholder placeholder,
}) {
final html.Element element = domRenderer.createElement('span');
element.className = _placeholderClass;
final html.CssStyleDeclaration style = element.style;
style
..display = 'inline-block'
..width = '${placeholder.width}px'
..height = '${placeholder.height}px'
..verticalAlign = _placeholderAlignmentToCssVerticalAlign(placeholder);
return element;
}
String _placeholderAlignmentToCssVerticalAlign(
ParagraphPlaceholder placeholder,
) {
// For more details about the vertical-align CSS property, see:
// - https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align
switch (placeholder.alignment) {
case ui.PlaceholderAlignment.top:
return 'top';
case ui.PlaceholderAlignment.middle:
return 'middle';
case ui.PlaceholderAlignment.bottom:
return 'bottom';
case ui.PlaceholderAlignment.aboveBaseline:
return 'baseline';
case ui.PlaceholderAlignment.belowBaseline:
return '-${placeholder.height}px';
case ui.PlaceholderAlignment.baseline:
// In CSS, the placeholder is already placed above the baseline. But
// Flutter's `baselineOffset` assumes the placeholder is placed below the
// baseline. That's why we need to subtract the placeholder's height from
// `baselineOffset`.
final double offset = placeholder.baselineOffset - placeholder.height;
return '${offset}px';
}
}
String _shadowListToCss(List<ui.Shadow> shadows) {
if (shadows.isEmpty) {
return '';
......
......@@ -628,6 +628,31 @@ class ParagraphRuler {
);
}
List<ui.TextBox> measurePlaceholderBoxes() {
assert(!_debugIsDisposed);
assert(_paragraph != null);
if (_paragraph!.placeholderCount == 0) {
return const <ui.TextBox>[];
}
final List<html.Element> placeholderElements =
constrainedDimensions._element.querySelectorAll('.$_placeholderClass');
final List<ui.TextBox> boxes = <ui.TextBox>[];
for (final html.Element element in placeholderElements) {
final html.Rectangle<num> rect = element.getBoundingClientRect();
boxes.add(ui.TextBox.fromLTRBD(
rect.left as double,
rect.top as double,
rect.right as double,
rect.bottom as double,
_paragraph!._textDirection,
));
}
return boxes;
}
/// Returns text position in a paragraph that contains multiple
/// nested spans given an offset.
int hitTest(ui.ParagraphConstraints constraints, ui.Offset offset) {
......@@ -905,6 +930,8 @@ class MeasurementResult {
/// of each laid out line.
final List<EngineLineMetrics>? lines;
final List<ui.TextBox> placeholderBoxes;
/// The text align value of the paragraph.
final ui.TextAlign textAlign;
......@@ -923,6 +950,7 @@ class MeasurementResult {
required this.alphabeticBaseline,
required this.ideographicBaseline,
required this.lines,
required this.placeholderBoxes,
required ui.TextAlign? textAlign,
required ui.TextDirection? textDirection,
}) : assert(constraintWidth != null), // ignore: unnecessary_null_comparison
......
// 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 'package:image/image.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'scuba.dart';
typedef PaintTest = void Function(RecordingCanvas recordingCanvas);
void main() async {
final EngineScubaTester scuba = await EngineScubaTester.initialize(
viewportSize: const Size(600, 600),
);
setUpStableTestFonts();
testEachCanvas('draws paragraphs with placeholders', (EngineCanvas canvas) {
final Rect screenRect = const Rect.fromLTWH(0, 0, 600, 600);
final RecordingCanvas recordingCanvas = RecordingCanvas(screenRect);
Offset offset = Offset.zero;
for (PlaceholderAlignment alignment in PlaceholderAlignment.values) {
_paintTextWithPlaceholder(recordingCanvas, offset, alignment);
offset = offset.translate(0.0, 80.0);
}
recordingCanvas.endRecording();
recordingCanvas.apply(canvas, screenRect);
return scuba.diffCanvasScreenshot(canvas, 'text_with_placeholders');
}, bSkipHoudini: true);
}
const Color black = Color(0xFF000000);
const Color blue = Color(0xFF0000FF);
const Color red = Color(0xFFFF0000);
const Size placeholderSize = Size(80.0, 50.0);
void _paintTextWithPlaceholder(
RecordingCanvas canvas,
Offset offset,
PlaceholderAlignment alignment,
) {
// First let's draw the paragraph.
final Paragraph paragraph = _createParagraphWithPlaceholder(alignment);
canvas.drawParagraph(paragraph, offset);
// Then fill the placeholders.
final TextBox placeholderBox = paragraph.getBoxesForPlaceholders().single;
canvas.drawRect(
placeholderBox.toRect().shift(offset),
Paint()..color = red,
);
}
Paragraph _createParagraphWithPlaceholder(PlaceholderAlignment alignment) {
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle());
builder
.pushStyle(TextStyle(color: black, fontFamily: 'Roboto', fontSize: 14));
builder.addText('Lorem ipsum');
builder.addPlaceholder(
placeholderSize.width,
placeholderSize.height,
alignment,
baselineOffset: 40.0,
baseline: TextBaseline.alphabetic,
);
builder.pushStyle(TextStyle(color: blue, fontFamily: 'Roboto', fontSize: 14));
builder.addText('dolor sit amet, consectetur.');
return builder.build()..layout(ParagraphConstraints(width: 200.0));
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册