Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit ed50eca

Browse files
author
Harry Terkelsen
authored
Use runes to get code units in CanvasKit. (#24024)
1 parent b87307c commit ed50eca

File tree

5 files changed

+141
-64
lines changed

5 files changed

+141
-64
lines changed

lib/web_ui/dev/goldens_lock.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
repository: https://github.com/flutter/goldens.git
2-
revision: d70eca62b254302b293973573d3b16ffd05b19db
2+
revision: 44f00682eee2afd7042c02ce802199c1c4ff223e

lib/web_ui/dev/test_runner.dart

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,7 @@ class TestCommand extends Command<bool> with ArgUtils {
9292
'fetch-goldens-repo',
9393
defaultsTo: true,
9494
negatable: true,
95-
help:
96-
'Whether to fetch the goldens repo. Set this to false to iterate '
95+
help: 'Whether to fetch the goldens repo. Set this to false to iterate '
9796
'on golden tests without fearing that the fetcher will overwrite '
9897
'your local changes.',
9998
)
@@ -174,39 +173,41 @@ class TestCommand extends Command<bool> with ArgUtils {
174173
final FilePath dir = FilePath.fromWebUi('');
175174
print('');
176175
print('Initial test run is done!');
177-
print('Watching ${dir.relativeToCwd}/lib and ${dir.relativeToCwd}/test to re-run tests');
176+
print(
177+
'Watching ${dir.relativeToCwd}/lib and ${dir.relativeToCwd}/test to re-run tests');
178178
print('');
179179
PipelineWatcher(
180-
dir: dir.absolute,
181-
pipeline: testPipeline,
182-
ignore: (event) {
183-
// Ignore font files that are copied whenever tests run.
184-
if (event.path.endsWith('.ttf')) {
185-
return true;
186-
}
180+
dir: dir.absolute,
181+
pipeline: testPipeline,
182+
ignore: (event) {
183+
// Ignore font files that are copied whenever tests run.
184+
if (event.path.endsWith('.ttf')) {
185+
return true;
186+
}
187187

188-
// Ignore auto-generated JS files.
189-
// The reason we are using `.contains()` instead of `.endsWith()` is
190-
// because the auto-generated files could end with any of the
191-
// following:
192-
//
193-
// - browser_test.dart.js
194-
// - browser_test.dart.js.map
195-
// - browser_test.dart.js.deps
196-
if (event.path.contains('browser_test.dart.js')) {
197-
return true;
198-
}
188+
// Ignore auto-generated JS files.
189+
// The reason we are using `.contains()` instead of `.endsWith()` is
190+
// because the auto-generated files could end with any of the
191+
// following:
192+
//
193+
// - browser_test.dart.js
194+
// - browser_test.dart.js.map
195+
// - browser_test.dart.js.deps
196+
if (event.path.contains('browser_test.dart.js')) {
197+
return true;
198+
}
199199

200-
// React to changes in lib/ and test/ folders.
201-
final String relativePath = path.relative(event.path, from: dir.absolute);
202-
if (relativePath.startsWith('lib/') || relativePath.startsWith('test/')) {
203-
return false;
204-
}
200+
// React to changes in lib/ and test/ folders.
201+
final String relativePath =
202+
path.relative(event.path, from: dir.absolute);
203+
if (relativePath.startsWith('lib/') ||
204+
relativePath.startsWith('test/')) {
205+
return false;
206+
}
205207

206-
// Ignore anything else.
207-
return true;
208-
}
209-
).start();
208+
// Ignore anything else.
209+
return true;
210+
}).start();
210211
// Return a never-ending future.
211212
return Completer<bool>().future;
212213
} else {
@@ -226,15 +227,17 @@ class TestCommand extends Command<bool> with ArgUtils {
226227
bool unitTestResult = await runUnitTests();
227228
bool integrationTestResult = await runIntegrationTests();
228229
if (integrationTestResult != unitTestResult) {
229-
print('Tests run. Integration tests passed: $integrationTestResult '
230+
print(
231+
'Tests run. Integration tests passed: $integrationTestResult '
230232
'unit tests passed: $unitTestResult');
231233
}
232234
return integrationTestResult && unitTestResult;
233235
} else {
234236
return await runUnitTests();
235237
}
236238
}
237-
throw UnimplementedError('Unknown test type requested: $testTypesRequested');
239+
throw UnimplementedError(
240+
'Unknown test type requested: $testTypesRequested');
238241
} on TestFailureException {
239242
return true;
240243
}
@@ -786,6 +789,7 @@ const List<String> _kTestFonts = <String>[
786789
'ahem.ttf',
787790
'Roboto-Regular.ttf',
788791
'NotoNaskhArabic-Regular.ttf',
792+
'NotoColorEmoji.ttf',
789793
];
790794

