1// Copyright 2013 The Flutter Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5part of engine; 6 7const String _testFontFamily = 'Ahem'; 8const String _testFontUrl = 'packages/flutter_web/assets/Ahem.ttf'; 9const String _robotoFontUrl = 10 'packages/flutter_web_ui/assets/Roboto-Regular.ttf'; 11 12/// This class is responsible for registering and loading fonts. 13/// 14/// Once an asset manager has been set in the framework, call 15/// [registerFonts] with it to register fonts declared in the 16/// font manifest. If test fonts are enabled, then call 17/// [registerTestFonts] as well. 18class FontCollection { 19 _FontManager _assetFontManager; 20 _FontManager _testFontManager; 21 22 /// Reads the font manifest using the [assetManager] and registers all of the 23 /// fonts declared within. 24 Future<void> registerFonts(AssetManager assetManager) async { 25 ByteData byteData; 26 27 try { 28 byteData = await assetManager.load('FontManifest.json'); 29 } on AssetManagerException catch (e) { 30 if (e.httpStatus == 404) { 31 html.window.console 32 .warn('Font manifest does not exist at `${e.url}` – ignoring.'); 33 return; 34 } else { 35 rethrow; 36 } 37 } 38 39 if (byteData == null) { 40 throw AssertionError( 41 'There was a problem trying to load FontManifest.json'); 42 } 43 44 final List<dynamic> fontManifest = 45 json.decode(utf8.decode(byteData.buffer.asUint8List())); 46 if (fontManifest == null) { 47 throw AssertionError( 48 'There was a problem trying to load FontManifest.json'); 49 } 50 51 if (supportsFontLoadingApi) { 52 _assetFontManager = _FontManager(); 53 } else { 54 _assetFontManager = _PolyfillFontManager(); 55 } 56 57 // If not on Chrome, add Roboto to the bundled fonts since it is provided 58 // by default by Flutter. 59 if (browserEngine != BrowserEngine.blink) { 60 _assetFontManager 61 .registerAsset('Roboto', 'url($_robotoFontUrl)', <String, String>{}); 62 } 63 64 for (Map<String, dynamic> fontFamily in fontManifest) { 65 final String family = fontFamily['family']; 66 final List<dynamic> fontAssets = fontFamily['fonts']; 67 68 for (dynamic fontAssetItem in fontAssets) { 69 final Map<String, dynamic> fontAsset = fontAssetItem; 70 final String asset = fontAsset['asset']; 71 final Map<String, String> descriptors = <String, String>{}; 72 for (String descriptor in fontAsset.keys) { 73 if (descriptor != 'asset') { 74 descriptors[descriptor] = '${fontAsset[descriptor]}'; 75 } 76 } 77 _assetFontManager.registerAsset( 78 family, 'url(${assetManager.getAssetUrl(asset)})', descriptors); 79 } 80 } 81 } 82 83 /// Registers fonts that are used by tests. 84 void debugRegisterTestFonts() { 85 _testFontManager = _FontManager(); 86 _testFontManager.registerAsset( 87 _testFontFamily, 'url($_testFontUrl)', const <String, String>{}); 88 } 89 90 /// Returns a [Future] that completes when the registered fonts are loaded 91 /// and ready to be used. 92 Future<void> ensureFontsLoaded() async { 93 await _assetFontManager?.ensureFontsLoaded(); 94 await _testFontManager?.ensureFontsLoaded(); 95 } 96 97 /// Unregister all fonts that have been registered. 98 void clear() { 99 _assetFontManager = null; 100 _testFontManager = null; 101 if (supportsFontLoadingApi) { 102 html.document.fonts.clear(); 103 } 104 } 105} 106 107/// Manages a collection of fonts and ensures they are loaded. 108class _FontManager { 109 final List<Future<void>> _fontLoadingFutures = <Future<void>>[]; 110 111 factory _FontManager() { 112 if (supportsFontLoadingApi) { 113 return _FontManager._(); 114 } else { 115 return _PolyfillFontManager(); 116 } 117 } 118 119 _FontManager._(); 120 121 void registerAsset( 122 String family, 123 String asset, 124 Map<String, String> descriptors, 125 ) { 126 final html.FontFace fontFace = html.FontFace(family, asset, descriptors); 127 _fontLoadingFutures.add(fontFace 128 .load() 129 .then((_) => html.document.fonts.add(fontFace), onError: (dynamic e) { 130 html.window.console 131 .warn('Error while trying to load font family "$family":\n$e'); 132 return null; 133 })); 134 } 135 136 /// Returns a [Future] that completes when all fonts that have been 137 /// registered with this font manager have been loaded and are ready to use. 138 Future<void> ensureFontsLoaded() { 139 return Future.wait(_fontLoadingFutures); 140 } 141} 142 143/// A font manager that works without using the CSS Font Loading API. 144/// 145/// The CSS Font Loading API is not implemented in IE 11 or Edge. To tell if a 146/// font is loaded, we continuously measure some text using that font until the 147/// width changes. 148class _PolyfillFontManager extends _FontManager { 149 _PolyfillFontManager() : super._(); 150 151 /// A String containing characters whose width varies greatly between fonts. 152 static const String _testString = 'giItT1WQy@!-/#'; 153 154 static const Duration _fontLoadTimeout = Duration(seconds: 2); 155 static const Duration _fontLoadRetryDuration = Duration(milliseconds: 50); 156 157 @override 158 void registerAsset( 159 String family, 160 String asset, 161 Map<String, String> descriptors, 162 ) { 163 final html.ParagraphElement paragraph = html.ParagraphElement(); 164 paragraph.style.position = 'absolute'; 165 paragraph.style.visibility = 'hidden'; 166 paragraph.style.fontSize = '72px'; 167 paragraph.style.fontFamily = 'sans-serif'; 168 if (descriptors['style'] != null) { 169 paragraph.style.fontStyle = descriptors['style']; 170 } 171 if (descriptors['weight'] != null) { 172 paragraph.style.fontWeight = descriptors['weight']; 173 } 174 paragraph.text = _testString; 175 176 html.document.body.append(paragraph); 177 final int sansSerifWidth = paragraph.offsetWidth; 178 179 paragraph.style.fontFamily = '$family, sans-serif'; 180 181 final Completer<void> completer = Completer<void>(); 182 183 DateTime _fontLoadStart; 184 185 void _watchWidth() { 186 if (paragraph.offsetWidth != sansSerifWidth) { 187 paragraph.remove(); 188 completer.complete(); 189 } else { 190 if (DateTime.now().difference(_fontLoadStart) > _fontLoadTimeout) { 191 completer.completeError( 192 Exception('Timed out trying to load font: $family')); 193 } else { 194 Timer(_fontLoadRetryDuration, _watchWidth); 195 } 196 } 197 } 198 199 final Map<String, String> fontStyleMap = <String, String>{}; 200 fontStyleMap['font-family'] = "'$family'"; 201 fontStyleMap['src'] = asset; 202 if (descriptors['style'] != null) { 203 fontStyleMap['font-style'] = descriptors['style']; 204 } 205 if (descriptors['weight'] != null) { 206 fontStyleMap['font-weight'] = descriptors['weight']; 207 } 208 final String fontFaceDeclaration = fontStyleMap.keys 209 .map((String name) => '$name: ${fontStyleMap[name]};') 210 .join(' '); 211 final html.StyleElement fontLoadStyle = html.StyleElement(); 212 fontLoadStyle.type = 'text/css'; 213 fontLoadStyle.innerHtml = '@font-face { $fontFaceDeclaration }'; 214 html.document.head.append(fontLoadStyle); 215 216 // HACK: If this is an icon font, then when it loads it won't change the 217 // width of our test string. So we just have to hope it loads before the 218 // layout phase. 219 if (family.toLowerCase().contains('icon')) { 220 paragraph.remove(); 221 return; 222 } 223 224 _fontLoadStart = DateTime.now(); 225 _watchWidth(); 226 227 _fontLoadingFutures.add(completer.future); 228 } 229} 230 231final bool supportsFontLoadingApi = html.document.fonts != null; 232