Skip to content

Commit

Permalink
[web] Add ability to customize font fallback download URL (flutter#51569
Browse files Browse the repository at this point in the history
)

Gives developers the ability to change the base URL to download fallback
fonts from `fonts.gstatic.com` to a URL of their choosing.

Fixes flutter/flutter#132689

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide] and the [C++,
Objective-C, Java style guides].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I added new tests to check the change I am making or feature I am
adding, or the PR is [test-exempt]. See [testing the engine] for
instructions on writing and running engine tests.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I signed the [CLA].
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
  • Loading branch information
harryterkelsen authored Mar 22, 2024
1 parent 1c80bed commit 80ed458
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 55 deletions.
1 change: 1 addition & 0 deletions lib/web_ui/flutter_js/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface FlutterConfiguration {
canvasKitVariant: CanvasKitVariant?;
renderer: WebRenderer?;
hostElement: HtmlElement?;
fontFallbackBaseUrl: string?;
}

export interface ServiceWorkerSettings {
Expand Down
4 changes: 2 additions & 2 deletions lib/web_ui/lib/src/engine/canvaskit/fonts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
// this, list out all of the fonts and find the URL for the regular
// Roboto font. The API reference is here:
// https://developers.google.com/fonts/docs/developer_api
const String _robotoUrl =
'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf';
String _robotoUrl =
'${configuration.fontFallbackBaseUrl}roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf';

/// Manages the fonts used in the Skia-based backend.
class SkiaFontCollection implements FlutterFontCollection {
Expand Down
59 changes: 39 additions & 20 deletions lib/web_ui/lib/src/engine/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ FlutterConfiguration get configuration {
}
return _configuration ??= FlutterConfiguration.legacy(_jsConfiguration);
}

FlutterConfiguration? _configuration;

FlutterConfiguration? _debugConfiguration;
Expand Down Expand Up @@ -106,14 +107,15 @@ class FlutterConfiguration {
// Warn the user of the deprecated behavior.
assert(() {
if (config != null) {
domWindow.console.warn('window.flutterConfiguration is now deprecated.\n'
'Use engineInitializer.initializeEngine(config) instead.\n'
'See: https://docs.flutter.dev/development/platform-integration/web/initialization');
domWindow.console.warn(
'window.flutterConfiguration is now deprecated.\n'
'Use engineInitializer.initializeEngine(config) instead.\n'
'See: https://docs.flutter.dev/development/platform-integration/web/initialization');
}
if (_requestedRendererType != null) {
domWindow.console.warn('window.flutterWebRenderer is now deprecated.\n'
'Use engineInitializer.initializeEngine(config) instead.\n'
'See: https://docs.flutter.dev/development/platform-integration/web/initialization');
'Use engineInitializer.initializeEngine(config) instead.\n'
'See: https://docs.flutter.dev/development/platform-integration/web/initialization');
}
return true;
}());
Expand Down Expand Up @@ -143,14 +145,16 @@ class FlutterConfiguration {
/// constructor.
void setUserConfiguration(JsFlutterConfiguration? configuration) {
if (configuration != null) {
assert(!_usedLegacyConfigStyle,
'Use engineInitializer.initializeEngine(config) only. '
'Using the (deprecated) window.flutterConfiguration and initializeEngine '
'configuration simultaneously is not supported.');
assert(_requestedRendererType == null || configuration.renderer == null,
'Use engineInitializer.initializeEngine(config) only. '
'Using the (deprecated) window.flutterWebRenderer and initializeEngine '
'configuration simultaneously is not supported.');
assert(
!_usedLegacyConfigStyle,
'Use engineInitializer.initializeEngine(config) only. '
'Using the (deprecated) window.flutterConfiguration and initializeEngine '
'configuration simultaneously is not supported.');
assert(
_requestedRendererType == null || configuration.renderer == null,
'Use engineInitializer.initializeEngine(config) only. '
'Using the (deprecated) window.flutterWebRenderer and initializeEngine '
'configuration simultaneously is not supported.');
_configuration = configuration;
}
}
Expand All @@ -177,9 +181,7 @@ class FlutterConfiguration {
/// true.
///
/// Using flutter tools option "--web-render=html" would set the value to false.
static const bool useSkia =
bool.fromEnvironment('FLUTTER_WEB_USE_SKIA');

static const bool useSkia = bool.fromEnvironment('FLUTTER_WEB_USE_SKIA');

// Runtime parameters.
//
Expand Down Expand Up @@ -235,7 +237,8 @@ class FlutterConfiguration {
/// --web-renderer=canvaskit \
/// --dart-define=FLUTTER_WEB_CANVASKIT_URL=https://example.com/custom-canvaskit-build/
/// ```
String get canvasKitBaseUrl => _configuration?.canvasKitBaseUrl ?? _defaultCanvasKitBaseUrl;
String get canvasKitBaseUrl =>
_configuration?.canvasKitBaseUrl ?? _defaultCanvasKitBaseUrl;
static const String _defaultCanvasKitBaseUrl = String.fromEnvironment(
'FLUTTER_WEB_CANVASKIT_URL',
defaultValue: 'canvaskit/',
Expand All @@ -262,7 +265,8 @@ class FlutterConfiguration {
///
/// This is mainly used for testing or for apps that want to ensure they
/// run on devices which don't support WebGL.
bool get canvasKitForceCpuOnly => _configuration?.canvasKitForceCpuOnly ?? _defaultCanvasKitForceCpuOnly;
bool get canvasKitForceCpuOnly =>
_configuration?.canvasKitForceCpuOnly ?? _defaultCanvasKitForceCpuOnly;
static const bool _defaultCanvasKitForceCpuOnly = bool.fromEnvironment(
'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY',
);
Expand All @@ -278,7 +282,9 @@ class FlutterConfiguration {
/// ```
/// flutter run -d chrome --profile --dart-define=FLUTTER_WEB_DEBUG_SHOW_SEMANTICS=true
/// ```
bool get debugShowSemanticsNodes => _configuration?.debugShowSemanticsNodes ?? _defaultDebugShowSemanticsNodes;
bool get debugShowSemanticsNodes =>
_configuration?.debugShowSemanticsNodes ??
_defaultDebugShowSemanticsNodes;
static const bool _defaultDebugShowSemanticsNodes = bool.fromEnvironment(
'FLUTTER_WEB_DEBUG_SHOW_SEMANTICS',
);
Expand Down Expand Up @@ -309,7 +315,16 @@ class FlutterConfiguration {
/// `window.flutterWebRenderer`.
///
/// This is used by the Renderer class to decide how to initialize the engine.
String? get requestedRendererType => _configuration?.renderer ?? _requestedRendererType;
String? get requestedRendererType =>
_configuration?.renderer ?? _requestedRendererType;

/// Returns the base URL to load fallback fonts from. Fallback fonts are
/// downloaded automatically when there is no font bundled with the app that
/// can show a glyph that is being rendered.
///
/// Defaults to 'https://fonts.gstatic.com/s/'.
String get fontFallbackBaseUrl =>
_configuration?.fontFallbackBaseUrl ?? 'https://fonts.gstatic.com/s/';

/// Whether to use color emojis or not.
///
Expand Down Expand Up @@ -364,6 +379,10 @@ extension JsFlutterConfigurationExtension on JsFlutterConfiguration {
external JSString? get _renderer;
String? get renderer => _renderer?.toDart;

@JS('fontFallbackBaseUrl')
external JSString? get _fontFallbackBaseUrl;
String? get fontFallbackBaseUrl => _fontFallbackBaseUrl?.toDart;

@JS('useColorEmoji')
external JSBoolean? get _useColorEmoji;
bool? get useColorEmoji => _useColorEmoji?.toDart;
Expand Down
9 changes: 3 additions & 6 deletions lib/web_ui/lib/src/engine/font_fallbacks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -449,11 +449,7 @@ class FallbackFontDownloadQueue {

final FontFallbackManager fallbackManager;

static const String _defaultFallbackFontsUrlPrefix =
'https://fonts.gstatic.com/s/';
String? fallbackFontUrlPrefixOverride;
String get fallbackFontUrlPrefix =>
fallbackFontUrlPrefixOverride ?? _defaultFallbackFontsUrlPrefix;
String get fallbackFontUrlPrefix => configuration.fontFallbackBaseUrl;

final Set<NotoFont> downloadedFonts = <NotoFont>{};
final Map<String, NotoFont> pendingFonts = <String, NotoFont>{};
Expand Down Expand Up @@ -497,7 +493,8 @@ class FallbackFontDownloadQueue {
downloadedFontFamilies.add(font.url);
} catch (e) {
pendingFonts.remove(font.url);
printWarning('Failed to load font ${font.name} at ${font.url}');
printWarning('Failed to load font ${font.name} at '
'$fallbackFontUrlPrefix${font.url}');
printWarning(e.toString());
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
// this, list out all of the fonts and find the URL for the regular
// Roboto font. The API reference is here:
// https://developers.google.com/fonts/docs/developer_api
const String _robotoUrl =
'https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf';
String _robotoUrl =
'${configuration.fontFallbackBaseUrl}roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf';

class SkwasmTypeface extends SkwasmObjectWrapper<RawTypeface> {
SkwasmTypeface(SkDataHandle data) : super(typefaceCreate(data), _registry);
Expand Down
1 change: 0 additions & 1 deletion lib/web_ui/test/canvaskit/canvas_golden_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ void testMain() {

setUp(() {
renderer.fontCollection.debugResetFallbackFonts();
renderer.fontCollection.fontFallbackManager!.downloadQueue.fallbackFontUrlPrefixOverride = 'assets/fallback_fonts/';
});

test('renders using non-recording canvas if weak refs are supported',
Expand Down
8 changes: 4 additions & 4 deletions lib/web_ui/test/canvaskit/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'dart:async';
import 'dart:js_interop';
import 'dart:typed_data';

import 'package:test/test.dart';
Expand All @@ -26,10 +27,9 @@ void setUpCanvasKitTest({bool withImplicitView = false}) {
setUpTestViewDimensions: false,
);

setUp(() => renderer.fontCollection.fontFallbackManager!.downloadQueue
.fallbackFontUrlPrefixOverride = 'assets/fallback_fonts/');
tearDown(() => renderer.fontCollection.fontFallbackManager!.downloadQueue
.fallbackFontUrlPrefixOverride = null);
setUp(() => debugOverrideJsConfiguration(<String, Object?>{
'fontFallbackBaseUrl': 'assets/fallback_fonts/',
}.jsify() as JsFlutterConfiguration?));
}

/// Convenience getter for the implicit view.
Expand Down
4 changes: 4 additions & 0 deletions lib/web_ui/test/canvaskit/skia_font_collection_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ void testMain() {
final SkiaFontCollection fontCollection = SkiaFontCollection();
testAssetScope.setAsset('FontManifest.json', stringAsUtf8Data('''
[
{
"family":"Roboto",
"fonts":[{"asset":"/assets/fonts/Roboto-Regular.ttf"}]
},
{
"family":"Ahem",
"fonts":[{"asset":"/assets/fonts/Roboto-Regular.ttf"}]
Expand Down
5 changes: 4 additions & 1 deletion lib/web_ui/test/common/test_initialization.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'dart:async';
import 'dart:js_interop';

import 'package:test/test.dart';
import 'package:ui/src/engine.dart' as engine;
Expand All @@ -27,7 +28,9 @@ void setUpUnitTests({
debugFontsScope = configureDebugFontsAssetScope(fakeAssetManager);
debugOnlyAssetManager = fakeAssetManager;
await bootstrapAndRunApp(withImplicitView: withImplicitView);
engine.renderer.fontCollection.fontFallbackManager?.downloadQueue.fallbackFontUrlPrefixOverride = 'assets/fallback_fonts/';
engine.debugOverrideJsConfiguration(<String, Object?>{
'fontFallbackBaseUrl': 'assets/fallback_fonts/',
}.jsify() as engine.JsFlutterConfiguration?);

if (setUpTestViewDimensions) {
// The following parameters are hard-coded in Flutter's test embedder. Since
Expand Down
69 changes: 50 additions & 19 deletions lib/web_ui/test/ui/fallback_fonts_golden_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:js_interop';
import 'dart:math' as math;

import 'package:test/bootstrap/browser.dart';
Expand Down Expand Up @@ -39,9 +40,12 @@ void testMain() {

setUp(() {
renderer.fontCollection.debugResetFallbackFonts();
renderer.fontCollection.fontFallbackManager!.downloadQueue.fallbackFontUrlPrefixOverride = 'assets/fallback_fonts/';
renderer.fontCollection.fontFallbackManager!.downloadQueue.debugOnLoadFontFamily
= (String family) => downloadedFontFamilies.add(family);
debugOverrideJsConfiguration(<String, Object?>{
'fontFallbackBaseUrl': 'assets/fallback_fonts/',
}.jsify() as JsFlutterConfiguration?);
renderer.fontCollection.fontFallbackManager!.downloadQueue
.debugOnLoadFontFamily =
(String family) => downloadedFontFamilies.add(family);
savedCallback = ui.PlatformDispatcher.instance.onPlatformMessage;
});

Expand All @@ -51,11 +55,30 @@ void testMain() {
});

test('Roboto is always a fallback font', () {
expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks, contains('Roboto'));
expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks,
contains('Roboto'));
});

test('can override font fallback base URL using JS', () {
expect(
renderer.fontCollection.fontFallbackManager!.downloadQueue
.fallbackFontUrlPrefix,
'assets/fallback_fonts/',
);
debugOverrideJsConfiguration(<String, Object?>{
'fontFallbackBaseUrl': 'http://my-special-fonts.com/',
}.jsify() as JsFlutterConfiguration?);

expect(
renderer.fontCollection.fontFallbackManager!.downloadQueue
.fallbackFontUrlPrefix,
'http://my-special-fonts.com/',
);
});

test('will download Noto Sans Arabic if Arabic text is added', () async {
expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks, <String>['Roboto']);
expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks,
<String>['Roboto']);

// Creating this paragraph should cause us to start to download the
// fallback font.
Expand Down Expand Up @@ -92,9 +115,11 @@ void testMain() {
// TODO(hterkelsen): https://github.com/flutter/flutter/issues/71520
});

test('will put the Noto Color Emoji font before other fallback fonts in the list',
test(
'will put the Noto Color Emoji font before other fallback fonts in the list',
() async {
expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks, <String>['Roboto']);
expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks,
<String>['Roboto']);

// Creating this paragraph should cause us to start to download the
// Arabic fallback font.
Expand All @@ -120,16 +145,20 @@ void testMain() {

await renderer.fontCollection.fontFallbackManager!.debugWhenIdle();

expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks, <String>[
'Roboto',
'Noto Color Emoji',
'Noto Sans Arabic',
]);
expect(
renderer.fontCollection.fontFallbackManager!.globalFontFallbacks,
<String>[
'Roboto',
'Noto Color Emoji',
'Noto Sans Arabic',
]);
});

test('will download Noto Color Emojis and Noto Symbols if no matching Noto Font',
test(
'will download Noto Color Emojis and Noto Symbols if no matching Noto Font',
() async {
expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks, <String>['Roboto']);
expect(renderer.fontCollection.fontFallbackManager!.globalFontFallbacks,
<String>['Roboto']);

// Creating this paragraph should cause us to start to download the
// fallback font.
Expand Down Expand Up @@ -170,7 +199,8 @@ void testMain() {
///
/// Then it does the same, but asserts that the families aren't downloaded again
/// (because they already exist in memory).
Future<void> checkDownloadedFamiliesForString(String text, List<String> expectedFamilies) async {
Future<void> checkDownloadedFamiliesForString(
String text, List<String> expectedFamilies) async {
// Try rendering text that requires fallback fonts, initially before the fonts are loaded.
ui.ParagraphBuilder pb = ui.ParagraphBuilder(ui.ParagraphStyle());
pb.addText(text);
Expand Down Expand Up @@ -217,7 +247,8 @@ void testMain() {
]);
});

test('findMinimumFontsForCodePoints for all supported code points', () async {
test('findMinimumFontsForCodePoints for all supported code points',
() async {
// Collect all supported code points from all fallback fonts in the Noto
// font tree.
final Set<String> testedFonts = <String>{};
Expand Down Expand Up @@ -452,7 +483,7 @@ void testMain() {
isNot(contains('Noto Color Emoji')));
});
},
// HTML renderer doesn't use the fallback font manager.
skip: isHtml,
timeout: const Timeout.factor(4));
// HTML renderer doesn't use the fallback font manager.
skip: isHtml,
timeout: const Timeout.factor(4));
}

0 comments on commit 80ed458

Please sign in to comment.