791795
void _copyTestFontsIntoWebUi() {

lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -211,15 +211,19 @@ Future<void> _registerSymbolsAndEmoji() async {
211211
String? symbolsFontUrl = extractUrlFromCss(symbolsCss);
212212
String? emojiFontUrl = extractUrlFromCss(emojiCss);
213213

214-
if (symbolsFontUrl == null || emojiFontUrl == null) {
215-
html.window.console
216-
.warn('Error parsing CSS for Noto Emoji and Symbols font.');
214+
if (symbolsFontUrl != null) {
215+
notoDownloadQueue.add(_ResolvedNotoSubset(
216+
symbolsFontUrl, 'Noto Sans Symbols', const <CodeunitRange>[]));
217+
} else {
218+
html.window.console.warn('Error parsing CSS for Noto Symbols font.');
217219
}
218220

219-
notoDownloadQueue.add(_ResolvedNotoSubset(
220-
symbolsFontUrl!, 'Noto Sans Symbols', const <CodeunitRange>[]));
221-
notoDownloadQueue.add(_ResolvedNotoSubset(
222-
emojiFontUrl!, 'Noto Color Emoji Compat', const <CodeunitRange>[]));
221+
if (emojiFontUrl != null) {
222+
notoDownloadQueue.add(_ResolvedNotoSubset(
223+
emojiFontUrl, 'Noto Color Emoji Compat', const <CodeunitRange>[]));
224+
} else {
225+
html.window.console.warn('Error parsing CSS for Noto Emoji font.');
226+
}
223227
}
224228

225229
/// Finds the minimum set of fonts which covers all of the [codeunits].
@@ -695,9 +699,10 @@ class NotoDownloader {
695699
if (assertionsEnabled) {
696700
_debugActiveDownloadCount += 1;
697701
}
698-
final Future<ByteBuffer> result = html.window.fetch(url).then((dynamic fetchResult) => fetchResult
699-
.arrayBuffer()
700-
.then<ByteBuffer>((dynamic x) => x as ByteBuffer));
702+
final Future<ByteBuffer> result = html.window.fetch(url).then(
703+
(dynamic fetchResult) => fetchResult
704+
.arrayBuffer()
705+
.then<ByteBuffer>((dynamic x) => x as ByteBuffer));
701706
if (assertionsEnabled) {
702707
result.whenComplete(() {
703708
_debugActiveDownloadCount -= 1;
@@ -713,8 +718,9 @@ class NotoDownloader {
713718
if (assertionsEnabled) {
714719
_debugActiveDownloadCount += 1;
715720
}
716-
final Future<String> result = html.window.fetch(url).then((dynamic response) =>
717-
response.text().then<String>((dynamic x) => x as String));
721+
final Future<String> result = html.window.fetch(url).then(
722+
(dynamic response) =>
723+
response.text().then<String>((dynamic x) => x as String));
718724
if (assertionsEnabled) {
719725
result.whenComplete(() {
720726
_debugActiveDownloadCount -= 1;

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

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class CkParagraphStyle implements ui.ParagraphStyle {
2020
ui.StrutStyle? strutStyle,
2121
String? ellipsis,
2222
ui.Locale? locale,
23-
}) : skParagraphStyle = toSkParagraphStyle(
23+
}) : skParagraphStyle = toSkParagraphStyle(
2424
textAlign,
2525
textDirection,
2626
maxLines,
@@ -34,11 +34,11 @@ class CkParagraphStyle implements ui.ParagraphStyle {
3434
ellipsis,
3535
locale,
3636
),
37-
_textDirection = textDirection ?? ui.TextDirection.ltr,
38-
_fontFamily = fontFamily,
39-
_fontSize = fontSize,
40-
_fontWeight = fontWeight,
41-
_fontStyle = fontStyle;
37+
_textDirection = textDirection ?? ui.TextDirection.ltr,
38+
_fontFamily = fontFamily,
39+
_fontSize = fontSize,
40+
_fontWeight = fontWeight,
41+
_fontStyle = fontStyle;
4242

4343
final SkParagraphStyle skParagraphStyle;
4444
final ui.TextDirection? _textDirection;
@@ -276,13 +276,14 @@ class CkTextStyle implements ui.TextStyle {
276276
}
277277

278278
/// Lazy-initialized list of font families sent to Skia.
279-
late final List<String> effectiveFontFamilies = _getEffectiveFontFamilies(fontFamily, fontFamilyFallback);
279+
late final List<String> effectiveFontFamilies =
280+
_getEffectiveFontFamilies(fontFamily, fontFamilyFallback);
280281

281282
/// Lazy-initialized Skia style used to pass the style to Skia.
282283
///
283284
/// This is lazy because not every style ends up being passed to Skia, so the
284285
/// conversion would be wasteful.
285-
late final SkTextStyle skTextStyle = () {
286+
late final SkTextStyle skTextStyle = () {
286287
// Write field values to locals so null checks promote types to non-null.
287288
final ui.Color? color = this.color;
288289
final ui.TextDecoration? decoration = this.decoration;
@@ -695,22 +696,22 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
695696
typefaces.addAll(typefacesForFamily);
696697
}
697698
}
698-
List<bool> codeUnitsSupported = List<bool>.filled(text.length, false);
699+
List<int> codeUnits = text.runes.toList();
700+
List<bool> codeUnitsSupported = List<bool>.filled(codeUnits.length, false);
699701
for (SkTypeface typeface in typefaces) {
700702
SkFont font = SkFont(typeface);
701703
Uint8List glyphs = font.getGlyphIDs(text);
702704
assert(glyphs.length == codeUnitsSupported.length);
703705
for (int i = 0; i < glyphs.length; i++) {
704-
codeUnitsSupported[i] |=
705-
glyphs[i] != 0 || _isControlCode(text.codeUnitAt(i));
706+
codeUnitsSupported[i] |= glyphs[i] != 0 || _isControlCode(codeUnits[i]);
706707
}
707708
}
708709

709710
if (codeUnitsSupported.any((x) => !x)) {
710711
List<int> missingCodeUnits = <int>[];
711712
for (int i = 0; i < codeUnitsSupported.length; i++) {
712713
if (!codeUnitsSupported[i]) {
713-
missingCodeUnits.add(text.codeUnitAt(i));
714+
missingCodeUnits.add(codeUnits[i]);
714715
}
715716
}
716717
_findFontsForMissingCodeunits(missingCodeUnits);
@@ -778,7 +779,8 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
778779
// This object is never deleted. It is effectively a static global constant.
779780
// Therefore it doesn't need to be wrapped in CkPaint.
780781
static final SkPaint _defaultTextForeground = SkPaint();
781-
static final SkPaint _defaultTextBackground = SkPaint()..setColorInt(0x00000000);
782+
static final SkPaint _defaultTextBackground = SkPaint()
783+
..setColorInt(0x00000000);
782784

783785
@override
784786
void pushStyle(ui.TextStyle style) {
@@ -796,7 +798,8 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
796798
foreground = _defaultTextForeground;
797799
}
798800

799-
final SkPaint background = skStyle.background?.skiaObject ?? _defaultTextBackground;
801+
final SkPaint background =
802+
skStyle.background?.skiaObject ?? _defaultTextBackground;
800803
_paragraphBuilder.pushPaintStyle(
801804
skStyle.skTextStyle, foreground, background);
802805
} else {

lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ void main() {
1919
internalBootstrapBrowserTest(() => testMain);
2020
}
2121

22-
const ui.Rect kDefaultRegion = const ui.Rect.fromLTRB(0, 0, 500, 250);
22+
const ui.Rect kDefaultRegion = const ui.Rect.fromLTRB(0, 0, 100, 50);
2323

2424
Future<void> matchPictureGolden(String goldenFile, CkPicture picture,
2525
{ui.Rect region = kDefaultRegion, bool write = false}) async {
@@ -105,22 +105,86 @@ void testMain() {
105105
final CkCanvas canvas = recorder.beginRecording(kDefaultRegion);
106106

107107
pb = CkParagraphBuilder(
108-
CkParagraphStyle(
109-
fontSize: 32,
110-
),
108+
CkParagraphStyle(),
111109
);
110+
pb.pushStyle(ui.TextStyle(fontSize: 32));
112111
pb.addText('مرحبا');
112+
pb.pop();
113113
final CkParagraph paragraph = pb.build();
114114
paragraph.layout(ui.ParagraphConstraints(width: 1000));
115115

116-
canvas.drawParagraph(paragraph, ui.Offset(200, 120));
116+
canvas.drawParagraph(paragraph, ui.Offset(0, 0));
117117

118118
await matchPictureGolden(
119119
'canvaskit_font_fallback_arabic.png', recorder.endRecording());
120120
// TODO: https://github.com/flutter/flutter/issues/60040
121121
// TODO: https://github.com/flutter/flutter/issues/71520
122122
}, skip: isIosSafari || isFirefox);
123123

124+
test('will download Noto Emojis and Noto Symbols if no matching Noto Font',
125+
() async {
126+
final Completer<void> fontChangeCompleter = Completer<void>();
127+
// Intercept the system font change message.
128+
ui.window.onPlatformMessage = (String name, ByteData? data,
129+
ui.PlatformMessageResponseCallback? callback) {
130+
if (name == 'flutter/system') {
131+
const JSONMessageCodec codec = JSONMessageCodec();
132+
final dynamic message = codec.decodeMessage(data);
133+
if (message is Map) {
134+
if (message['type'] == 'fontsChange') {
135+
fontChangeCompleter.complete();
136+
}
137+
}
138+
}
139+
if (savedCallback != null) {
140+
savedCallback!(name, data, callback);
141+
}
142+
};
143+
144+
TestDownloader.mockDownloads[
145+
'https://fonts.googleapis.com/css2?family=Noto+Color+Emoji+Compat'] =
146+
'''
147+
/* arabic */
148+
@font-face {
149+
font-family: 'Noto Color Emoji';
150+
src: url(packages/ui/assets/NotoColorEmoji.ttf) format('ttf');
151+
}
152+
''';
153+
154+
expect(skiaFontCollection.globalFontFallbacks, ['Roboto']);
155+
156+
// Creating this paragraph should cause us to start to download the
157+
// fallback font.
158+
CkParagraphBuilder pb = CkParagraphBuilder(
159+
CkParagraphStyle(),
160+
);
161+
pb.addText('Hello 😊');
162+
163+
await fontChangeCompleter.future;
164+
165+
expect(skiaFontCollection.globalFontFallbacks,
166+
contains('Noto Color Emoji Compat 0'));
167+
168+
final CkPictureRecorder recorder = CkPictureRecorder();
169+
final CkCanvas canvas = recorder.beginRecording(kDefaultRegion);
170+
171+
pb = CkParagraphBuilder(
172+
CkParagraphStyle(),
173+
);
174+
pb.pushStyle(ui.TextStyle(fontSize: 26));
175+
pb.addText('Hello 😊');
176+
pb.pop();
177+
final CkParagraph paragraph = pb.build();
178+
paragraph.layout(ui.ParagraphConstraints(width: 1000));
179+
180+
canvas.drawParagraph(paragraph, ui.Offset(0, 0));
181+
182+
await matchPictureGolden(
183+
'canvaskit_font_fallback_emoji.png', recorder.endRecording());
184+
// TODO: https://github.com/flutter/flutter/issues/60040
185+
// TODO: https://github.com/flutter/flutter/issues/71520
186+
}, skip: isIosSafari || isFirefox);
187+
124188
test('will gracefully fail if we cannot parse the Google Fonts CSS',
125189
() async {
126190
TestDownloader.mockDownloads[

0 commit comments

Comments
 (0)