Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/web_ui/dev/goldens_lock.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
repository: https://github.com/flutter/goldens.git
revision: d70eca62b254302b293973573d3b16ffd05b19db
revision: 44f00682eee2afd7042c02ce802199c1c4ff223e
68 changes: 36 additions & 32 deletions lib/web_ui/dev/test_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,7 @@ class TestCommand extends Command<bool> with ArgUtils {
'fetch-goldens-repo',
defaultsTo: true,
negatable: true,
help:
'Whether to fetch the goldens repo. Set this to false to iterate '
help: 'Whether to fetch the goldens repo. Set this to false to iterate '
'on golden tests without fearing that the fetcher will overwrite '
'your local changes.',
)
Expand Down Expand Up @@ -174,39 +173,41 @@ class TestCommand extends Command<bool> with ArgUtils {
final FilePath dir = FilePath.fromWebUi('');
print('');
print('Initial test run is done!');
print('Watching ${dir.relativeToCwd}/lib and ${dir.relativeToCwd}/test to re-run tests');
print(
'Watching ${dir.relativeToCwd}/lib and ${dir.relativeToCwd}/test to re-run tests');
print('');
PipelineWatcher(
dir: dir.absolute,
pipeline: testPipeline,
ignore: (event) {
// Ignore font files that are copied whenever tests run.
if (event.path.endsWith('.ttf')) {
return true;
}
dir: dir.absolute,
pipeline: testPipeline,
ignore: (event) {
// Ignore font files that are copied whenever tests run.
if (event.path.endsWith('.ttf')) {
return true;
}

// Ignore auto-generated JS files.
// The reason we are using `.contains()` instead of `.endsWith()` is
// because the auto-generated files could end with any of the
// following:
//
// - browser_test.dart.js
// - browser_test.dart.js.map
// - browser_test.dart.js.deps
if (event.path.contains('browser_test.dart.js')) {
return true;
}
// Ignore auto-generated JS files.
// The reason we are using `.contains()` instead of `.endsWith()` is
// because the auto-generated files could end with any of the
// following:
//
// - browser_test.dart.js
// - browser_test.dart.js.map
// - browser_test.dart.js.deps
if (event.path.contains('browser_test.dart.js')) {
return true;
}

// React to changes in lib/ and test/ folders.
final String relativePath = path.relative(event.path, from: dir.absolute);
if (relativePath.startsWith('lib/') || relativePath.startsWith('test/')) {
return false;
}
// React to changes in lib/ and test/ folders.
final String relativePath =
path.relative(event.path, from: dir.absolute);
if (relativePath.startsWith('lib/') ||
relativePath.startsWith('test/')) {
return false;
}

// Ignore anything else.
return true;
}
).start();
// Ignore anything else.
return true;
}).start();
// Return a never-ending future.
return Completer<bool>().future;
} else {
Expand All @@ -226,15 +227,17 @@ class TestCommand extends Command<bool> with ArgUtils {
bool unitTestResult = await runUnitTests();
bool integrationTestResult = await runIntegrationTests();
if (integrationTestResult != unitTestResult) {
print('Tests run. Integration tests passed: $integrationTestResult '
print(
'Tests run. Integration tests passed: $integrationTestResult '
'unit tests passed: $unitTestResult');
}
return integrationTestResult && unitTestResult;
} else {
return await runUnitTests();
}
}
throw UnimplementedError('Unknown test type requested: $testTypesRequested');
throw UnimplementedError(
'Unknown test type requested: $testTypesRequested');
} on TestFailureException {
return true;
}
Expand Down Expand Up @@ -786,6 +789,7 @@ const List<String> _kTestFonts = <String>[
'ahem.ttf',
'Roboto-Regular.ttf',
'NotoNaskhArabic-Regular.ttf',
'NotoColorEmoji.ttf',
];

