mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Implement TextStyle.shadows (flutter/engine#13769)
* Add shadows to Engine classes * add text shadow test * update golden locks file, update ui.ParagraphStyle, fix issues * Change maxDiffRate for mac clients
This commit is contained in:
parent
6bf82e4675
commit
054875d7ab
@ -18,6 +18,7 @@ class EngineParagraph implements ui.Paragraph {
|
||||
@required ui.TextAlign textAlign,
|
||||
@required ui.TextDirection textDirection,
|
||||
@required ui.Paint background,
|
||||
@required List<ui.Shadow> shadows,
|
||||
}) : assert((plainText == null && paint == null) ||
|
||||
(plainText != null && paint != null)),
|
||||
_paragraphElement = paragraphElement,
|
||||
@ -26,7 +27,8 @@ class EngineParagraph implements ui.Paragraph {
|
||||
_textAlign = textAlign,
|
||||
_textDirection = textDirection,
|
||||
_paint = paint,
|
||||
_background = background;
|
||||
_background = background,
|
||||
_shadows = shadows;
|
||||
|
||||
final html.HtmlElement _paragraphElement;
|
||||
final ParagraphGeometricStyle _geometricStyle;
|
||||
@ -35,6 +37,7 @@ class EngineParagraph implements ui.Paragraph {
|
||||
final ui.TextAlign _textAlign;
|
||||
final ui.TextDirection _textDirection;
|
||||
final ui.Paint _background;
|
||||
final List<ui.Shadow> _shadows;
|
||||
|
||||
@visibleForTesting
|
||||
String get plainText => _plainText;
|
||||
@ -287,7 +290,8 @@ class EngineParagraph implements ui.Paragraph {
|
||||
return ui.TextRange(start: textPosition.offset, end: textPosition.offset);
|
||||
}
|
||||
|
||||
final int start = WordBreaker.prevBreakIndex(_plainText, textPosition.offset);
|
||||
final int start =
|
||||
WordBreaker.prevBreakIndex(_plainText, textPosition.offset);
|
||||
final int end = WordBreaker.nextBreakIndex(_plainText, textPosition.offset);
|
||||
return ui.TextRange(start: start, end: end);
|
||||
}
|
||||
@ -321,6 +325,7 @@ class EngineParagraphStyle implements ui.ParagraphStyle {
|
||||
ui.StrutStyle strutStyle,
|
||||
String ellipsis,
|
||||
ui.Locale locale,
|
||||
List<ui.Shadow> shadows,
|
||||
}) : _textAlign = textAlign,
|
||||
_textDirection = textDirection,
|
||||
_fontWeight = fontWeight,
|
||||
@ -332,7 +337,8 @@ class EngineParagraphStyle implements ui.ParagraphStyle {
|
||||
// TODO(b/128317744): add support for strut style.
|
||||
_strutStyle = strutStyle,
|
||||
_ellipsis = ellipsis,
|
||||
_locale = locale;
|
||||
_locale = locale,
|
||||
_shadows = shadows;
|
||||
|
||||
final ui.TextAlign _textAlign;
|
||||
final ui.TextDirection _textDirection;
|
||||
@ -345,6 +351,7 @@ class EngineParagraphStyle implements ui.ParagraphStyle {
|
||||
final EngineStrutStyle _strutStyle;
|
||||
final String _ellipsis;
|
||||
final ui.Locale _locale;
|
||||
final List<ui.Shadow> _shadows;
|
||||
|
||||
String get _effectiveFontFamily {
|
||||
if (assertionsEnabled) {
|
||||
@ -413,6 +420,7 @@ class EngineParagraphStyle implements ui.ParagraphStyle {
|
||||
'height: ${_height != null ? "${_height.toStringAsFixed(1)}x" : "unspecified"}, '
|
||||
'ellipsis: ${_ellipsis != null ? "\"$_ellipsis\"" : "unspecified"}, '
|
||||
'locale: ${_locale ?? "unspecified"}'
|
||||
'shadows: ${_shadows ?? "unspecified"}'
|
||||
')';
|
||||
} else {
|
||||
return super.toString();
|
||||
@ -798,6 +806,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
|
||||
ui.Locale locale = _paragraphStyle._locale;
|
||||
ui.Paint background;
|
||||
ui.Paint foreground;
|
||||
List<ui.Shadow> shadows;
|
||||
|
||||
int i = 0;
|
||||
|
||||
@ -852,6 +861,9 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
|
||||
if (style._foreground != null) {
|
||||
foreground = style._foreground;
|
||||
}
|
||||
if (style._shadows != null) {
|
||||
shadows = style._shadows;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
@ -871,6 +883,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
|
||||
locale: locale,
|
||||
background: background,
|
||||
foreground: foreground,
|
||||
shadows: shadows,
|
||||
);
|
||||
|
||||
ui.Paint paint;
|
||||
@ -900,6 +913,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
|
||||
wordSpacing: wordSpacing,
|
||||
decoration: _textDecorationToCssString(decoration, decorationStyle),
|
||||
ellipsis: _paragraphStyle._ellipsis,
|
||||
shadows: shadows,
|
||||
),
|
||||
plainText: '',
|
||||
paint: paint,
|
||||
@ -953,6 +967,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
|
||||
wordSpacing: wordSpacing,
|
||||
decoration: _textDecorationToCssString(decoration, decorationStyle),
|
||||
ellipsis: _paragraphStyle._ellipsis,
|
||||
shadows: shadows,
|
||||
),
|
||||
plainText: plainText,
|
||||
paint: paint,
|
||||
@ -996,6 +1011,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
|
||||
lineHeight: _paragraphStyle._height,
|
||||
maxLines: _paragraphStyle._maxLines,
|
||||
ellipsis: _paragraphStyle._ellipsis,
|
||||
shadows: _paragraphStyle._shadows,
|
||||
),
|
||||
plainText: null,
|
||||
paint: null,
|
||||
@ -1082,6 +1098,9 @@ void _applyParagraphStyleToElement({
|
||||
if (style._effectiveFontFamily != null) {
|
||||
cssStyle.fontFamily = canonicalizeFontFamily(style._effectiveFontFamily);
|
||||
}
|
||||
if (style._shadows != null) {
|
||||
cssStyle.textShadow = _shadowListToCss(style._shadows);
|
||||
}
|
||||
} else {
|
||||
if (style._textAlign != previousStyle._textAlign) {
|
||||
cssStyle.textAlign = textAlignToCssValue(
|
||||
@ -1108,6 +1127,9 @@ void _applyParagraphStyleToElement({
|
||||
if (style._fontFamily != previousStyle._fontFamily) {
|
||||
cssStyle.fontFamily = canonicalizeFontFamily(style._fontFamily);
|
||||
}
|
||||
if (style._shadows != previousStyle._shadows) {
|
||||
cssStyle.textShadow = _shadowListToCss(style._shadows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1150,7 +1172,8 @@ void _applyTextStyleToElement({
|
||||
}
|
||||
} else {
|
||||
if (style._effectiveFontFamily != null) {
|
||||
cssStyle.fontFamily = canonicalizeFontFamily(style._effectiveFontFamily);
|
||||
cssStyle.fontFamily =
|
||||
canonicalizeFontFamily(style._effectiveFontFamily);
|
||||
}
|
||||
}
|
||||
if (style._letterSpacing != null) {
|
||||
@ -1162,6 +1185,9 @@ void _applyTextStyleToElement({
|
||||
if (style._decoration != null) {
|
||||
updateDecoration = true;
|
||||
}
|
||||
if (style._shadows != null) {
|
||||
cssStyle.textShadow = _shadowListToCss(style._shadows);
|
||||
}
|
||||
} else {
|
||||
if (style._color != previousStyle._color ||
|
||||
style._foreground != previousStyle._foreground) {
|
||||
@ -1197,6 +1223,9 @@ void _applyTextStyleToElement({
|
||||
style._decorationColor != previousStyle._decorationColor) {
|
||||
updateDecoration = true;
|
||||
}
|
||||
if (style._shadows != previousStyle._shadows) {
|
||||
cssStyle.textShadow = _shadowListToCss(style._shadows);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateDecoration) {
|
||||
@ -1214,6 +1243,27 @@ void _applyTextStyleToElement({
|
||||
}
|
||||
}
|
||||
|
||||
String _shadowListToCss(List<ui.Shadow> shadows) {
|
||||
if (shadows.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
// CSS text-shadow is a comma separated list of shadows.
|
||||
// <offsetx> <offsety> <blur-radius> <color>.
|
||||
// Shadows are applied front-to-back with first shadow on top.
|
||||
// Color is optional. offsetx,y are required. blur-radius is optional as well
|
||||
// and defaults to 0.
|
||||
StringBuffer sb = new StringBuffer();
|
||||
for (int i = 0, len = shadows.length; i < len; i++) {
|
||||
if (i != 0) {
|
||||
sb.write(',');
|
||||
}
|
||||
ui.Shadow shadow = shadows[i];
|
||||
sb.write('${shadow.offset.dx}px ${shadow.offset.dy}px '
|
||||
'${shadow.blurRadius}px ${shadow.color.toCssString()}');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/// Applies background color properties in text style to paragraph or span
|
||||
/// elements.
|
||||
void _applyTextBackgroundToElement({
|
||||
|
||||
@ -17,6 +17,7 @@ class ParagraphGeometricStyle {
|
||||
this.wordSpacing,
|
||||
this.decoration,
|
||||
this.ellipsis,
|
||||
this.shadows,
|
||||
});
|
||||
|
||||
final ui.FontWeight fontWeight;
|
||||
@ -29,6 +30,7 @@ class ParagraphGeometricStyle {
|
||||
final double wordSpacing;
|
||||
final String decoration;
|
||||
final String ellipsis;
|
||||
final List<ui.Shadow> shadows;
|
||||
|
||||
// Since all fields above are primitives, cache hashcode since ruler lookups
|
||||
// use this style as key.
|
||||
@ -109,7 +111,8 @@ class ParagraphGeometricStyle {
|
||||
letterSpacing == typedOther.letterSpacing &&
|
||||
wordSpacing == typedOther.wordSpacing &&
|
||||
decoration == typedOther.decoration &&
|
||||
ellipsis == typedOther.ellipsis;
|
||||
ellipsis == typedOther.ellipsis &&
|
||||
shadows == typedOther.shadows;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -124,8 +127,12 @@ class ParagraphGeometricStyle {
|
||||
wordSpacing,
|
||||
decoration,
|
||||
ellipsis,
|
||||
_hashShadows(shadows),
|
||||
);
|
||||
|
||||
int _hashShadows(List<ui.Shadow> shadows) =>
|
||||
(shadows == null ? '' : _shadowListToCss(shadows)).hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (assertionsEnabled) {
|
||||
@ -137,6 +144,7 @@ class ParagraphGeometricStyle {
|
||||
' wordSpacing: $wordSpacing,'
|
||||
' decoration: $decoration,'
|
||||
' ellipsis: $ellipsis,'
|
||||
' shadows: $shadows,'
|
||||
')';
|
||||
} else {
|
||||
return super.toString();
|
||||
@ -241,6 +249,10 @@ class TextDimensions {
|
||||
if (style.lineHeight != null) {
|
||||
_element.style.lineHeight = style.lineHeight.toString();
|
||||
}
|
||||
final List<ui.Shadow> shadowList = style.shadows;
|
||||
if (shadowList != null) {
|
||||
_element.style.textShadow = _shadowListToCss(shadowList);
|
||||
}
|
||||
_invalidateBoundsCache();
|
||||
}
|
||||
|
||||
@ -765,7 +777,7 @@ class ParagraphRuler {
|
||||
return null;
|
||||
}
|
||||
final List<MeasurementResult> constraintCache =
|
||||
_measurementCache[plainText];
|
||||
_measurementCache[plainText];
|
||||
if (constraintCache == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -918,7 +918,7 @@ class TextRange {
|
||||
const TextRange({
|
||||
this.start,
|
||||
this.end,
|
||||
}) : assert(start != null && start >= -1),
|
||||
}) : assert(start != null && start >= -1),
|
||||
assert(end != null && end >= -1);
|
||||
|
||||
/// A text range that starts and ends at offset.
|
||||
@ -971,20 +971,17 @@ class TextRange {
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (identical(this, other))
|
||||
return true;
|
||||
if (other is! TextRange)
|
||||
return false;
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! TextRange) return false;
|
||||
final TextRange typedOther = other;
|
||||
return typedOther.start == start
|
||||
&& typedOther.end == end;
|
||||
return typedOther.start == start && typedOther.end == end;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(
|
||||
start.hashCode,
|
||||
end.hashCode,
|
||||
);
|
||||
start.hashCode,
|
||||
end.hashCode,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => 'TextRange(start: $start, end: $end)';
|
||||
|
||||
@ -39,18 +39,18 @@ class EngineScubaTester {
|
||||
return EngineScubaTester(viewportSize);
|
||||
}
|
||||
|
||||
Future<void> diffScreenshot(String fileName) async {
|
||||
await matchGoldenFile('$fileName.png', region: ui.Rect.fromLTWH(0, 0, viewportSize.width, viewportSize.height));
|
||||
Future<void> diffScreenshot(String fileName, {double maxDiffRate}) async {
|
||||
await matchGoldenFile('$fileName.png',
|
||||
region: ui.Rect.fromLTWH(0, 0, viewportSize.width, viewportSize.height),
|
||||
maxDiffRate: maxDiffRate);
|
||||
}
|
||||
|
||||
/// Prepares the DOM and inserts all the necessary nodes, then invokes scuba's
|
||||
/// screenshot diffing.
|
||||
///
|
||||
/// It also cleans up the DOM after itself.
|
||||
Future<void> diffCanvasScreenshot(
|
||||
EngineCanvas canvas,
|
||||
String fileName,
|
||||
) async {
|
||||
Future<void> diffCanvasScreenshot(EngineCanvas canvas, String fileName,
|
||||
{double maxDiffRate}) async {
|
||||
// Wrap in <flt-scene> so that our CSS selectors kick in.
|
||||
final html.Element sceneElement = html.Element.tag('flt-scene');
|
||||
try {
|
||||
@ -60,7 +60,7 @@ class EngineScubaTester {
|
||||
if (TextMeasurementService.enableExperimentalCanvasImplementation) {
|
||||
screenshotName += '+canvas_measurement';
|
||||
}
|
||||
await diffScreenshot(screenshotName);
|
||||
await diffScreenshot(screenshotName, maxDiffRate: maxDiffRate);
|
||||
} finally {
|
||||
// The page is reused across tests, so remove the element after taking the
|
||||
// Scuba screenshot.
|
||||
@ -72,7 +72,8 @@ class EngineScubaTester {
|
||||
typedef CanvasTest = FutureOr<void> Function(EngineCanvas canvas);
|
||||
|
||||
/// Runs the given test [body] with each type of canvas.
|
||||
void testEachCanvas(String description, CanvasTest body) {
|
||||
void testEachCanvas(String description, CanvasTest body,
|
||||
{double maxDiffRate, bool bSkipHoudini = false}) {
|
||||
const ui.Rect bounds = ui.Rect.fromLTWH(0, 0, 600, 800);
|
||||
test('$description (bitmap)', () {
|
||||
try {
|
||||
@ -100,14 +101,16 @@ void testEachCanvas(String description, CanvasTest body) {
|
||||
TextMeasurementService.clearCache();
|
||||
}
|
||||
});
|
||||
test('$description (houdini)', () {
|
||||
try {
|
||||
TextMeasurementService.initialize(rulerCacheCapacity: 2);
|
||||
return body(HoudiniCanvas(bounds));
|
||||
} finally {
|
||||
TextMeasurementService.clearCache();
|
||||
}
|
||||
});
|
||||
if (!bSkipHoudini) {
|
||||
test('$description (houdini)', () {
|
||||
try {
|
||||
TextMeasurementService.initialize(rulerCacheCapacity: 2);
|
||||
return body(HoudiniCanvas(bounds));
|
||||
} finally {
|
||||
TextMeasurementService.clearCache();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final ui.TextStyle _defaultTextStyle = ui.TextStyle(
|
||||
|
||||
@ -158,8 +158,64 @@ void main() async {
|
||||
);
|
||||
}
|
||||
|
||||
void drawTextWithShadow(EngineCanvas canvas) {
|
||||
// Single-line text.
|
||||
canvas.drawParagraph(
|
||||
paragraph(
|
||||
'Hello World',
|
||||
maxWidth: 600,
|
||||
textStyle: TextStyle(
|
||||
color: const Color.fromRGBO(0, 0, 0, 1.0),
|
||||
background: Paint()..color = const Color.fromRGBO(255, 50, 50, 1.0),
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 30,
|
||||
shadows: <Shadow>[
|
||||
Shadow(
|
||||
blurRadius: 0,
|
||||
color: const Color.fromRGBO(255, 0, 255, 1.0),
|
||||
offset: Offset(10, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Offset.zero,
|
||||
);
|
||||
|
||||
// Multi-line text.
|
||||
canvas.drawParagraph(
|
||||
paragraph(
|
||||
'Multi line Hello World paragraph',
|
||||
maxWidth: 200,
|
||||
textStyle: TextStyle(
|
||||
color: const Color.fromRGBO(0, 0, 0, 1.0),
|
||||
background: Paint()..color = const Color.fromRGBO(50, 50, 255, 1.0),
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 30,
|
||||
shadows: <Shadow>[
|
||||
Shadow(
|
||||
blurRadius: 0,
|
||||
color: const Color.fromRGBO(255, 0, 255, 1.0),
|
||||
offset: Offset(10, 5),
|
||||
),
|
||||
Shadow(
|
||||
blurRadius: 0,
|
||||
color: const Color.fromRGBO(0, 255, 255, 1.0),
|
||||
offset: Offset(-10, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Offset(0, 40),
|
||||
);
|
||||
}
|
||||
|
||||
testEachCanvas('draws text with a background', (EngineCanvas canvas) {
|
||||
drawTextWithBackground(canvas);
|
||||
return scuba.diffCanvasScreenshot(canvas, 'text_background');
|
||||
});
|
||||
|
||||
testEachCanvas('draws text with a shadow', (EngineCanvas canvas) {
|
||||
drawTextWithShadow(canvas);
|
||||
return scuba.diffCanvasScreenshot(canvas, 'text_shadow', maxDiffRate: 0.2);
|
||||
}, bSkipHoudini: true);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user