Skip to content

Commit 679a436

Browse files
authored
[web] Implement TextStyle.shadows (flutter#13769)
* Add shadows to Engine classes * add text shadow test * update golden locks file, update ui.ParagraphStyle, fix issues * Change maxDiffRate for mac clients
1 parent e19ee72 commit 679a436

File tree

5 files changed

+150
-32
lines changed

5 files changed

+150
-32
lines changed

lib/web_ui/lib/src/engine/text/paragraph.dart

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class EngineParagraph implements ui.Paragraph {
1818
@required ui.TextAlign textAlign,
1919
@required ui.TextDirection textDirection,
2020
@required ui.Paint background,
21+
@required List<ui.Shadow> shadows,
2122
}) : assert((plainText == null && paint == null) ||
2223
(plainText != null && paint != null)),
2324
_paragraphElement = paragraphElement,
@@ -26,7 +27,8 @@ class EngineParagraph implements ui.Paragraph {
2627
_textAlign = textAlign,
2728
_textDirection = textDirection,
2829
_paint = paint,
29-
_background = background;
30+
_background = background,
31+
_shadows = shadows;
3032

3133
final html.HtmlElement _paragraphElement;
3234
final ParagraphGeometricStyle _geometricStyle;
@@ -35,6 +37,7 @@ class EngineParagraph implements ui.Paragraph {
3537
final ui.TextAlign _textAlign;
3638
final ui.TextDirection _textDirection;
3739
final ui.Paint _background;
40+
final List<ui.Shadow> _shadows;
3841

3942
@visibleForTesting
4043
String get plainText => _plainText;
@@ -287,7 +290,8 @@ class EngineParagraph implements ui.Paragraph {
287290
return ui.TextRange(start: textPosition.offset, end: textPosition.offset);
288291
}
289292

290-
final int start = WordBreaker.prevBreakIndex(_plainText, textPosition.offset);
293+
final int start =
294+
WordBreaker.prevBreakIndex(_plainText, textPosition.offset);
291295
final int end = WordBreaker.nextBreakIndex(_plainText, textPosition.offset);
292296
return ui.TextRange(start: start, end: end);
293297
}
@@ -321,6 +325,7 @@ class EngineParagraphStyle implements ui.ParagraphStyle {
321325
ui.StrutStyle strutStyle,
322326
String ellipsis,
323327
ui.Locale locale,
328+
List<ui.Shadow> shadows,
324329
}) : _textAlign = textAlign,
325330
_textDirection = textDirection,
326331
_fontWeight = fontWeight,
@@ -332,7 +337,8 @@ class EngineParagraphStyle implements ui.ParagraphStyle {
332337
// TODO(b/128317744): add support for strut style.
333338
_strutStyle = strutStyle,
334339
_ellipsis = ellipsis,
335-
_locale = locale;
340+
_locale = locale,
341+
_shadows = shadows;
336342

337343
final ui.TextAlign _textAlign;
338344
final ui.TextDirection _textDirection;
@@ -345,6 +351,7 @@ class EngineParagraphStyle implements ui.ParagraphStyle {
345351
final EngineStrutStyle _strutStyle;
346352
final String _ellipsis;
347353
final ui.Locale _locale;
354+
final List<ui.Shadow> _shadows;
348355

349356
String get _effectiveFontFamily {
350357
if (assertionsEnabled) {
@@ -413,6 +420,7 @@ class EngineParagraphStyle implements ui.ParagraphStyle {
413420
'height: ${_height != null ? "${_height.toStringAsFixed(1)}x" : "unspecified"}, '
414421
'ellipsis: ${_ellipsis != null ? "\"$_ellipsis\"" : "unspecified"}, '
415422
'locale: ${_locale ?? "unspecified"}'
423+
'shadows: ${_shadows ?? "unspecified"}'
416424
')';
417425
} else {
418426
return super.toString();
@@ -798,6 +806,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
798806
ui.Locale locale = _paragraphStyle._locale;
799807
ui.Paint background;
800808
ui.Paint foreground;
809+
List<ui.Shadow> shadows;
801810

802811
int i = 0;
803812

@@ -852,6 +861,9 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
852861
if (style._foreground != null) {
853862
foreground = style._foreground;
854863
}
864+
if (style._shadows != null) {
865+
shadows = style._shadows;
866+
}
855867
i++;
856868
}
857869

@@ -871,6 +883,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
871883
locale: locale,
872884
background: background,
873885
foreground: foreground,
886+
shadows: shadows,
874887
);
875888

876889
ui.Paint paint;
@@ -900,6 +913,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
900913
wordSpacing: wordSpacing,
901914
decoration: _textDecorationToCssString(decoration, decorationStyle),
902915
ellipsis: _paragraphStyle._ellipsis,
916+
shadows: shadows,
903917
),
904918
plainText: '',
905919
paint: paint,
@@ -953,6 +967,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
953967
wordSpacing: wordSpacing,
954968
decoration: _textDecorationToCssString(decoration, decorationStyle),
955969
ellipsis: _paragraphStyle._ellipsis,
970+
shadows: shadows,
956971
),
957972
plainText: plainText,
958973
paint: paint,
@@ -996,6 +1011,7 @@ class EngineParagraphBuilder implements ui.ParagraphBuilder {
9961011
lineHeight: _paragraphStyle._height,
9971012
maxLines: _paragraphStyle._maxLines,
9981013
ellipsis: _paragraphStyle._ellipsis,
1014+
shadows: _paragraphStyle._shadows,
9991015
),
10001016
plainText: null,
10011017
paint: null,
@@ -1082,6 +1098,9 @@ void _applyParagraphStyleToElement({
10821098
if (style._effectiveFontFamily != null) {
10831099
cssStyle.fontFamily = canonicalizeFontFamily(style._effectiveFontFamily);
10841100
}
1101+
if (style._shadows != null) {
1102+
cssStyle.textShadow = _shadowListToCss(style._shadows);
1103+
}
10851104
} else {
10861105
if (style._textAlign != previousStyle._textAlign) {
10871106
cssStyle.textAlign = textAlignToCssValue(
@@ -1108,6 +1127,9 @@ void _applyParagraphStyleToElement({
11081127
if (style._fontFamily != previousStyle._fontFamily) {
11091128
cssStyle.fontFamily = canonicalizeFontFamily(style._fontFamily);
11101129
}
1130+
if (style._shadows != previousStyle._shadows) {
1131+
cssStyle.textShadow = _shadowListToCss(style._shadows);
1132+
}
11111133
}
11121134
}
11131135

@@ -1150,7 +1172,8 @@ void _applyTextStyleToElement({
11501172
}
11511173
} else {
11521174
if (style._effectiveFontFamily != null) {
1153-
cssStyle.fontFamily = canonicalizeFontFamily(style._effectiveFontFamily);
1175+
cssStyle.fontFamily =
1176+
canonicalizeFontFamily(style._effectiveFontFamily);
11541177
}
11551178
}
11561179
if (style._letterSpacing != null) {
@@ -1162,6 +1185,9 @@ void _applyTextStyleToElement({
11621185
if (style._decoration != null) {
11631186
updateDecoration = true;
11641187
}
1188+
if (style._shadows != null) {
1189+
cssStyle.textShadow = _shadowListToCss(style._shadows);
1190+
}
11651191
} else {
11661192
if (style._color != previousStyle._color ||
11671193
style._foreground != previousStyle._foreground) {
@@ -1197,6 +1223,9 @@ void _applyTextStyleToElement({
11971223
style._decorationColor != previousStyle._decorationColor) {
11981224
updateDecoration = true;
11991225
}
1226+
if (style._shadows != previousStyle._shadows) {
1227+
cssStyle.textShadow = _shadowListToCss(style._shadows);
1228+
}
12001229
}
12011230

12021231
if (updateDecoration) {
@@ -1214,6 +1243,27 @@ void _applyTextStyleToElement({
12141243
}
12151244
}
12161245

1246+
String _shadowListToCss(List<ui.Shadow> shadows) {
1247+
if (shadows.isEmpty) {
1248+
return '';
1249+
}
1250+
// CSS text-shadow is a comma separated list of shadows.
1251+
// <offsetx> <offsety> <blur-radius> <color>.
1252+
// Shadows are applied front-to-back with first shadow on top.
1253+
// Color is optional. offsetx,y are required. blur-radius is optional as well
1254+
// and defaults to 0.
1255+
StringBuffer sb = new StringBuffer();
1256+
for (int i = 0, len = shadows.length; i < len; i++) {
1257+
if (i != 0) {
1258+
sb.write(',');
1259+
}
1260+
ui.Shadow shadow = shadows[i];
1261+
sb.write('${shadow.offset.dx}px ${shadow.offset.dy}px '
1262+
'${shadow.blurRadius}px ${shadow.color.toCssString()}');
1263+
}
1264+
return sb.toString();
1265+
}
1266+
12171267
/// Applies background color properties in text style to paragraph or span
12181268
/// elements.
12191269
void _applyTextBackgroundToElement({

lib/web_ui/lib/src/engine/text/ruler.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class ParagraphGeometricStyle {
1717
this.wordSpacing,
1818
this.decoration,
1919
this.ellipsis,
20+
this.shadows,
2021
});
2122

2223
final ui.FontWeight fontWeight;
@@ -29,6 +30,7 @@ class ParagraphGeometricStyle {
2930
final double wordSpacing;
3031
final String decoration;
3132
final String ellipsis;
33+
final List<ui.Shadow> shadows;
3234

3335
// Since all fields above are primitives, cache hashcode since ruler lookups
3436
// use this style as key.
@@ -109,7 +111,8 @@ class ParagraphGeometricStyle {
109111
letterSpacing == typedOther.letterSpacing &&
110112
wordSpacing == typedOther.wordSpacing &&
111113
decoration == typedOther.decoration &&
112-
ellipsis == typedOther.ellipsis;
114+
ellipsis == typedOther.ellipsis &&
115+
shadows == typedOther.shadows;
113116
}
114117

115118
@override
@@ -124,8 +127,12 @@ class ParagraphGeometricStyle {
124127
wordSpacing,
125128
decoration,
126129
ellipsis,
130+
_hashShadows(shadows),
127131
);
128132

133+
int _hashShadows(List<ui.Shadow> shadows) =>
134+
(shadows == null ? '' : _shadowListToCss(shadows)).hashCode;
135+
129136
@override
130137
String toString() {
131138
if (assertionsEnabled) {
@@ -137,6 +144,7 @@ class ParagraphGeometricStyle {
137144
' wordSpacing: $wordSpacing,'
138145
' decoration: $decoration,'
139146
' ellipsis: $ellipsis,'
147+
' shadows: $shadows,'
140148
')';
141149
} else {
142150
return super.toString();
@@ -241,6 +249,10 @@ class TextDimensions {
241249
if (style.lineHeight != null) {
242250
_element.style.lineHeight = style.lineHeight.toString();
243251
}
252+
final List<ui.Shadow> shadowList = style.shadows;
253+
if (shadowList != null) {
254+
_element.style.textShadow = _shadowListToCss(shadowList);
255+
}
244256
_invalidateBoundsCache();
245257
}
246258

@@ -765,7 +777,7 @@ class ParagraphRuler {
765777
return null;
766778
}
767779
final List<MeasurementResult> constraintCache =
768-
_measurementCache[plainText];
780+
_measurementCache[plainText];
769781
if (constraintCache == null) {
770782
return null;
771783
}

lib/web_ui/lib/src/ui/text.dart

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -918,7 +918,7 @@ class TextRange {
918918
const TextRange({
919919
this.start,
920920
this.end,
921-
}) : assert(start != null && start >= -1),
921+
}) : assert(start != null && start >= -1),
922922
assert(end != null && end >= -1);
923923

924924
/// A text range that starts and ends at offset.
@@ -971,20 +971,17 @@ class TextRange {
971971

972972
@override
973973
bool operator ==(dynamic other) {
974-
if (identical(this, other))
975-
return true;
976-
if (other is! TextRange)
977-
return false;
974+
if (identical(this, other)) return true;
975+
if (other is! TextRange) return false;
978976
final TextRange typedOther = other;
979-
return typedOther.start == start
980-
&& typedOther.end == end;
977+
return typedOther.start == start && typedOther.end == end;
981978
}
982979

983980
@override
984981
int get hashCode => hashValues(
985-
start.hashCode,
986-
end.hashCode,
987-
);
982+
start.hashCode,
983+
end.hashCode,
984+
);
988985

989986
@override
990987
String toString() => 'TextRange(start: $start, end: $end)';

lib/web_ui/test/golden_tests/engine/scuba.dart

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,18 @@ class EngineScubaTester {
3939
return EngineScubaTester(viewportSize);
4040
}
4141

42-
Future<void> diffScreenshot(String fileName) async {
43-
await matchGoldenFile('$fileName.png', region: ui.Rect.fromLTWH(0, 0, viewportSize.width, viewportSize.height));
42+
Future<void> diffScreenshot(String fileName, {double maxDiffRate}) async {
43+
await matchGoldenFile('$fileName.png',
44+
region: ui.Rect.fromLTWH(0, 0, viewportSize.width, viewportSize.height),
45+
maxDiffRate: maxDiffRate);
4446
}
4547

4648
/// Prepares the DOM and inserts all the necessary nodes, then invokes scuba's
4749
/// screenshot diffing.
4850
///
4951
/// It also cleans up the DOM after itself.
50-
Future<void> diffCanvasScreenshot(
51-
EngineCanvas canvas,
52-
String fileName,
53-
) async {
52+
Future<void> diffCanvasScreenshot(EngineCanvas canvas, String fileName,
53+
{double maxDiffRate}) async {
5454
// Wrap in <flt-scene> so that our CSS selectors kick in.
5555
final html.Element sceneElement = html.Element.tag('flt-scene');
5656
try {
@@ -60,7 +60,7 @@ class EngineScubaTester {
6060
if (TextMeasurementService.enableExperimentalCanvasImplementation) {
6161
screenshotName += '+canvas_measurement';
6262
}
63-
await diffScreenshot(screenshotName);
63+
await diffScreenshot(screenshotName, maxDiffRate: maxDiffRate);
6464
} finally {
6565
// The page is reused across tests, so remove the element after taking the
6666
// Scuba screenshot.
@@ -72,7 +72,8 @@ class EngineScubaTester {
7272
typedef CanvasTest = FutureOr<void> Function(EngineCanvas canvas);
7373

7474
/// Runs the given test [body] with each type of canvas.
75-
void testEachCanvas(String description, CanvasTest body) {
75+
void testEachCanvas(String description, CanvasTest body,
76+
{double maxDiffRate, bool bSkipHoudini = false}) {
7677
const ui.Rect bounds = ui.Rect.fromLTWH(0, 0, 600, 800);
7778
test('$description (bitmap)', () {
7879
try {
@@ -100,14 +101,16 @@ void testEachCanvas(String description, CanvasTest body) {
100101
TextMeasurementService.clearCache();
101102
}
102103
});
103-
test('$description (houdini)', () {
104-
try {
105-
TextMeasurementService.initialize(rulerCacheCapacity: 2);
106-
return body(HoudiniCanvas(bounds));
107-
} finally {
108-
TextMeasurementService.clearCache();
109-
}
110-
});
104+
if (!bSkipHoudini) {
105+
test('$description (houdini)', () {
106+
try {
107+
TextMeasurementService.initialize(rulerCacheCapacity: 2);
108+
return body(HoudiniCanvas(bounds));
109+
} finally {
110+
TextMeasurementService.clearCache();
111+
}
112+
});
113+
}
111114
}
112115

113116
final ui.TextStyle _defaultTextStyle = ui.TextStyle(

0 commit comments

Comments
 (0)