From e8a33f1be493f044bb397efb0468518a2ebe04d7 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 16 Mar 2021 09:16:10 -0700 Subject: [PATCH] Improve error message when CanvasKit is unable to parse a font (flutter/engine#24827) --- .../lib/web_ui/lib/src/engine/assets.dart | 5 +- .../src/engine/canvaskit/canvaskit_api.dart | 2 +- .../src/engine/canvaskit/font_fallbacks.dart | 32 ++++--- .../lib/src/engine/canvaskit/fonts.dart | 35 +++++--- .../lib/src/engine/canvaskit/image.dart | 2 +- .../lib/src/engine/canvaskit/surface.dart | 36 ++++---- .../web_ui/lib/src/engine/canvaskit/text.dart | 6 +- .../lib/src/engine/html/platform_view.dart | 7 +- .../lib/src/engine/html/scene_builder.dart | 53 ++++++----- .../lib/src/engine/platform_dispatcher.dart | 89 ++++++++++++------- .../lib/src/engine/text/font_collection.dart | 27 +++--- .../lib/web_ui/lib/src/engine/util.dart | 6 ++ .../lib/web_ui/test/canvaskit/scene_test.dart | 2 +- .../canvaskit/skia_font_collection_test.dart | 78 ++++++++++++++++ 14 files changed, 259 insertions(+), 121 deletions(-) create mode 100644 engine/src/flutter/lib/web_ui/test/canvaskit/skia_font_collection_test.dart diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/assets.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/assets.dart index 4f0fab752c0..6dbd62fdd1f 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/assets.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/assets.dart @@ -61,14 +61,13 @@ class AssetManager { final html.EventTarget? target = e.target; if (target is html.HttpRequest) { if (target.status == 404 && asset == 'AssetManifest.json') { - html.window.console - .warn('Asset manifest does not exist at `$url` – ignoring.'); + printWarning('Asset manifest does not exist at `$url` – ignoring.'); return Uint8List.fromList(utf8.encode('{}')).buffer.asByteData(); } throw AssetManagerException(url, target.status!); } - html.window.console.warn('Caught ProgressEvent with target: $target'); + printWarning('Caught ProgressEvent with target: $target'); rethrow; } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index 5742bc1a92f..ee2ccee7540 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -1652,7 +1652,7 @@ class SkFont { class SkFontMgr { external String? getFamilyName(int fontId); external void delete(); - external SkTypeface MakeTypefaceFromData(Uint8List font); + external SkTypeface? MakeTypefaceFromData(Uint8List font); } @JS('window.flutterCanvasKit.TypefaceFontProvider') diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart index 458ac1cbe9a..060d7e62383 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -60,11 +60,18 @@ class FontFallbackData { final Map fontFallbackCounts = {}; void registerFallbackFont(String family, Uint8List bytes) { + final SkTypeface? typeface = + canvasKit.FontMgr.RefDefault().MakeTypefaceFromData(bytes); + if (typeface == null) { + printWarning('Failed to parse fallback font $family as a font.'); + return; + } fontFallbackCounts.putIfAbsent(family, () => 0); int fontFallbackTag = fontFallbackCounts[family]!; fontFallbackCounts[family] = fontFallbackCounts[family]! + 1; String countedFamily = '$family $fontFallbackTag'; - registeredFallbackFonts.add(_RegisteredFont(bytes, countedFamily)); + registeredFallbackFonts + .add(_RegisteredFont(bytes, countedFamily, typeface)); globalFontFallbacks.add(countedFamily); } } @@ -123,7 +130,7 @@ Future findFontsForMissingCodeunits(List codeUnits) async { _registerSymbolsAndEmoji(); } else { if (!notoDownloadQueue.isPending) { - html.window.console.log( + printWarning( 'Could not find a set of Noto fonts to display all missing ' 'characters. Please add a font asset for the missing characters.' ' See: https://flutter.dev/docs/cookbook/design/fonts'); @@ -179,7 +186,7 @@ _ResolvedNotoFont? _makeResolvedNotoFontFromCss(String css, String name) { if (line.startsWith(' src:')) { int urlStart = line.indexOf('url('); if (urlStart == -1) { - html.window.console.warn('Unable to resolve Noto font URL: $line'); + printWarning('Unable to resolve Noto font URL: $line'); return null; } int urlEnd = line.indexOf(')'); @@ -207,7 +214,7 @@ _ResolvedNotoFont? _makeResolvedNotoFontFromCss(String css, String name) { } } else if (line == '}') { if (fontFaceUrl == null || fontFaceUnicodeRanges == null) { - html.window.console.warn('Unable to parse Google Fonts CSS: $css'); + printWarning('Unable to parse Google Fonts CSS: $css'); return null; } subsets @@ -220,7 +227,7 @@ _ResolvedNotoFont? _makeResolvedNotoFontFromCss(String css, String name) { } if (resolvingFontFace) { - html.window.console.warn('Unable to parse Google Fonts CSS: $css'); + printWarning('Unable to parse Google Fonts CSS: $css'); return null; } @@ -233,7 +240,7 @@ _ResolvedNotoFont? _makeResolvedNotoFontFromCss(String css, String name) { } if (rangesMap.isEmpty) { - html.window.console.warn('Parsed Google Fonts CSS was empty: $css'); + printWarning('Parsed Google Fonts CSS was empty: $css'); return null; } @@ -267,14 +274,14 @@ Future _registerSymbolsAndEmoji() async { if (line.startsWith(' src:')) { int urlStart = line.indexOf('url('); if (urlStart == -1) { - html.window.console.warn('Unable to resolve Noto font URL: $line'); + printWarning('Unable to resolve Noto font URL: $line'); return null; } int urlEnd = line.indexOf(')'); return line.substring(urlStart + 4, urlEnd); } } - html.window.console.warn('Unable to determine URL for Noto font'); + printWarning('Unable to determine URL for Noto font'); return null; } @@ -285,14 +292,14 @@ Future _registerSymbolsAndEmoji() async { notoDownloadQueue.add(_ResolvedNotoSubset( symbolsFontUrl, 'Noto Sans Symbols', const [])); } else { - html.window.console.warn('Error parsing CSS for Noto Symbols font.'); + printWarning('Error parsing CSS for Noto Symbols font.'); } if (emojiFontUrl != null) { notoDownloadQueue.add(_ResolvedNotoSubset( emojiFontUrl, 'Noto Color Emoji Compat', const [])); } else { - html.window.console.warn('Error parsing CSS for Noto Emoji font.'); + printWarning('Error parsing CSS for Noto Emoji font.'); } } @@ -724,9 +731,8 @@ class FallbackFontDownloadQueue { debugDescription: subset.family); } catch (e) { pendingSubsets.remove(subset.url); - html.window.console - .warn('Failed to load font ${subset.family} at ${subset.url}'); - html.window.console.warn(e); + printWarning('Failed to load font ${subset.family} at ${subset.url}'); + printWarning(e.toString()); return; } downloadedSubsets.add(subset); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart index 0465928d7f4..a8fba15ab86 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart @@ -70,14 +70,20 @@ class SkiaFontCollection { if (fontFamily == null) { fontFamily = _readActualFamilyName(list); if (fontFamily == null) { - html.window.console - .warn('Failed to read font family name. Aborting font load.'); + printWarning('Failed to read font family name. Aborting font load.'); return; } } - _registeredFonts.add(_RegisteredFont(list, fontFamily)); - await ensureFontsLoaded(); + final SkTypeface? typeface = + canvasKit.FontMgr.RefDefault().MakeTypefaceFromData(list); + if (typeface != null) { + _registeredFonts.add(_RegisteredFont(list, fontFamily, typeface)); + await ensureFontsLoaded(); + } else { + printWarning('Failed to parse font family "$fontFamily"'); + return; + } } Future registerFonts(AssetManager assetManager) async { @@ -87,8 +93,7 @@ class SkiaFontCollection { byteData = await assetManager.load('FontManifest.json'); } on AssetManagerException catch (e) { if (e.httpStatus == 404) { - html.window.console - .warn('Font manifest does not exist at `${e.url}` – ignoring.'); + printWarning('Font manifest does not exist at `${e.url}` – ignoring.'); return; } else { rethrow; @@ -135,13 +140,21 @@ class SkiaFontCollection { try { buffer = await html.window.fetch(url).then(_getArrayBuffer); } catch (e) { - html.window.console.warn('Failed to load font $family at $url'); - html.window.console.warn(e); + printWarning('Failed to load font $family at $url'); + printWarning(e.toString()); return null; } final Uint8List bytes = buffer.asUint8List(); - return _RegisteredFont(bytes, family); + SkTypeface? typeface = + canvasKit.FontMgr.RefDefault().MakeTypefaceFromData(bytes); + if (typeface != null) { + return _RegisteredFont(bytes, family, typeface); + } else { + printWarning('Failed to load font $family at $url'); + printWarning('Verify that $url contains a valid font.'); + return null; + } } String? _readActualFamilyName(Uint8List bytes) { @@ -175,9 +188,7 @@ class _RegisteredFont { /// This is used to determine which code points are supported by this font. final SkTypeface typeface; - _RegisteredFont(this.bytes, this.family) - : this.typeface = - canvasKit.FontMgr.RefDefault().MakeTypefaceFromData(bytes) { + _RegisteredFont(this.bytes, this.family, this.typeface) { // This is a hack which causes Skia to cache the decoded font. SkFont skFont = SkFont(typeface); skFont.getGlyphBounds([0], null, null); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart index e0c9bcc76c5..2b4870e9b58 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -180,7 +180,7 @@ class CkImage implements ui.Image, StackTraceDebugger { colorSpace: SkColorSpaceSRGB, ); if (originalBytes == null) { - html.window.console.warn('Unable to encode image to bytes. We will not ' + printWarning('Unable to encode image to bytes. We will not ' 'be able to resurrect it once it has been garbage collected.'); return; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart index 7ad4b2aec1c..069e8ba63c0 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -71,7 +71,7 @@ class Surface { } void _syncCacheBytes() { - if(_skiaCacheBytes != null) { + if (_skiaCacheBytes != null) { _grContext?.setResourceCacheLimitBytes(_skiaCacheBytes!); } } @@ -130,12 +130,12 @@ class Surface { _currentDevicePixelRatio = window.devicePixelRatio; _currentSize = _currentSize == null - // First frame. Allocate a canvas of the exact size as the window. The - // window is frequently never resized, particularly on mobile, so using - // the exact size is most optimal. - ? size - // The window is growing. Overallocate to prevent frequent reallocations. - : size * 1.4; + // First frame. Allocate a canvas of the exact size as the window. The + // window is frequently never resized, particularly on mobile, so using + // the exact size is most optimal. + ? size + // The window is growing. Overallocate to prevent frequent reallocations. + : size * 1.4; _surface?.dispose(); _surface = null; @@ -199,9 +199,11 @@ class Surface { htmlElement.append(htmlCanvas); if (webGLVersion == -1) { - return _makeSoftwareCanvasSurface(htmlCanvas, 'WebGL support not detected'); + return _makeSoftwareCanvasSurface( + htmlCanvas, 'WebGL support not detected'); } else if (canvasKitForceCpuOnly) { - return _makeSoftwareCanvasSurface(htmlCanvas, 'CPU rendering forced by application'); + return _makeSoftwareCanvasSurface( + htmlCanvas, 'CPU rendering forced by application'); } else { // Try WebGL first. final int glContext = canvasKit.GetWebGLContext( @@ -215,13 +217,15 @@ class Surface { ); if (glContext == 0) { - return _makeSoftwareCanvasSurface(htmlCanvas, 'Failed to initialize WebGL context'); + return _makeSoftwareCanvasSurface( + htmlCanvas, 'Failed to initialize WebGL context'); } _grContext = canvasKit.MakeGrContext(glContext); if (_grContext == null) { - throw CanvasKitError('Failed to initialize CanvasKit. CanvasKit.MakeGrContext returned null.'); + throw CanvasKitError( + 'Failed to initialize CanvasKit. CanvasKit.MakeGrContext returned null.'); } // Set the cache byte limit for this grContext, if not specified it will use @@ -236,7 +240,8 @@ class Surface { ); if (skSurface == null) { - return _makeSoftwareCanvasSurface(htmlCanvas, 'Failed to initialize WebGL surface'); + return _makeSoftwareCanvasSurface( + htmlCanvas, 'Failed to initialize WebGL surface'); } return CkSurface(skSurface, _grContext, glContext); @@ -245,11 +250,10 @@ class Surface { static bool _didWarnAboutWebGlInitializationFailure = false; - CkSurface _makeSoftwareCanvasSurface(html.CanvasElement htmlCanvas, String reason) { + CkSurface _makeSoftwareCanvasSurface( + html.CanvasElement htmlCanvas, String reason) { if (!_didWarnAboutWebGlInitializationFailure) { - html.window.console.warn( - 'WARNING: Falling back to CPU-only rendering. $reason.' - ); + printWarning('WARNING: Falling back to CPU-only rendering. $reason.'); _didWarnAboutWebGlInitializationFailure = true; } return CkSurface( diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart index 88004af027e..7f6f271844f 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -572,7 +572,7 @@ class CkParagraph extends ManagedSkiaObject try { skiaObject.layout(constraints.width); } catch (e) { - html.window.console.warn('CanvasKit threw an exception while laying ' + printWarning('CanvasKit threw an exception while laying ' 'out the paragraph. The font was "${_paragraphStyle._fontFamily}". ' 'Exception:\n$e'); rethrow; @@ -775,7 +775,7 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { List? typefacesForFamily = skiaFontCollection.familyToTypefaceMap[font]; if (typefacesForFamily == null) { - html.window.console.warn('A fallback font was registered but we ' + printWarning('A fallback font was registered but we ' 'cannot retrieve the typeface for it.'); continue; } @@ -864,7 +864,7 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { if (_styleStack.length <= 1) { // The top-level text style is paragraph-level. We don't pop it off. if (assertionsEnabled) { - html.window.console.warn( + printWarning( 'Cannot pop text style in ParagraphBuilder. ' 'Already popped all text styles from the style stack.', ); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/platform_view.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/platform_view.dart index 5987fe2f840..d990c6b0119 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/platform_view.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/platform_view.dart @@ -52,7 +52,7 @@ class PersistedPlatformView extends PersistedLeafSurface { if (platformView != null) { _shadowRoot.append(platformView); } else { - html.window.console.warn('No platform view created for id $viewId'); + printWarning('No platform view created for id $viewId'); } return element; } @@ -93,7 +93,10 @@ class PersistedPlatformView extends PersistedLeafSurface { @override void update(PersistedPlatformView oldSurface) { - assert(viewId == oldSurface.viewId, 'PersistedPlatformView with different viewId should never be updated. Check the canUpdateAsMatch method.',); + assert( + viewId == oldSurface.viewId, + 'PersistedPlatformView with different viewId should never be updated. Check the canUpdateAsMatch method.', + ); super.update(oldSurface); // Only update if the view has been resized if (dx != oldSurface.dx || diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart index 3e078ab3c22..785a5d0980a 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html/scene_builder.dart @@ -65,7 +65,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { double dy, { ui.OffsetEngineLayer? oldLayer, }) { - return _pushSurface(PersistedOffset(oldLayer as PersistedOffset?, dx, dy)); + return _pushSurface( + PersistedOffset(oldLayer as PersistedOffset?, dx, dy)); } /// Pushes a transform operation onto the operation stack. @@ -90,13 +91,14 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { // logical device pixels. if (!ui.debugEmulateFlutterTesterEnvironment) { assert(matrix4[0] == window.devicePixelRatio && - matrix4[5] == window.devicePixelRatio); + matrix4[5] == window.devicePixelRatio); } matrix = Matrix4.identity().storage; } else { matrix = toMatrix32(matrix4); } - return _pushSurface(PersistedTransform(oldLayer as PersistedTransform?, matrix)); + return _pushSurface( + PersistedTransform(oldLayer as PersistedTransform?, matrix)); } /// Pushes a rectangular clip operation onto the operation stack. @@ -113,7 +115,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { }) { assert(clipBehavior != null); // ignore: unnecessary_null_comparison assert(clipBehavior != ui.Clip.none); - return _pushSurface(PersistedClipRect(oldLayer as PersistedClipRect?, rect, clipBehavior)); + return _pushSurface( + PersistedClipRect(oldLayer as PersistedClipRect?, rect, clipBehavior)); } /// Pushes a rounded-rectangular clip operation onto the operation stack. @@ -127,7 +130,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { ui.Clip? clipBehavior, ui.ClipRRectEngineLayer? oldLayer, }) { - return _pushSurface(PersistedClipRRect(oldLayer, rrect, clipBehavior)); + return _pushSurface( + PersistedClipRRect(oldLayer, rrect, clipBehavior)); } /// Pushes a path clip operation onto the operation stack. @@ -143,7 +147,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { }) { assert(clipBehavior != null); // ignore: unnecessary_null_comparison assert(clipBehavior != ui.Clip.none); - return _pushSurface(PersistedClipPath(oldLayer as PersistedClipPath?, path, clipBehavior)); + return _pushSurface( + PersistedClipPath(oldLayer as PersistedClipPath?, path, clipBehavior)); } /// Pushes an opacity operation onto the operation stack. @@ -160,7 +165,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { ui.Offset offset = ui.Offset.zero, ui.OpacityEngineLayer? oldLayer, }) { - return _pushSurface(PersistedOpacity(oldLayer as PersistedOpacity?, alpha, offset)); + return _pushSurface( + PersistedOpacity(oldLayer as PersistedOpacity?, alpha, offset)); } /// Pushes a color filter operation onto the operation stack. @@ -179,7 +185,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { ui.ColorFilterEngineLayer? oldLayer, }) { assert(filter != null); // ignore: unnecessary_null_comparison - return _pushSurface(PersistedColorFilter(oldLayer as PersistedColorFilter?, filter)); + return _pushSurface( + PersistedColorFilter(oldLayer as PersistedColorFilter?, filter)); } /// Pushes an image filter operation onto the operation stack. @@ -198,7 +205,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { ui.ImageFilterEngineLayer? oldLayer, }) { assert(filter != null); // ignore: unnecessary_null_comparison - return _pushSurface(PersistedImageFilter(oldLayer as PersistedImageFilter?, filter)); + return _pushSurface( + PersistedImageFilter(oldLayer as PersistedImageFilter?, filter)); } /// Pushes a backdrop filter operation onto the operation stack. @@ -212,7 +220,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { ui.ImageFilter filter, { ui.BackdropFilterEngineLayer? oldLayer, }) { - return _pushSurface(PersistedBackdropFilter(oldLayer as PersistedBackdropFilter?, filter as EngineImageFilter)); + return _pushSurface(PersistedBackdropFilter( + oldLayer as PersistedBackdropFilter?, filter as EngineImageFilter)); } /// Pushes a shader mask operation onto the operation stack. @@ -228,10 +237,9 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { ui.BlendMode blendMode, { ui.ShaderMaskEngineLayer? oldLayer, }) { - assert(shader != null && maskRect != null && blendMode != null); // ignore: unnecessary_null_comparison + assert(blendMode != null); // ignore: unnecessary_null_comparison return _pushSurface(PersistedShaderMask( - oldLayer as PersistedShaderMask?, - shader, maskRect, blendMode)); + oldLayer as PersistedShaderMask?, shader, maskRect, blendMode)); } /// Pushes a physical layer operation for an arbitrary shape onto the @@ -255,7 +263,6 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { ui.Clip clipBehavior = ui.Clip.none, ui.PhysicalShapeEngineLayer? oldLayer, }) { - assert(color != null, 'color must not be null'); // ignore: unnecessary_null_comparison return _pushSurface(PersistedPhysicalShape( oldLayer as PersistedPhysicalShape?, path as SurfacePath, @@ -276,7 +283,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { /// no need to call [addToScene] for its children layers. @override void addRetained(ui.EngineLayer retainedLayer) { - final PersistedContainerSurface retainedSurface = retainedLayer as PersistedContainerSurface; + final PersistedContainerSurface retainedSurface = + retainedLayer as PersistedContainerSurface; if (assertionsEnabled) { assert(debugAssertSurfaceState(retainedSurface, PersistedSurfaceState.active, PersistedSurfaceState.released)); @@ -340,8 +348,7 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { ) { if (!_webOnlyDidWarnAboutPerformanceOverlay) { _webOnlyDidWarnAboutPerformanceOverlay = true; - html.window.console - .warn('The performance overlay isn\'t supported on the web'); + printWarning('The performance overlay isn\'t supported on the web'); } } @@ -362,7 +369,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { if (willChangeHint) { hints |= 2; } - _addSurface(PersistedPicture(offset.dx, offset.dy, picture as EnginePicture, hints)); + _addSurface(PersistedPicture( + offset.dx, offset.dy, picture as EnginePicture, hints)); } /// Adds a backend texture to the scene. @@ -378,12 +386,12 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { bool freeze = false, ui.FilterQuality filterQuality = ui.FilterQuality.low, }) { - assert(offset != null, 'Offset argument was null'); // ignore: unnecessary_null_comparison - _addTexture(offset.dx, offset.dy, width, height, textureId, filterQuality.index); + _addTexture( + offset.dx, offset.dy, width, height, textureId, filterQuality.index); } - void _addTexture( - double dx, double dy, double width, double height, int textureId, int filterQuality) { + void _addTexture(double dx, double dy, double width, double height, + int textureId, int filterQuality) { // In test mode, allow this to be a no-op. if (!ui.debugEmulateFlutterTesterEnvironment) { throw UnimplementedError('Textures are not supported in Flutter Web'); @@ -413,7 +421,6 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { double width = 0.0, double height = 0.0, }) { - assert(offset != null, 'Offset argument was null'); // ignore: unnecessary_null_comparison _addPlatformView(offset.dx, offset.dy, width, height, viewId); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 89eaee544b4..bd714524bd5 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -25,16 +25,19 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// The [EnginePlatformDispatcher] singleton. static EnginePlatformDispatcher get instance => _instance; - static final EnginePlatformDispatcher _instance = EnginePlatformDispatcher._(); + static final EnginePlatformDispatcher _instance = + EnginePlatformDispatcher._(); /// The current platform configuration. @override ui.PlatformConfiguration get configuration => _configuration; - ui.PlatformConfiguration _configuration = ui.PlatformConfiguration(locales: parseBrowserLanguages()); + ui.PlatformConfiguration _configuration = + ui.PlatformConfiguration(locales: parseBrowserLanguages()); /// Receives all events related to platform configuration changes. @override - ui.VoidCallback? get onPlatformConfigurationChanged => _onPlatformConfigurationChanged; + ui.VoidCallback? get onPlatformConfigurationChanged => + _onPlatformConfigurationChanged; ui.VoidCallback? _onPlatformConfigurationChanged; Zone? _onPlatformConfigurationChangedZone; @override @@ -46,7 +49,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// Engine code should use this method instead of the callback directly. /// Otherwise zones won't work properly. void invokeOnPlatformConfigurationChanged() { - invoke(_onPlatformConfigurationChanged, _onPlatformConfigurationChangedZone); + invoke( + _onPlatformConfigurationChanged, _onPlatformConfigurationChangedZone); } /// The current list of windows, @@ -57,7 +61,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// /// This should be considered a protected member, only to be used by /// [PlatformDispatcher] subclasses. - Map _windowConfigurations = {}; + Map _windowConfigurations = + {}; /// A callback that is invoked whenever the platform's [devicePixelRatio], /// [physicalSize], [padding], [viewInsets], or [systemGestureInsets] @@ -169,7 +174,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// Engine code should use this method instead of the callback directly. /// Otherwise zones won't work properly. void invokeOnPointerDataPacket(ui.PointerDataPacket dataPacket) { - invoke1(_onPointerDataPacket, _onPointerDataPacketZone, dataPacket); + invoke1( + _onPointerDataPacket, _onPointerDataPacketZone, dataPacket); } /// A callback that is invoked when key data is available. @@ -239,7 +245,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// Engine code should use this method instead of the callback directly. /// Otherwise zones won't work properly. void invokeOnReportTimings(List timings) { - invoke1>(_onReportTimings, _onReportTimingsZone, timings); + invoke1>( + _onReportTimings, _onReportTimingsZone, timings); } @override @@ -248,7 +255,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { ByteData? data, ui.PlatformMessageResponseCallback? callback, ) { - _sendPlatformMessage(name, data, _zonedPlatformMessageResponseCallback(callback)); + _sendPlatformMessage( + name, data, _zonedPlatformMessageResponseCallback(callback)); } // TODO(ianh): Deprecate onPlatformMessage once the framework is moved over @@ -293,10 +301,11 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// Wraps the given [callback] in another callback that ensures that the /// original callback is called in the zone it was registered in. static ui.PlatformMessageResponseCallback? - _zonedPlatformMessageResponseCallback( - ui.PlatformMessageResponseCallback? callback) { - if (callback == null) + _zonedPlatformMessageResponseCallback( + ui.PlatformMessageResponseCallback? callback) { + if (callback == null) { return null; + } // Store the zone in which the callback is being registered. final Zone registrationZone = Zone.current; @@ -327,6 +336,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { } switch (name) { + /// This should be in sync with shell/common/shell.cc case 'flutter/skia': const MethodCodec codec = JSONMethodCodec(); @@ -346,19 +356,18 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { // Also respond in HTML mode. Otherwise, apps would have to detect // CanvasKit vs HTML before invoking this method. - _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope([true])); + _replyToPlatformMessage( + callback, codec.encodeSuccessEnvelope([true])); break; } return; case 'flutter/assets': - assert(ui.webOnlyAssetManager != null); // ignore: unnecessary_null_comparison final String url = utf8.decode(data!.buffer.asUint8List()); ui.webOnlyAssetManager.load(url).then((ByteData assetData) { _replyToPlatformMessage(callback, assetData); }, onError: (dynamic error) { - html.window.console - .warn('Error while trying to load an asset: $error'); + printWarning('Error while trying to load an asset: $error'); _replyToPlatformMessage(callback, null); }); return; @@ -371,7 +380,10 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { // TODO(gspencergoog): As multi-window support expands, the pop call // will need to include the window ID. Right now only one window is // supported. - (_windows[0] as EngineFlutterWindow).browserHistory.exit().then((_) { + (_windows[0] as EngineFlutterWindow) + .browserHistory + .exit() + .then((_) { _replyToPlatformMessage( callback, codec.encodeSuccessEnvelope(true)); }); @@ -379,13 +391,15 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { case 'HapticFeedback.vibrate': final String type = decoded.arguments; domRenderer.vibrate(_getHapticFeedbackDuration(type)); - _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); + _replyToPlatformMessage( + callback, codec.encodeSuccessEnvelope(true)); return; case 'SystemChrome.setApplicationSwitcherDescription': final Map arguments = decoded.arguments; domRenderer.setTitle(arguments['label']); domRenderer.setThemeColor(ui.Color(arguments['primaryColor'])); - _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); + _replyToPlatformMessage( + callback, codec.encodeSuccessEnvelope(true)); return; case 'SystemChrome.setPreferredOrientations': final List arguments = decoded.arguments; @@ -396,7 +410,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { return; case 'SystemSound.play': // There are no default system sounds on web. - _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); + _replyToPlatformMessage( + callback, codec.encodeSuccessEnvelope(true)); return; case 'Clipboard.setData': ClipboardMessageHandler().setDataMethodCall(decoded, callback); @@ -454,10 +469,13 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { // TODO(gspencergoog): As multi-window support expands, the navigation call // will need to include the window ID. Right now only one window is // supported. - (_windows[0] as EngineFlutterWindow).handleNavigationMessage(data).then((bool handled) { + (_windows[0] as EngineFlutterWindow) + .handleNavigationMessage(data) + .then((bool handled) { if (handled) { const MethodCodec codec = JSONMethodCodec(); - _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); + _replyToPlatformMessage( + callback, codec.encodeSuccessEnvelope(true)); } else { callback?.call(null); } @@ -481,8 +499,6 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { _replyToPlatformMessage(callback, null); } - - int _getHapticFeedbackDuration(String type) { switch (type) { case 'HapticFeedbackType.lightImpact': @@ -508,8 +524,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { @override void scheduleFrame() { if (scheduleFrameCallback == null) { - throw new Exception( - 'scheduleFrameCallback must be initialized first.'); + throw new Exception('scheduleFrameCallback must be initialized first.'); } scheduleFrameCallback!(); } @@ -561,13 +576,15 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { } /// Additional accessibility features that may be enabled by the platform. - ui.AccessibilityFeatures get accessibilityFeatures => configuration.accessibilityFeatures; + ui.AccessibilityFeatures get accessibilityFeatures => + configuration.accessibilityFeatures; /// A callback that is invoked when the value of [accessibilityFeatures] changes. /// /// The framework invokes this callback in the same zone in which the /// callback was set. - ui.VoidCallback? get onAccessibilityFeaturesChanged => _onAccessibilityFeaturesChanged; + ui.VoidCallback? get onAccessibilityFeaturesChanged => + _onAccessibilityFeaturesChanged; ui.VoidCallback? _onAccessibilityFeaturesChanged; Zone? _onAccessibilityFeaturesChangedZone; set onAccessibilityFeaturesChanged(ui.VoidCallback? callback) { @@ -578,7 +595,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// Engine code should use this method instead of the callback directly. /// Otherwise zones won't work properly. void invokeOnAccessibilityFeaturesChanged() { - invoke(_onAccessibilityFeaturesChanged, _onAccessibilityFeaturesChangedZone); + invoke( + _onAccessibilityFeaturesChanged, _onAccessibilityFeaturesChangedZone); } /// Change the retained semantics data about this window. @@ -604,7 +622,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// /// * https://developer.mozilla.org/en-US/docs/Web/API/NavigatorLanguage/languages, /// which explains browser quirks in the implementation notes. - ui.Locale get locale => locales.isEmpty ? const ui.Locale.fromSubtags() : locales.first; + ui.Locale get locale => + locales.isEmpty ? const ui.Locale.fromSubtags() : locales.first; /// The full system-reported supported locales of the device. /// @@ -796,7 +815,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to /// observe when this callback is invoked. - ui.VoidCallback? get onPlatformBrightnessChanged => _onPlatformBrightnessChanged; + ui.VoidCallback? get onPlatformBrightnessChanged => + _onPlatformBrightnessChanged; ui.VoidCallback? _onPlatformBrightnessChanged; Zone? _onPlatformBrightnessChangedZone; set onPlatformBrightnessChanged(ui.VoidCallback? callback) { @@ -890,7 +910,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// * [SystemChannels.navigation], which handles subsequent navigation /// requests from the embedder. String get defaultRouteName { - return _defaultRouteName ??= (_windows[0]! as EngineFlutterWindow).browserHistory.currentPath; + return _defaultRouteName ??= + (_windows[0]! as EngineFlutterWindow).browserHistory.currentPath; } /// Lazily initialized when the `defaultRouteName` getter is invoked. @@ -961,7 +982,8 @@ void invoke1(void callback(A a)?, Zone? zone, A arg) { } /// Invokes [callback] inside the given [zone] passing it [arg1] and [arg2]. -void invoke2(void Function(A1 a1, A2 a2)? callback, Zone? zone, A1 arg1, A2 arg2) { +void invoke2( + void Function(A1 a1, A2 a2)? callback, Zone? zone, A1 arg1, A2 arg2) { if (callback == null) { return; } @@ -978,7 +1000,8 @@ void invoke2(void Function(A1 a1, A2 a2)? callback, Zone? zone, A1 arg1, } /// Invokes [callback] inside the given [zone] passing it [arg1], [arg2], and [arg3]. -void invoke3(void Function(A1 a1, A2 a2, A3 a3)? callback, Zone? zone, A1 arg1, A2 arg2, A3 arg3) { +void invoke3(void Function(A1 a1, A2 a2, A3 a3)? callback, + Zone? zone, A1 arg1, A2 arg2, A3 arg3) { if (callback == null) { return; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/text/font_collection.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/text/font_collection.dart index 8587c9713c6..9e55eeea269 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/text/font_collection.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/text/font_collection.dart @@ -29,8 +29,7 @@ class FontCollection { byteData = await assetManager.load('FontManifest.json'); } on AssetManagerException catch (e) { if (e.httpStatus == 404) { - html.window.console - .warn('Font manifest does not exist at `${e.url}` – ignoring.'); + printWarning('Font manifest does not exist at `${e.url}` – ignoring.'); return; } else { rethrow; @@ -50,7 +49,8 @@ class FontCollection { _assetFontManager = _PolyfillFontManager(); } - for (Map fontFamily in fontManifest.cast>()) { + for (Map fontFamily + in fontManifest.cast>()) { final String? family = fontFamily['family']; final List fontAssets = fontFamily['fonts']; @@ -78,8 +78,8 @@ class FontCollection { _testFontManager = FontManager(); _testFontManager!.registerAsset( _ahemFontFamily, 'url($_ahemFontUrl)', const {}); - _testFontManager!.registerAsset( - _robotoFontFamily, 'url($_robotoTestFontUrl)', const {}); + _testFontManager!.registerAsset(_robotoFontFamily, + 'url($_robotoTestFontUrl)', const {}); } /// Returns a [Future] that completes when the registered fonts are loaded @@ -180,12 +180,10 @@ class FontManager { _fontLoadingFutures.add(fontFace.load().then((_) { html.document.fonts!.add(fontFace); }, onError: (dynamic e) { - html.window.console - .warn('Error while trying to load font family "$family":\n$e'); + printWarning('Error while trying to load font family "$family":\n$e'); })); } catch (e) { - html.window.console - .warn('Error while loading font family "$family":\n$e'); + printWarning('Error while loading font family "$family":\n$e'); } } @@ -241,8 +239,8 @@ class _PolyfillFontManager extends FontManager { paragraph.style.position = 'absolute'; paragraph.style.visibility = 'hidden'; paragraph.style.fontSize = '72px'; - final String fallbackFontName = browserEngine == BrowserEngine.ie11 ? - 'Times New Roman' : 'sans-serif'; + final String fallbackFontName = + browserEngine == BrowserEngine.ie11 ? 'Times New Roman' : 'sans-serif'; paragraph.style.fontFamily = fallbackFontName; if (descriptors['style'] != null) { paragraph.style.fontStyle = descriptors['style']; @@ -309,5 +307,8 @@ class _PolyfillFontManager extends FontManager { } } -final bool supportsFontLoadingApi = js_util.hasProperty(html.window, 'FontFace'); -final bool supportsFontsClearApi = js_util.hasProperty(html.document, 'fonts') && js_util.hasProperty(html.document.fonts!, 'clear'); +final bool supportsFontLoadingApi = + js_util.hasProperty(html.window, 'FontFace'); +final bool supportsFontsClearApi = + js_util.hasProperty(html.document, 'fonts') && + js_util.hasProperty(html.document.fonts!, 'clear'); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart index e0304a4962a..170716bbd7e 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart @@ -595,3 +595,9 @@ int clampInt(int value, int min, int max) { return value; } } + +/// Prints a warning message to the console. +/// +/// This function can be overridden in tests. This could be useful, for example, +/// to verify that warnings are printed under certain circumstances. +void Function(String) printWarning = html.window.console.warn; diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/scene_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/scene_test.dart index e76e7e945d2..0a9222305f1 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/scene_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/scene_test.dart @@ -16,7 +16,7 @@ void main() { } void testMain() { - group('LayerScene', () { + group('$LayerScene', () { setUpAll(() async { await ui.webOnlyInitializePlatform(); }); diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/skia_font_collection_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/skia_font_collection_test.dart new file mode 100644 index 00000000000..dd1ba26a0e7 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/skia_font_collection_test.dart @@ -0,0 +1,78 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.6 +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; + +import 'package:ui/src/engine.dart'; + +import 'common.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('$SkiaFontCollection', () { + List warnings = []; + void Function(String) oldPrintWarning; + + setUpAll(() async { + await initializeCanvasKit(); + oldPrintWarning = printWarning; + printWarning = (String warning) { + warnings.add(warning); + }; + }); + + tearDownAll(() { + printWarning = oldPrintWarning; + }); + + setUp(() { + warnings.clear(); + }); + + test('logs no warnings with the default mock asset manager', () { + final SkiaFontCollection fontCollection = SkiaFontCollection(); + final WebOnlyMockAssetManager mockAssetManager = + WebOnlyMockAssetManager(); + expect(fontCollection.registerFonts(mockAssetManager), completes); + expect(fontCollection.ensureFontsLoaded(), completes); + expect(warnings, isEmpty); + }); + + test('logs a warning if one of the registered fonts is invalid', () async { + final SkiaFontCollection fontCollection = SkiaFontCollection(); + final WebOnlyMockAssetManager mockAssetManager = + WebOnlyMockAssetManager(); + mockAssetManager.defaultFontManifest = ''' +[ + { + "family":"Roboto", + "fonts":[{"asset":"packages/ui/assets/Roboto-Regular.ttf"}] + }, + { + "family": "BrokenFont", + "fonts":[{"asset":"packages/bogus/BrokenFont.ttf"}] + } + ] + '''; + // It should complete without error, but emit a warning about BrokenFont. + await fontCollection.registerFonts(mockAssetManager); + await fontCollection.ensureFontsLoaded(); + expect( + warnings, + containsAllInOrder( + [ + 'Failed to load font BrokenFont at packages/bogus/BrokenFont.ttf', + 'Verify that packages/bogus/BrokenFont.ttf contains a valid font.', + ], + ), + ); + }); + // TODO: https://github.com/flutter/flutter/issues/60040 + }, skip: isIosSafari); +}