• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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