diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index c51b8b40b37ca..ff623934c6176 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -16,8 +16,15 @@ late CanvasKit canvasKit; /// static APIs. /// /// See, e.g. [SkPaint]. +/// +/// This also acts as a cache of an initialized CanvasKit instance. We can use +/// this, for example, to perform a hot restart without needing to redownload +/// and reinitialize CanvasKit. +@JS('window.flutterCanvasKit') +external set windowFlutterCanvasKit(CanvasKit? value); + @JS('window.flutterCanvasKit') -external set windowFlutterCanvasKit(CanvasKit value); +external CanvasKit? get windowFlutterCanvasKit; @JS() @anonymous @@ -141,7 +148,7 @@ class ColorSpace {} @anonymous class SkWebGLContextOptions { external factory SkWebGLContextOptions({ - required int anitalias, + required int antialias, // WebGL version: 1 or 2. required int majorVersion, }); diff --git a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart index 74582ab6c80c4..ac6039f2bb665 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart @@ -87,20 +87,23 @@ String canvasKitWasmModuleUrl(String file) => canvasKitBuildUrl + file; /// This calls `CanvasKitInit` and assigns the global [canvasKit] object. Future initializeCanvasKit() { final Completer canvasKitCompleter = Completer(); - late StreamSubscription loadSubscription; - loadSubscription = domRenderer.canvasKitScript!.onLoad.listen((_) { - loadSubscription.cancel(); - final CanvasKitInitPromise canvasKitInitPromise = - CanvasKitInit(CanvasKitInitOptions( - locateFile: js.allowInterop( - (String file, String unusedBase) => canvasKitWasmModuleUrl(file)), - )); - canvasKitInitPromise.then(js.allowInterop((CanvasKit ck) { - canvasKit = ck; - windowFlutterCanvasKit = canvasKit; - canvasKitCompleter.complete(); - })); - }); + if (windowFlutterCanvasKit != null) { + canvasKit = windowFlutterCanvasKit!; + canvasKitCompleter.complete(); + } else { + domRenderer.canvasKitLoaded!.then((_) { + final CanvasKitInitPromise canvasKitInitPromise = + CanvasKitInit(CanvasKitInitOptions( + locateFile: js.allowInterop( + (String file, String unusedBase) => canvasKitWasmModuleUrl(file)), + )); + canvasKitInitPromise.then(js.allowInterop((CanvasKit ck) { + canvasKit = ck; + windowFlutterCanvasKit = canvasKit; + canvasKitCompleter.complete(); + })); + }); + } /// Add a Skia scene host. skiaSceneHost = html.Element.tag('flt-scene'); diff --git a/lib/web_ui/lib/src/engine/canvaskit/surface.dart b/lib/web_ui/lib/src/engine/canvaskit/surface.dart index fbcc20e962156..399fe49fc86f3 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/surface.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/surface.dart @@ -210,7 +210,7 @@ class Surface { SkWebGLContextOptions( // Default to no anti-aliasing. Paint commands can be explicitly // anti-aliased by setting their `Paint` object's `antialias` property. - anitalias: 0, + antialias: 0, majorVersion: webGLVersion, ), ); diff --git a/lib/web_ui/lib/src/engine/dom_renderer.dart b/lib/web_ui/lib/src/engine/dom_renderer.dart index f56ff368e8139..c86d1029ec565 100644 --- a/lib/web_ui/lib/src/engine/dom_renderer.dart +++ b/lib/web_ui/lib/src/engine/dom_renderer.dart @@ -48,6 +48,9 @@ class DomRenderer { html.ScriptElement? get canvasKitScript => _canvasKitScript; html.ScriptElement? _canvasKitScript; + Future? get canvasKitLoaded => _canvasKitLoaded; + Future? _canvasKitLoaded; + /// The element that contains the [sceneElement]. /// /// This element is created and inserted in the HTML DOM once. It is never @@ -441,7 +444,8 @@ flt-glass-pane * { _sceneHostElement = createElement('flt-scene-host'); - final html.Element semanticsHostElement = createElement('flt-semantics-host'); + final html.Element semanticsHostElement = + createElement('flt-semantics-host'); semanticsHostElement.style ..position = 'absolute' ..transformOrigin = '0 0 0'; @@ -509,11 +513,21 @@ flt-glass-pane * { }); } - if (useCanvasKit) { + // Only reset CanvasKit if it's not already available. + if (useCanvasKit && windowFlutterCanvasKit == null) { _canvasKitScript?.remove(); _canvasKitScript = html.ScriptElement(); _canvasKitScript!.src = canvasKitJavaScriptBindingsUrl; + Completer canvasKitLoadCompleter = Completer(); + _canvasKitLoaded = canvasKitLoadCompleter.future; + + late StreamSubscription loadSubscription; + loadSubscription = _canvasKitScript!.onLoad.listen((_) { + loadSubscription.cancel(); + canvasKitLoadCompleter.complete(true); + }); + // TODO(hterkelsen): Rather than this monkey-patch hack, we should // build CanvasKit ourselves. See: // https://github.com/flutter/flutter/issues/52588 @@ -591,7 +605,8 @@ flt-glass-pane * { /// logical pixels. To compensate, we inject an inverse scale at the root /// level. void updateSemanticsScreenProperties() { - _semanticsHostElement!.style.transform = 'scale(${1 / html.window.devicePixelRatio})'; + _semanticsHostElement!.style.transform = + 'scale(${1 / html.window.devicePixelRatio})'; } /// Called immediately after browser window metrics change. diff --git a/lib/web_ui/test/canvaskit/hot_restart_test.dart b/lib/web_ui/test/canvaskit/hot_restart_test.dart new file mode 100644 index 0000000000000..f1aa63c23b3c8 --- /dev/null +++ b/lib/web_ui/test/canvaskit/hot_restart_test.dart @@ -0,0 +1,40 @@ +// 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. + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; + +import 'package:ui/ui.dart' as ui; + +import 'common.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('initialize', () { + test( + 're-uses the same initialized instance if it is already set on the window', + () async { + expect(windowFlutterCanvasKit, isNull); + + DomRenderer(); + await ui.webOnlyInitializePlatform( + assetManager: WebOnlyMockAssetManager()); + expect(windowFlutterCanvasKit, isNotNull); + + var firstCanvasKitInstance = windowFlutterCanvasKit; + + // Triggers a reset of the CanvasKit script element. + DomRenderer(); + await ui.webOnlyInitializePlatform( + assetManager: WebOnlyMockAssetManager()); + // The instance is the same. + expect(firstCanvasKitInstance, windowFlutterCanvasKit); + }); + // TODO: https://github.com/flutter/flutter/issues/60040 + }, skip: isIosSafari); +} diff --git a/lib/web_ui/test/canvaskit/initialization_test.dart b/lib/web_ui/test/canvaskit/initialization_test.dart index 0c09c2122f3c8..50792f1ae6610 100644 --- a/lib/web_ui/test/canvaskit/initialization_test.dart +++ b/lib/web_ui/test/canvaskit/initialization_test.dart @@ -20,7 +20,8 @@ void testMain() { test('populates flt-renderer and flt-build-mode', () { DomRenderer(); - expect(html.document.body!.attributes['flt-renderer'], 'canvaskit (requested explicitly)'); + expect(html.document.body!.attributes['flt-renderer'], + 'canvaskit (requested explicitly)'); expect(html.document.body!.attributes['flt-build-mode'], 'debug'); }); // TODO: https://github.com/flutter/flutter/issues/60040