Improve error message when CanvasKit is unable to parse a font (flutter/engine#24827)

This commit is contained in:
Harry Terkelsen 2021-03-16 09:16:10 -07:00 committed by GitHub
parent 87f4e5a35a
commit e8a33f1be4
14 changed files with 259 additions and 121 deletions

View File

@ -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;
}
}

View File

@ -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')

View File

@ -60,11 +60,18 @@ class FontFallbackData {
final Map<String, int> fontFallbackCounts = <String, int>{};
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<void> findFontsForMissingCodeunits(List<int> 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<void> _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<void> _registerSymbolsAndEmoji() async {
notoDownloadQueue.add(_ResolvedNotoSubset(
symbolsFontUrl, 'Noto Sans Symbols', const <CodeunitRange>[]));
} 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 <CodeunitRange>[]));
} 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);

View File

@ -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<void> 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);

View File

@ -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;
}

View File

@ -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(

View File

@ -572,7 +572,7 @@ class CkParagraph extends ManagedSkiaObject<SkParagraph>
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<SkTypeface>? 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.',
);

View File

@ -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 ||

View File

@ -65,7 +65,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder {
double dy, {
ui.OffsetEngineLayer? oldLayer,
}) {
return _pushSurface<PersistedOffset>(PersistedOffset(oldLayer as PersistedOffset?, dx, dy));
return _pushSurface<PersistedOffset>(
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>(PersistedTransform(oldLayer as PersistedTransform?, matrix));
return _pushSurface<PersistedTransform>(
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>(PersistedClipRect(oldLayer as PersistedClipRect?, rect, clipBehavior));
return _pushSurface<PersistedClipRect>(
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>(PersistedClipRRect(oldLayer, rrect, clipBehavior));
return _pushSurface<PersistedClipRRect>(
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>(PersistedClipPath(oldLayer as PersistedClipPath?, path, clipBehavior));
return _pushSurface<PersistedClipPath>(
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>(PersistedOpacity(oldLayer as PersistedOpacity?, alpha, offset));
return _pushSurface<PersistedOpacity>(
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>(PersistedColorFilter(oldLayer as PersistedColorFilter?, filter));
return _pushSurface<PersistedColorFilter>(
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>(PersistedImageFilter(oldLayer as PersistedImageFilter?, filter));
return _pushSurface<PersistedImageFilter>(
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>(PersistedBackdropFilter(oldLayer as PersistedBackdropFilter?, filter as EngineImageFilter));
return _pushSurface<PersistedBackdropFilter>(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>(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>(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);
}

View File

@ -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<Object, ui.ViewConfiguration> _windowConfigurations = <Object, ui.ViewConfiguration>{};
Map<Object, ui.ViewConfiguration> _windowConfigurations =
<Object, ui.ViewConfiguration>{};
/// 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<ui.PointerDataPacket>(_onPointerDataPacket, _onPointerDataPacketZone, dataPacket);
invoke1<ui.PointerDataPacket>(
_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<ui.FrameTiming> timings) {
invoke1<List<ui.FrameTiming>>(_onReportTimings, _onReportTimingsZone, timings);
invoke1<List<ui.FrameTiming>>(
_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<String, dynamic> 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<dynamic> 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<A>(void callback(A a)?, Zone? zone, A arg) {
}
/// Invokes [callback] inside the given [zone] passing it [arg1] and [arg2].
void invoke2<A1, A2>(void Function(A1 a1, A2 a2)? callback, Zone? zone, A1 arg1, A2 arg2) {
void invoke2<A1, A2>(
void Function(A1 a1, A2 a2)? callback, Zone? zone, A1 arg1, A2 arg2) {
if (callback == null) {
return;
}
@ -978,7 +1000,8 @@ void invoke2<A1, A2>(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<A1, A2, A3>(void Function(A1 a1, A2 a2, A3 a3)? callback, Zone? zone, A1 arg1, A2 arg2, A3 arg3) {
void invoke3<A1, A2, A3>(void Function(A1 a1, A2 a2, A3 a3)? callback,
Zone? zone, A1 arg1, A2 arg2, A3 arg3) {
if (callback == null) {
return;
}

View File

@ -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<String, dynamic> fontFamily in fontManifest.cast<Map<String, dynamic>>()) {
for (Map<String, dynamic> fontFamily
in fontManifest.cast<Map<String, dynamic>>()) {
final String? family = fontFamily['family'];
final List<dynamic> fontAssets = fontFamily['fonts'];
@ -78,8 +78,8 @@ class FontCollection {
_testFontManager = FontManager();
_testFontManager!.registerAsset(
_ahemFontFamily, 'url($_ahemFontUrl)', const <String, String>{});
_testFontManager!.registerAsset(
_robotoFontFamily, 'url($_robotoTestFontUrl)', const <String, String>{});
_testFontManager!.registerAsset(_robotoFontFamily,
'url($_robotoTestFontUrl)', const <String, String>{});
}
/// 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');

View File

@ -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;

View File

@ -16,7 +16,7 @@ void main() {
}
void testMain() {
group('LayerScene', () {
group('$LayerScene', () {
setUpAll(() async {
await ui.webOnlyInitializePlatform();
});

View File

@ -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<String> warnings = <String>[];
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(
<String>[
'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);
}