void _copyTestFontsIntoWebUi() {
Expand Down
30 changes: 18 additions & 12 deletions lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -211,15 +211,19 @@ Future<void> _registerSymbolsAndEmoji() async {
String? symbolsFontUrl = extractUrlFromCss(symbolsCss);
String? emojiFontUrl = extractUrlFromCss(emojiCss);

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

notoDownloadQueue.add(_ResolvedNotoSubset(
symbolsFontUrl!, 'Noto Sans Symbols', const <CodeunitRange>[]));
notoDownloadQueue.add(_ResolvedNotoSubset(
emojiFontUrl!, 'Noto Color Emoji Compat', const <CodeunitRange>[]));
if (emojiFontUrl != null) {
notoDownloadQueue.add(_ResolvedNotoSubset(
emojiFontUrl, 'Noto Color Emoji Compat', const <CodeunitRange>[]));
} else {
html.window.console.warn('Error parsing CSS for Noto Emoji font.');
}
}

/// Finds the minimum set of fonts which covers all of the [codeunits].
Expand Down Expand Up @@ -695,9 +699,10 @@ class NotoDownloader {
if (assertionsEnabled) {
_debugActiveDownloadCount += 1;
}
final Future<ByteBuffer> result = html.window.fetch(url).then((dynamic fetchResult) => fetchResult
.arrayBuffer()
.then<ByteBuffer>((dynamic x) => x as ByteBuffer));
final Future<ByteBuffer> result = html.window.fetch(url).then(
(dynamic fetchResult) => fetchResult
.arrayBuffer()
.then<ByteBuffer>((dynamic x) => x as ByteBuffer));
if (assertionsEnabled) {
result.whenComplete(() {
_debugActiveDownloadCount -= 1;
Expand All @@ -713,8 +718,9 @@ class NotoDownloader {
if (assertionsEnabled) {
_debugActiveDownloadCount += 1;
}
final Future<String> result = html.window.fetch(url).then((dynamic response) =>
response.text().then<String>((dynamic x) => x as String));
final Future<String> result = html.window.fetch(url).then(
(dynamic response) =>
response.text().then<String>((dynamic x) => x as String));
if (assertionsEnabled) {
result.whenComplete(() {
_debugActiveDownloadCount -= 1;
Expand Down
31 changes: 17 additions & 14 deletions lib/web_ui/lib/src/engine/canvaskit/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class CkParagraphStyle implements ui.ParagraphStyle {
ui.StrutStyle? strutStyle,
String? ellipsis,
ui.Locale? locale,
}) : skParagraphStyle = toSkParagraphStyle(
}) : skParagraphStyle = toSkParagraphStyle(
textAlign,
textDirection,
maxLines,
Expand All @@ -34,11 +34,11 @@ class CkParagraphStyle implements ui.ParagraphStyle {
ellipsis,
locale,
),
_textDirection = textDirection ?? ui.TextDirection.ltr,
_fontFamily = fontFamily,
_fontSize = fontSize,
_fontWeight = fontWeight,
_fontStyle = fontStyle;
_textDirection = textDirection ?? ui.TextDirection.ltr,
_fontFamily = fontFamily,
_fontSize = fontSize,
_fontWeight = fontWeight,
_fontStyle = fontStyle;

final SkParagraphStyle skParagraphStyle;
final ui.TextDirection? _textDirection;
Expand Down Expand Up @@ -276,13 +276,14 @@ class CkTextStyle implements ui.TextStyle {
}

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

/// Lazy-initialized Skia style used to pass the style to Skia.
///
/// This is lazy because not every style ends up being passed to Skia, so the
/// conversion would be wasteful.
late final SkTextStyle skTextStyle = () {
late final SkTextStyle skTextStyle = () {
// Write field values to locals so null checks promote types to non-null.
final ui.Color? color = this.color;
final ui.TextDecoration? decoration = this.decoration;
Expand Down Expand Up @@ -695,22 +696,22 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
typefaces.addAll(typefacesForFamily);
}
}
List<bool> codeUnitsSupported = List<bool>.filled(text.length, false);
List<int> codeUnits = text.runes.toList();
List<bool> codeUnitsSupported = List<bool>.filled(codeUnits.length, false);
for (SkTypeface typeface in typefaces) {
SkFont font = SkFont(typeface);
Uint8List glyphs = font.getGlyphIDs(text);
assert(glyphs.length == codeUnitsSupported.length);
for (int i = 0; i < glyphs.length; i++) {
codeUnitsSupported[i] |=
glyphs[i] != 0 || _isControlCode(text.codeUnitAt(i));
codeUnitsSupported[i] |= glyphs[i] != 0 || _isControlCode(codeUnits[i]);
}
}

if (codeUnitsSupported.any((x) => !x)) {
List<int> missingCodeUnits = <int>[];
for (int i = 0; i < codeUnitsSupported.length; i++) {
if (!codeUnitsSupported[i]) {
missingCodeUnits.add(text.codeUnitAt(i));
missingCodeUnits.add(codeUnits[i]);
}
}
_findFontsForMissingCodeunits(missingCodeUnits);
Expand Down Expand Up @@ -778,7 +779,8 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
// This object is never deleted. It is effectively a static global constant.
// Therefore it doesn't need to be wrapped in CkPaint.
static final SkPaint _defaultTextForeground = SkPaint();
static final SkPaint _defaultTextBackground = SkPaint()..setColorInt(0x00000000);
static final SkPaint _defaultTextBackground = SkPaint()
..setColorInt(0x00000000);

@override
void pushStyle(ui.TextStyle style) {
Expand All @@ -796,7 +798,8 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
foreground = _defaultTextForeground;
}

final SkPaint background = skStyle.background?.skiaObject ?? _defaultTextBackground;
final SkPaint background =
skStyle.background?.skiaObject ?? _defaultTextBackground;
_paragraphBuilder.pushPaintStyle(
skStyle.skTextStyle, foreground, background);
} else {
Expand Down
74 changes: 69 additions & 5 deletions lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ void main() {
internalBootstrapBrowserTest(() => testMain);
}

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

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

pb = CkParagraphBuilder(
CkParagraphStyle(
fontSize: 32,
),
CkParagraphStyle(),
);
pb.pushStyle(ui.TextStyle(fontSize: 32));
pb.addText('مرحبا');
pb.pop();
final CkParagraph paragraph = pb.build();
paragraph.layout(ui.ParagraphConstraints(width: 1000));

canvas.drawParagraph(paragraph, ui.Offset(200, 120));
canvas.drawParagraph(paragraph, ui.Offset(0, 0));

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

test('will download Noto Emojis and Noto Symbols if no matching Noto Font',
() async {
final Completer<void> fontChangeCompleter = Completer<void>();
// Intercept the system font change message.
ui.window.onPlatformMessage = (String name, ByteData? data,
ui.PlatformMessageResponseCallback? callback) {
if (name == 'flutter/system') {
const JSONMessageCodec codec = JSONMessageCodec();
final dynamic message = codec.decodeMessage(data);
if (message is Map) {
if (message['type'] == 'fontsChange') {
fontChangeCompleter.complete();
}
}
}
if (savedCallback != null) {
savedCallback!(name, data, callback);
}
};

TestDownloader.mockDownloads[
'https://fonts.googleapis.com/css2?family=Noto+Color+Emoji+Compat'] =
'''
/* arabic */
@font-face {
font-family: 'Noto Color Emoji';
src: url(packages/ui/assets/NotoColorEmoji.ttf) format('ttf');
}
''';

expect(skiaFontCollection.globalFontFallbacks, ['Roboto']);

// Creating this paragraph should cause us to start to download the
// fallback font.
CkParagraphBuilder pb = CkParagraphBuilder(
CkParagraphStyle(),
);
pb.addText('Hello 😊');

await fontChangeCompleter.future;

expect(skiaFontCollection.globalFontFallbacks,
contains('Noto Color Emoji Compat 0'));

final CkPictureRecorder recorder = CkPictureRecorder();
final CkCanvas canvas = recorder.beginRecording(kDefaultRegion);

pb = CkParagraphBuilder(
CkParagraphStyle(),
);
pb.pushStyle(ui.TextStyle(fontSize: 26));
pb.addText('Hello 😊');
pb.pop();
final CkParagraph paragraph = pb.build();
paragraph.layout(ui.ParagraphConstraints(width: 1000));

canvas.drawParagraph(paragraph, ui.Offset(0, 0));

await matchPictureGolden(
'canvaskit_font_fallback_emoji.png', recorder.endRecording());
// TODO: https://github.com/flutter/flutter/issues/60040
// TODO: https://github.com/flutter/flutter/issues/71520
}, skip: isIosSafari || isFirefox);

test('will gracefully fail if we cannot parse the Google Fonts CSS',
() async {
TestDownloader.mockDownloads[
Expand Down