diff --git a/lib/web_ui/lib/src/engine/text/paragraph.dart b/lib/web_ui/lib/src/engine/text/paragraph.dart index 7b6faaf3ed20919bb20fefbbac9c9bda76d66cd4..98b62127dfe4742a860b01d648578be7391c840e 100644 --- a/lib/web_ui/lib/src/engine/text/paragraph.dart +++ b/lib/web_ui/lib/src/engine/text/paragraph.dart @@ -18,6 +18,7 @@ class EngineParagraph implements ui.Paragraph { @required ui.TextAlign textAlign, @required ui.TextDirection textDirection, @required ui.Paint background, + @required List 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 _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 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 _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 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 shadows) { + if (shadows.isEmpty) { + return ''; + } + // CSS text-shadow is a comma separated list of shadows. + // . + // 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({ diff --git a/lib/web_ui/lib/src/engine/text/ruler.dart b/lib/web_ui/lib/src/engine/text/ruler.dart index e8c1944aff59cbe091fbe4b9f18c0a5fdd39e4b9..58d180e593848abe4c8f02a452777112a333b0d0 100644 --- a/lib/web_ui/lib/src/engine/text/ruler.dart +++ b/lib/web_ui/lib/src/engine/text/ruler.dart @@ -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 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 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 shadowList = style.shadows; + if (shadowList != null) { + _element.style.textShadow = _shadowListToCss(shadowList); + } _invalidateBoundsCache(); } @@ -765,7 +777,7 @@ class ParagraphRuler { return null; } final List constraintCache = - _measurementCache[plainText]; + _measurementCache[plainText]; if (constraintCache == null) { return null; } diff --git a/lib/web_ui/lib/src/ui/text.dart b/lib/web_ui/lib/src/ui/text.dart index d003c7b6eda65d6a1a87cb2dc3ae0ee0f9c458c5..f4a798ef4cbc45800029033834b0365fa43ea38f 100644 --- a/lib/web_ui/lib/src/ui/text.dart +++ b/lib/web_ui/lib/src/ui/text.dart @@ -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)'; diff --git a/lib/web_ui/test/golden_tests/engine/scuba.dart b/lib/web_ui/test/golden_tests/engine/scuba.dart index 598daa11a5c3c71cb8e376f5c2e5325842ee0da0..18dff26fd7af91a4487dbdd09e36d3736e61df8e 100644 --- a/lib/web_ui/test/golden_tests/engine/scuba.dart +++ b/lib/web_ui/test/golden_tests/engine/scuba.dart @@ -39,18 +39,18 @@ class EngineScubaTester { return EngineScubaTester(viewportSize); } - Future diffScreenshot(String fileName) async { - await matchGoldenFile('$fileName.png', region: ui.Rect.fromLTWH(0, 0, viewportSize.width, viewportSize.height)); + Future 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 diffCanvasScreenshot( - EngineCanvas canvas, - String fileName, - ) async { + Future diffCanvasScreenshot(EngineCanvas canvas, String fileName, + {double maxDiffRate}) async { // Wrap in 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 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( diff --git a/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart b/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart index b0c10abf83f57a50a5b081b5f8930afb7c9f56b3..04742ad3f92f2afd3c409e49a7d163bfa0f5cd4a 100644 --- a/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/text_style_golden_test.dart @@ -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( + 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( + 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); }