diff --git a/ci/analyze.sh b/ci/analyze.sh index 1d5aabb5d09db..017ca38fd265a 100755 --- a/ci/analyze.sh +++ b/ci/analyze.sh @@ -77,6 +77,11 @@ echo "" echo "" # Check that dart libraries conform. -echo "Checking web_ui api conformance..." +echo "Checking the integrity of the Web SDK" (cd "$FLUTTER_DIR/web_sdk"; "$DART" pub get) -(cd "$FLUTTER_DIR"; "$DART" "web_sdk/test/api_conform_test.dart") +WEB_SDK_TEST_FILES="$FLUTTER_DIR/web_sdk/test/*" +for testFile in $WEB_SDK_TEST_FILES +do + echo "Running $testFile" + (cd "$FLUTTER_DIR"; FLUTTER_DIR="$FLUTTER_DIR" "$DART" --enable-asserts $testFile) +done diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 901669237596c..a50623cb79c66 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -528,7 +528,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/color_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/debug_canvas_reuse_overlay.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/dom_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/image_filter.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/offscreen_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/offset.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/opacity.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/painting.dart @@ -555,11 +554,11 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/normalized_gradien FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/shader.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/shader_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/vertex_shaders.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/surface.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/surface_stats.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/transform.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html_image_codec.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/initialization.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/key_map.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/keyboard.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -580,6 +579,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_converter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/profiler.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/rrect_renderer.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart @@ -620,7 +620,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/ulps.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/util.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/validators.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/web_experiments.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/window.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/annotations.dart FILE: ../../../flutter/lib/web_ui/lib/src/ui/canvas.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 5148cb4b0c66c..f2128eac51b14 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -2,516 +2,157 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@JS() -library engine; - -// This file is transformed during the build process in order to make it a -// single library. Some notable transformations: +// This file is transformed during the build process into a single library with +// part files (`dart:_engine`) by performing the following: // -// 1. Imports of engine/* files are stripped out. -// 2. Exports of engine/* files are replaced with a part directive. +// - Replace all exports with part directives. +// - Rewrite the libraries into `part of` part files without imports. +// - Add imports to this file sufficient to cover the needs of `dart:_engine`. // // The code that performs the transformations lives in: -// - https://github.com/flutter/engine/blob/main/web_sdk/sdk_rewriter.dart - -import 'dart:async'; -// Some of these names are used in services/buffers.dart for example. -// ignore: unused_import -import 'dart:collection' - show ListBase, IterableBase, DoubleLinkedQueue, DoubleLinkedQueueEntry; -// ignore: unused_import -import 'dart:convert' hide Codec; -import 'dart:developer' as developer; -import 'dart:html' as html; -// ignore: unused_import -import 'dart:js' as js; -// ignore: unused_import -import 'dart:js_util' as js_util; -// ignore: unused_import -import 'dart:math' as math; -// ignore: unused_import -import 'dart:svg' as svg; -import 'dart:typed_data'; - -import 'package:js/js.dart'; -// ignore: unused_import -import 'package:meta/meta.dart'; - -import '../ui.dart' as ui; +// +// - https://github.com/flutter/engine/blob/main/web_sdk/sdk_rewriter.dart -// ignore: unused_import -import 'engine/configuration.dart'; -import 'engine/embedder.dart'; -import 'engine/keyboard.dart'; -import 'engine/mouse_cursor.dart'; -import 'engine/navigation/js_url_strategy.dart'; -import 'engine/navigation/url_strategy.dart'; -import 'engine/platform_dispatcher.dart'; -import 'engine/platform_views/content_manager.dart'; -import 'engine/profiler.dart'; -import 'engine/web_experiments.dart'; -import 'engine/window.dart'; +library engine; export 'engine/alarm_clock.dart'; - export 'engine/assets.dart'; - export 'engine/browser_detection.dart'; - export 'engine/canvas_pool.dart'; - export 'engine/canvaskit/canvas.dart'; - export 'engine/canvaskit/canvaskit_api.dart'; - export 'engine/canvaskit/canvaskit_canvas.dart'; - export 'engine/canvaskit/color_filter.dart'; - export 'engine/canvaskit/embedded_views.dart'; - export 'engine/canvaskit/font_fallbacks.dart'; - export 'engine/canvaskit/fonts.dart'; - export 'engine/canvaskit/image.dart'; - export 'engine/canvaskit/image_filter.dart'; - export 'engine/canvaskit/image_wasm_codecs.dart'; - export 'engine/canvaskit/image_web_codecs.dart'; - export 'engine/canvaskit/initialization.dart'; - export 'engine/canvaskit/interval_tree.dart'; - export 'engine/canvaskit/layer.dart'; - export 'engine/canvaskit/layer_scene_builder.dart'; - export 'engine/canvaskit/layer_tree.dart'; - export 'engine/canvaskit/mask_filter.dart'; - export 'engine/canvaskit/n_way_canvas.dart'; - export 'engine/canvaskit/painting.dart'; - export 'engine/canvaskit/path.dart'; - export 'engine/canvaskit/path_metrics.dart'; - export 'engine/canvaskit/picture.dart'; - export 'engine/canvaskit/picture_recorder.dart'; - export 'engine/canvaskit/raster_cache.dart'; - export 'engine/canvaskit/rasterizer.dart'; - export 'engine/canvaskit/shader.dart'; - export 'engine/canvaskit/skia_object_cache.dart'; - export 'engine/canvaskit/surface.dart'; - export 'engine/canvaskit/surface_factory.dart'; - export 'engine/canvaskit/text.dart'; - export 'engine/canvaskit/util.dart'; - export 'engine/canvaskit/vertices.dart'; - export 'engine/clipboard.dart'; - export 'engine/color_filter.dart'; - export 'engine/configuration.dart'; - export 'engine/embedder.dart'; - export 'engine/engine_canvas.dart'; - export 'engine/font_change_util.dart'; - export 'engine/frame_reference.dart'; - export 'engine/host_node.dart'; - export 'engine/html/backdrop_filter.dart'; - export 'engine/html/bitmap_canvas.dart'; - export 'engine/html/canvas.dart'; - export 'engine/html/clip.dart'; - export 'engine/html/color_filter.dart'; - export 'engine/html/debug_canvas_reuse_overlay.dart'; - export 'engine/html/dom_canvas.dart'; - export 'engine/html/image_filter.dart'; - -export 'engine/html/offscreen_canvas.dart'; - export 'engine/html/offset.dart'; - export 'engine/html/opacity.dart'; - export 'engine/html/painting.dart'; - export 'engine/html/path/conic.dart'; - export 'engine/html/path/cubic.dart'; - export 'engine/html/path/path.dart'; - export 'engine/html/path/path_iterator.dart'; - export 'engine/html/path/path_metrics.dart'; - export 'engine/html/path/path_ref.dart'; - export 'engine/html/path/path_to_svg.dart'; - export 'engine/html/path/path_utils.dart'; - export 'engine/html/path/path_windings.dart'; - export 'engine/html/path/tangent.dart'; - export 'engine/html/path_to_svg_clip.dart'; - export 'engine/html/picture.dart'; - export 'engine/html/platform_view.dart'; - export 'engine/html/recording_canvas.dart'; - export 'engine/html/render_vertices.dart'; - export 'engine/html/scene.dart'; - export 'engine/html/scene_builder.dart'; - export 'engine/html/shader_mask.dart'; - export 'engine/html/shaders/image_shader.dart'; - export 'engine/html/shaders/normalized_gradient.dart'; - export 'engine/html/shaders/shader.dart'; - export 'engine/html/shaders/shader_builder.dart'; - export 'engine/html/shaders/vertex_shaders.dart'; - -export 'engine/html/shaders/webgl_context.dart'; - export 'engine/html/surface.dart'; - export 'engine/html/surface_stats.dart'; - export 'engine/html/transform.dart'; - export 'engine/html_image_codec.dart'; - +export 'engine/initialization.dart'; export 'engine/key_map.dart'; - export 'engine/keyboard.dart'; - export 'engine/keyboard_binding.dart'; - export 'engine/mouse_cursor.dart'; - export 'engine/navigation/history.dart'; - export 'engine/navigation/js_url_strategy.dart'; - export 'engine/navigation/url_strategy.dart'; - export 'engine/onscreen_logging.dart'; - export 'engine/picture.dart'; - export 'engine/platform_dispatcher.dart'; - export 'engine/platform_views.dart'; - export 'engine/platform_views/content_manager.dart'; - export 'engine/platform_views/message_handler.dart'; - export 'engine/platform_views/slots.dart'; - export 'engine/plugins.dart'; - export 'engine/pointer_binding.dart'; - export 'engine/pointer_converter.dart'; - export 'engine/profiler.dart'; - export 'engine/rrect_renderer.dart'; - +export 'engine/safe_browser_api.dart'; export 'engine/semantics/accessibility.dart'; - export 'engine/semantics/checkable.dart'; - export 'engine/semantics/image.dart'; - export 'engine/semantics/incrementable.dart'; - export 'engine/semantics/label_and_value.dart'; - export 'engine/semantics/live_region.dart'; - export 'engine/semantics/scrollable.dart'; - export 'engine/semantics/semantics.dart'; - export 'engine/semantics/semantics_helper.dart'; - export 'engine/semantics/tappable.dart'; - export 'engine/semantics/text_field.dart'; - export 'engine/services/buffers.dart'; - export 'engine/services/message_codec.dart'; - export 'engine/services/message_codecs.dart'; - export 'engine/services/serialization.dart'; - export 'engine/shadow.dart'; - export 'engine/test_embedding.dart'; - export 'engine/text/canvas_paragraph.dart'; - export 'engine/text/font_collection.dart'; - export 'engine/text/layout_service.dart'; - export 'engine/text/line_break_properties.dart'; - export 'engine/text/line_breaker.dart'; - export 'engine/text/measurement.dart'; - export 'engine/text/paint_service.dart'; - export 'engine/text/paragraph.dart'; - export 'engine/text/ruler.dart'; - export 'engine/text/text_direction.dart'; - export 'engine/text/unicode_range.dart'; - export 'engine/text/word_break_properties.dart'; - export 'engine/text/word_breaker.dart'; - export 'engine/text_editing/autofill_hint.dart'; - export 'engine/text_editing/input_type.dart'; - export 'engine/text_editing/text_capitalization.dart'; - export 'engine/text_editing/text_editing.dart'; - export 'engine/util.dart'; - export 'engine/validators.dart'; - export 'engine/vector_math.dart'; - -export 'engine/web_experiments.dart'; - export 'engine/window.dart'; - -/// The mode the app is running in. -/// Keep these in sync with the same constants on the framework-side under foundation/constants.dart. -const bool kReleaseMode = - bool.fromEnvironment('dart.vm.product', defaultValue: false); -/// A constant that is true if the application was compiled in profile mode. -const bool kProfileMode = - bool.fromEnvironment('dart.vm.profile', defaultValue: false); -/// A constant that is true if the application was compiled in debug mode. -const bool kDebugMode = !kReleaseMode && !kProfileMode; -/// Returns mode of the app is running in as a string. -String get buildMode => kReleaseMode - ? 'release' - : kProfileMode - ? 'profile' - : 'debug'; - -/// A benchmark metric that includes frame-related computations prior to -/// submitting layer and picture operations to the underlying renderer, such as -/// HTML and CanvasKit. During this phase we compute transforms, clips, and -/// other information needed for rendering. -const String kProfilePrerollFrame = 'preroll_frame'; - -/// A benchmark metric that includes submitting layer and picture information -/// to the renderer. -const String kProfileApplyFrame = 'apply_frame'; - -bool _engineInitialized = false; - -final List _hotRestartListeners = []; - -/// Requests that [listener] is called just before hot restarting the app. -void registerHotRestartListener(ui.VoidCallback listener) { - _hotRestartListeners.add(listener); -} - -/// Pretends that hot restart is about to happen. -/// -/// Useful in tests to check that the engine performs appropriate clean-ups, -/// such as removing static DOM listeners, prior to allowing the Dart runtime -/// to re-initialize the program. -void debugEmulateHotRestart() { - for (final ui.VoidCallback listener in _hotRestartListeners) { - listener(); - } -} - -/// This method performs one-time initialization of the Web environment that -/// supports the Flutter framework. -/// -/// This is only available on the Web, as native Flutter configures the -/// environment in the native embedder. -void initializeEngine() { - if (_engineInitialized) { - return; - } - - // Setup the hook that allows users to customize URL strategy before running - // the app. - _addUrlStrategyListener(); - - // Called by the Web runtime just before hot restarting the app. - // - // This extension cleans up resources that are registered with browser's - // global singletons that Dart compiler is unable to clean-up automatically. - // - // This extension does not need to clean-up Dart statics. Those are cleaned - // up by the compiler. - developer.registerExtension('ext.flutter.disassemble', (_, __) { - for (final ui.VoidCallback listener in _hotRestartListeners) { - listener(); - } - return Future.value( - developer.ServiceExtensionResponse.result('OK')); - }); - - _engineInitialized = true; - - // Initialize the FlutterViewEmbedder before initializing framework bindings. - ensureFlutterViewEmbedderInitialized(); - - WebExperiments.ensureInitialized(); - - if (Profiler.isBenchmarkMode) { - Profiler.ensureInitialized(); - } - - bool waitingForAnimation = false; - scheduleFrameCallback = () { - // We're asked to schedule a frame and call `frameHandler` when the frame - // fires. - if (!waitingForAnimation) { - waitingForAnimation = true; - html.window.requestAnimationFrame((num highResTime) { - frameTimingsOnVsync(); - - // Reset immediately, because `frameHandler` can schedule more frames. - waitingForAnimation = false; - - // We have to convert high-resolution time to `int` so we can construct - // a `Duration` out of it. However, high-res time is supplied in - // milliseconds as a double value, with sub-millisecond information - // hidden in the fraction. So we first multiply it by 1000 to uncover - // microsecond precision, and only then convert to `int`. - final int highResTimeMicroseconds = (1000 * highResTime).toInt(); - - // In Flutter terminology "building a frame" consists of "beginning - // frame" and "drawing frame". - // - // We do not call `frameTimingsOnBuildFinish` from here because - // part of the rasterization process, particularly in the HTML - // renderer, takes place in the `SceneBuilder.build()`. - frameTimingsOnBuildStart(); - if (EnginePlatformDispatcher.instance.onBeginFrame != null) { - EnginePlatformDispatcher.instance.invokeOnBeginFrame( - Duration(microseconds: highResTimeMicroseconds)); - } - - if (EnginePlatformDispatcher.instance.onDrawFrame != null) { - // TODO(yjbanov): technically Flutter flushes microtasks between - // onBeginFrame and onDrawFrame. We don't, which hasn't - // been an issue yet, but eventually we'll have to - // implement it properly. - EnginePlatformDispatcher.instance.invokeOnDrawFrame(); - } - }); - } - }; - - Keyboard.initialize(); - MouseCursor.initialize(); -} - -void _addUrlStrategyListener() { - jsSetUrlStrategy = allowInterop((JsUrlStrategy? jsStrategy) { - customUrlStrategy = - jsStrategy == null ? null : CustomUrlStrategy.fromJs(jsStrategy); - }); - registerHotRestartListener(() { - jsSetUrlStrategy = null; - }); -} - -/// The shared instance of PlatformViewManager shared across the engine to handle -/// rendering of PlatformViews into the web app. -// TODO(dit): How to make this overridable from tests? -final PlatformViewManager platformViewManager = PlatformViewManager(); - -/// Converts a matrix represented using [Float64List] to one represented using -/// [Float32List]. -/// -/// 32-bit precision is sufficient because Flutter Engine itself (as well as -/// Skia) use 32-bit precision under the hood anyway. -/// -/// 32-bit matrices require 2x less memory and in V8 they are allocated on the -/// JavaScript heap, thus avoiding a malloc. -/// -/// See also: -/// * https://bugs.chromium.org/p/v8/issues/detail?id=9199 -/// * https://bugs.chromium.org/p/v8/issues/detail?id=2022 -Float32List toMatrix32(Float64List matrix64) { - final Float32List matrix32 = Float32List(16); - matrix32[15] = matrix64[15]; - matrix32[14] = matrix64[14]; - matrix32[13] = matrix64[13]; - matrix32[12] = matrix64[12]; - matrix32[11] = matrix64[11]; - matrix32[10] = matrix64[10]; - matrix32[9] = matrix64[9]; - matrix32[8] = matrix64[8]; - matrix32[7] = matrix64[7]; - matrix32[6] = matrix64[6]; - matrix32[5] = matrix64[5]; - matrix32[4] = matrix64[4]; - matrix32[3] = matrix64[3]; - matrix32[2] = matrix64[2]; - matrix32[1] = matrix64[1]; - matrix32[0] = matrix64[0]; - return matrix32; -} diff --git a/lib/web_ui/lib/src/engine/canvas_pool.dart b/lib/web_ui/lib/src/engine/canvas_pool.dart index 02bf5507ad67d..6528fae7eb346 100644 --- a/lib/web_ui/lib/src/engine/canvas_pool.dart +++ b/lib/web_ui/lib/src/engine/canvas_pool.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:html' as html; -import 'dart:js_util' as js_util; import 'dart:math' as math; import 'dart:typed_data'; @@ -23,6 +22,7 @@ import 'html/shaders/image_shader.dart'; import 'html/shaders/shader.dart'; import 'platform_dispatcher.dart'; import 'rrect_renderer.dart'; +import 'safe_browser_api.dart'; import 'shadow.dart'; import 'util.dart'; import 'vector_math.dart'; @@ -205,25 +205,12 @@ class CanvasPool extends _SaveStackTracking { } html.CanvasElement? _allocCanvas(int width, int height) { - final dynamic canvas = - // ignore: implicit_dynamic_function - js_util.callMethod(html.document, 'createElement', ['CANVAS']); - if (canvas != null) { - try { - canvas.width = (width * _density).ceil(); - canvas.height = (height * _density).ceil(); - } catch (e) { - return null; - } - return canvas as html.CanvasElement; - } - return null; - // !!! We don't use the code below since NNBD assumes it can never return - // null and optimizes out code. - // return canvas = html.CanvasElement( - // width: _widthInBitmapPixels, - // height: _heightInBitmapPixels, - // ); + // The dartdocs for `tryCreateCanvasElement` on why we don't use the + // `html.CanvasElement` constructor. + return tryCreateCanvasElement( + (width * _density).ceil(), + (height * _density).ceil(), + ); } @override 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 e228e81ff98ed..3eb49efe23e03 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -2162,3 +2162,64 @@ class SkPartialImageInfo { external int get height; external int get width; } + +/// Monkey-patch the top-level `module` and `exports` objects so that +/// CanvasKit doesn't attempt to register itself as an anonymous module. +/// +/// The idea behind making these fake `exports` and `module` objects is +/// that `canvaskit.js` contains the following lines of code: +/// +/// if (typeof exports === 'object' && typeof module === 'object') +/// module.exports = CanvasKitInit; +/// else if (typeof define === 'function' && define['amd']) +/// define([], function() { return CanvasKitInit; }); +/// +/// We need to avoid hitting the case where CanvasKit defines an anonymous +/// module, since this breaks RequireJS, which DDC and some plugins use. +/// Temporarily removing the `define` function won't work because RequireJS +/// could load in between this code running and the CanvasKit code running. +/// Also, we cannot monkey-patch the `define` function because it is +/// non-configurable (it is a top-level 'var'). +// TODO(hterkelsen): Rather than this monkey-patch hack, we should +// build CanvasKit ourselves. See: +// https://github.com/flutter/flutter/issues/52588 +void patchCanvasKitModule(html.ScriptElement canvasKitScript) { + // First check if `exports` and `module` are already defined. If so, then + // CommonJS is being used, and we shouldn't have any problems. + final js.JsFunction objectConstructor = js.context['Object'] as js.JsFunction; + if (js.context['exports'] == null) { + final js.JsObject exportsAccessor = js.JsObject.jsify({ + 'get': allowInterop(() { + if (html.document.currentScript == canvasKitScript) { + return js.JsObject(objectConstructor); + } else { + return js.context['_flutterWebCachedExports']; + } + }), + 'set': allowInterop((dynamic value) { + js.context['_flutterWebCachedExports'] = value; + }), + 'configurable': true, + }); + objectConstructor.callMethod( + 'defineProperty', [js.context, 'exports', exportsAccessor]); + } + if (js.context['module'] == null) { + final js.JsObject moduleAccessor = js.JsObject.jsify({ + 'get': allowInterop(() { + if (html.document.currentScript == canvasKitScript) { + return js.JsObject(objectConstructor); + } else { + return js.context['_flutterWebCachedModule']; + } + }), + 'set': allowInterop((dynamic value) { + js.context['_flutterWebCachedModule'] = value; + }), + 'configurable': true, + }); + objectConstructor.callMethod( + 'defineProperty', [js.context, 'module', moduleAccessor]); + } + html.document.head!.append(canvasKitScript); +} diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart index e6ad228641ea8..e51d71c9f1d93 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -9,6 +9,7 @@ import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; import '../html_image_codec.dart'; +import '../safe_browser_api.dart'; import '../util.dart'; import 'canvaskit_api.dart'; import 'image_wasm_codecs.dart'; diff --git a/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart b/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart index 02d06cb550f07..48af52b6cbd1e 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart @@ -7,36 +7,19 @@ /// See also: /// /// * `image_wasm_codecs.dart`, which uses codecs supplied by the CanvasKit WASM bundle. -@JS() -library image_web_codecs; import 'dart:async'; import 'dart:html' as html; -import 'dart:js_util' as js_util; import 'dart:math' as math; import 'dart:typed_data'; -import 'package:js/js.dart'; import 'package:ui/ui.dart' as ui; -import '../browser_detection.dart'; +import '../safe_browser_api.dart'; import '../util.dart'; import 'canvaskit_api.dart'; import 'image.dart'; -@JS('window.ImageDecoder') -external Object? get _imageDecoderConstructor; - -/// Whether the current browser supports `ImageDecoder`. -bool browserSupportsImageDecoder = - _imageDecoderConstructor != null && browserEngine == BrowserEngine.blink; - -/// Sets the value of [browserSupportsImageDecoder] to its default value. -void debugResetBrowserSupportsImageDecoder() { - browserSupportsImageDecoder = - _imageDecoderConstructor != null; -} - /// Image decoder backed by the browser's `ImageDecoder`. class CkBrowserImageDecoder implements ui.Codec { static Future create({ @@ -64,7 +47,7 @@ class CkBrowserImageDecoder implements ui.Codec { } try { - final _ImageDecoder webDecoder = _ImageDecoder(_ImageDecoderOptions( + final ImageDecoder webDecoder = ImageDecoder(ImageDecoderOptions( type: contentType, data: data, @@ -82,13 +65,13 @@ class CkBrowserImageDecoder implements ui.Codec { preferAnimation: true, )); - await js_util.promiseToFuture(webDecoder.tracks.ready); + await promiseToFuture(webDecoder.tracks.ready); // Flutter doesn't have an API for progressive loading of images, so we // wait until the image is fully decoded. // package:js bindings don't work with getters that return a Promise, which // is why js_util is used instead. - await js_util.promiseToFuture(js_util.getProperty(webDecoder, 'completed')); + await promiseToFuture(getJsProperty(webDecoder, 'completed')); return CkBrowserImageDecoder._(webDecoder, debugSource); } catch (error) { if (error is html.DomException) { @@ -109,7 +92,7 @@ class CkBrowserImageDecoder implements ui.Codec { CkBrowserImageDecoder._(this.webDecoder, this.debugSource); - final _ImageDecoder webDecoder; + final ImageDecoder webDecoder; final String debugSource; /// Whether this decoded has been disposed of. @@ -145,10 +128,10 @@ class CkBrowserImageDecoder implements ui.Codec { @override Future getNextFrame() async { _debugCheckNotDisposed(); - final _DecodeResult result = await js_util.promiseToFuture<_DecodeResult>( - webDecoder.decode(_DecodeOptions(frameIndex: _nextFrameIndex)), + final DecodeResult result = await promiseToFuture( + webDecoder.decode(DecodeOptions(frameIndex: _nextFrameIndex)), ); - final _VideoFrame frame = result.image; + final VideoFrame frame = result.image; _nextFrameIndex = (_nextFrameIndex + 1) % frameCount; final SkImage? skImage = canvasKit.MakeLazyImageFromTextureSource( @@ -184,99 +167,6 @@ class CkBrowserImageDecoder implements ui.Codec { } } -/// Corresponds to JavaScript's `Promise`. -/// -/// This type doesn't need any members. Instead, it should be first converted -/// to Dart's [Future] using [promiseToFuture] then interacted with through the -/// [Future] API. -@JS() -@anonymous -class JsPromise {} - -/// Corresponds to the browser's `ImageDecoder` type. -/// -/// See also: -/// -/// * https://www.w3.org/TR/webcodecs/#imagedecoder-interface -@JS('window.ImageDecoder') -class _ImageDecoder { - external _ImageDecoder(_ImageDecoderOptions options); - external _ImageTrackList get tracks; - external bool get complete; - external JsPromise decode(_DecodeOptions options); - external void close(); -} - -/// The result of [_ImageDecoder.decode]. -/// -/// See also: -/// -/// * https://www.w3.org/TR/webcodecs/#imagedecoderesult-interface -@JS() -@anonymous -class _DecodeResult { - external _VideoFrame get image; - external bool get complete; -} - -/// Options passed to [_ImageDecoder.decode]. -/// -/// See also: -/// -/// * https://www.w3.org/TR/webcodecs/#dictdef-imagedecodeoptions -@JS() -@anonymous -class _DecodeOptions { - external factory _DecodeOptions({ - required int frameIndex, - }); -} - -/// The only frame in a static image, or one of the frames in an animated one. -/// -/// This class maps to the `VideoFrame` type provided by the browser. -/// -/// See also: -/// -/// * https://www.w3.org/TR/webcodecs/#videoframe-interface -@JS() -@anonymous -class _VideoFrame { - external int allocationSize(); - external JsPromise copyTo(Uint8List destination); - external String? get format; - external int get codedWidth; - external int get codedHeight; - external int get displayWidth; - external int get displayHeight; - external int? get duration; - external void close(); -} - -/// Corresponds to the browser's `ImageTrackList` type. -/// -/// See also: -/// -/// * https://www.w3.org/TR/webcodecs/#imagetracklist-interface -@JS() -@anonymous -class _ImageTrackList { - external JsPromise get ready; - external _ImageTrack? get selectedTrack; -} - -/// Corresponds to the browser's `ImageTrack` type. -/// -/// See also: -/// -/// * https://www.w3.org/TR/webcodecs/#imagetrack -@JS() -@anonymous -class _ImageTrack { - external int get repetitionCount; - external int get frameCount; -} - /// Represents an image file format, such as PNG or JPEG. class ImageFileFormat { const ImageFileFormat(this.header, this.contentType); @@ -412,22 +302,3 @@ bool isAvif(Uint8List data) { } return false; } - -/// Options passed to the `ImageDecoder` constructor. -/// -/// See also: -/// -/// * https://www.w3.org/TR/webcodecs/#imagedecoderinit-interface -@JS() -@anonymous -class _ImageDecoderOptions { - external factory _ImageDecoderOptions({ - required String type, - required Uint8List data, - required String premultiplyAlpha, - required int? desiredWidth, - required int? desiredHeight, - required String colorSpaceConversion, - required bool preferAnimation, - }); -} diff --git a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart index 87b77f6d9d154..4eb26dc596933 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart @@ -1,27 +1,19 @@ // 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. -@JS() library canvaskit_initialization; import 'dart:async'; import 'dart:html' as html; -import 'dart:js' as js; - -import 'package:js/js.dart'; import '../../engine.dart' show kProfileMode; import '../browser_detection.dart'; import '../configuration.dart'; import '../embedder.dart'; +import '../safe_browser_api.dart'; import 'canvaskit_api.dart'; import 'fonts.dart'; -/// A JavaScript entrypoint that allows developer to set rendering backend -/// at runtime before launching the application. -@JS('window.flutterWebRenderer') -external String? get requestedRendererType; - /// Whether to use CanvasKit as the rendering backend. bool get useCanvasKit => FlutterConfiguration.flutterWebAutoDetect ? _detectRenderer() : FlutterConfiguration.useSkia; @@ -65,10 +57,10 @@ Future initializeCanvasKit({String? canvasKitBase}) { _canvasKitLoaded!.then((_) { final CanvasKitInitPromise canvasKitInitPromise = CanvasKitInit(CanvasKitInitOptions( - locateFile: js.allowInterop( + locateFile: allowInterop( (String file, String unusedBase) => canvasKitWasmModuleUrl(file)), )); - canvasKitInitPromise.then(js.allowInterop((CanvasKit ck) { + canvasKitInitPromise.then(allowInterop((CanvasKit ck) { canvasKit = ck; windowFlutterCanvasKit = canvasKit; canvasKitCompleter.complete(); @@ -104,66 +96,7 @@ void _startDownloadingCanvasKit(String? canvasKitBase) { canvasKitLoadCompleter.complete(); }); - // TODO(hterkelsen): Rather than this monkey-patch hack, we should - // build CanvasKit ourselves. See: - // https://github.com/flutter/flutter/issues/52588 - - // Monkey-patch the top-level `module` and `exports` objects so that - // CanvasKit doesn't attempt to register itself as an anonymous module. - // - // The idea behind making these fake `exports` and `module` objects is - // that `canvaskit.js` contains the following lines of code: - // - // if (typeof exports === 'object' && typeof module === 'object') - // module.exports = CanvasKitInit; - // else if (typeof define === 'function' && define['amd']) - // define([], function() { return CanvasKitInit; }); - // - // We need to avoid hitting the case where CanvasKit defines an anonymous - // module, since this breaks RequireJS, which DDC and some plugins use. - // Temporarily removing the `define` function won't work because RequireJS - // could load in between this code running and the CanvasKit code running. - // Also, we cannot monkey-patch the `define` function because it is - // non-configurable (it is a top-level 'var'). - - // First check if `exports` and `module` are already defined. If so, then - // CommonJS is being used, and we shouldn't have any problems. - final js.JsFunction objectConstructor = js.context['Object'] as js.JsFunction; - if (js.context['exports'] == null) { - final js.JsObject exportsAccessor = js.JsObject.jsify({ - 'get': js.allowInterop(() { - if (html.document.currentScript == _canvasKitScript) { - return js.JsObject(objectConstructor); - } else { - return js.context['_flutterWebCachedExports']; - } - }), - 'set': js.allowInterop((dynamic value) { - js.context['_flutterWebCachedExports'] = value; - }), - 'configurable': true, - }); - objectConstructor.callMethod( - 'defineProperty', [js.context, 'exports', exportsAccessor]); - } - if (js.context['module'] == null) { - final js.JsObject moduleAccessor = js.JsObject.jsify({ - 'get': js.allowInterop(() { - if (html.document.currentScript == _canvasKitScript) { - return js.JsObject(objectConstructor); - } else { - return js.context['_flutterWebCachedModule']; - } - }), - 'set': js.allowInterop((dynamic value) { - js.context['_flutterWebCachedModule'] = value; - }), - 'configurable': true, - }); - objectConstructor.callMethod( - 'defineProperty', [js.context, 'module', moduleAccessor]); - } - html.document.head!.append(_canvasKitScript!); + patchCanvasKitModule(_canvasKitScript!); } } diff --git a/lib/web_ui/lib/src/engine/configuration.dart b/lib/web_ui/lib/src/engine/configuration.dart index b0e3f8e202e40..f2bc94fbcddba 100644 --- a/lib/web_ui/lib/src/engine/configuration.dart +++ b/lib/web_ui/lib/src/engine/configuration.dart @@ -169,3 +169,8 @@ class JsFlutterConfiguration { external int? get canvasKitMaximumSurfaces; external bool? get debugShowSemanticsNodes; } + +/// A JavaScript entrypoint that allows developer to set rendering backend +/// at runtime before launching the application. +@JS('window.flutterWebRenderer') +external String? get requestedRendererType; diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index fe22f42982a89..6a27d910916cc 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:html' as html; -import 'dart:js_util' as js_util; import 'package:ui/ui.dart' as ui; @@ -16,6 +15,7 @@ import 'host_node.dart'; import 'keyboard_binding.dart'; import 'platform_dispatcher.dart'; import 'pointer_binding.dart'; +import 'safe_browser_api.dart'; import 'semantics.dart'; import 'text_editing/text_editing.dart'; import 'util.dart'; @@ -104,26 +104,12 @@ class FlutterViewEmbedder { static const String _staleHotRestartStore = '__flutter_state'; List? _staleHotRestartState; - /// Used to decide if the browser tab still has the focus. - /// - /// This information is useful for deciding on the blur behavior. - /// See [DefaultTextEditingStrategy]. - /// - /// This getter calls the `hasFocus` method of the `Document` interface. - /// See for more details: - /// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus - bool get windowHasFocus => - // ignore: implicit_dynamic_function - js_util.callMethod(html.document, 'hasFocus', []) as bool; - void _setupHotRestart() { // This persists across hot restarts to clear stale DOM. - _staleHotRestartState = - // ignore: implicit_dynamic_function - js_util.getProperty(html.window, _staleHotRestartStore) as List?; + _staleHotRestartState = getJsProperty?>(html.window, _staleHotRestartStore); if (_staleHotRestartState == null) { _staleHotRestartState = []; - js_util.setProperty( + setJsProperty( html.window, _staleHotRestartStore, _staleHotRestartState); } @@ -366,8 +352,7 @@ class FlutterViewEmbedder { // Creates a [HostNode] into a `root` [html.Element]. HostNode _createHostNode(html.Element root) { - // ignore: implicit_dynamic_function - if (js_util.getProperty(root, 'attachShadow') != null) { + if (getJsProperty(root, 'attachShadow') != null) { return ShadowDomHostNode(root); } else { // attachShadow not available, fall back to ElementHostNode. diff --git a/lib/web_ui/lib/src/engine/html/offscreen_canvas.dart b/lib/web_ui/lib/src/engine/html/offscreen_canvas.dart deleted file mode 100644 index fb7bbfb6c27a0..0000000000000 --- a/lib/web_ui/lib/src/engine/html/offscreen_canvas.dart +++ /dev/null @@ -1,103 +0,0 @@ -// 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 'dart:async'; -import 'dart:html' as html; -import 'dart:js_util' as js_util; - -import '../platform_dispatcher.dart'; - -/// Polyfill for html.OffscreenCanvas that is not supported on some browsers. -class OffScreenCanvas { - html.OffscreenCanvas? offScreenCanvas; - html.CanvasElement? canvasElement; - int width; - int height; - static bool? _supported; - - OffScreenCanvas(this.width, this.height) { - if (OffScreenCanvas.supported) { - offScreenCanvas = html.OffscreenCanvas(width, height); - } else { - canvasElement = html.CanvasElement( - width: width, - height: height, - ); - canvasElement!.className = 'gl-canvas'; - final double cssWidth = width / EnginePlatformDispatcher.browserDevicePixelRatio; - final double cssHeight = height / EnginePlatformDispatcher.browserDevicePixelRatio; - canvasElement!.style - ..position = 'absolute' - ..width = '${cssWidth}px' - ..height = '${cssHeight}px'; - } - } - - void dispose() { - offScreenCanvas = null; - canvasElement = null; - } - - /// Returns CanvasRenderContext2D or OffscreenCanvasRenderingContext2D to - /// paint into. - Object? getContext2d() { - return offScreenCanvas != null - ? offScreenCanvas!.getContext('2d') - : canvasElement!.getContext('2d'); - } - - /// Feature detection for transferToImageBitmap on OffscreenCanvas. - bool get transferToImageBitmapSupported => - js_util.hasProperty(offScreenCanvas!, 'transferToImageBitmap'); - - /// Creates an ImageBitmap object from the most recently rendered image - /// of the OffscreenCanvas. - /// - /// !Warning API still in experimental status, feature detect before using. - Object? transferToImageBitmap() { - return js_util.callMethod(offScreenCanvas!, 'transferToImageBitmap', - []); - } - - /// Draws canvas contents to a rendering context. - void transferImage(Object targetContext) { - // Actual size of canvas may be larger than viewport size. Use - // source/destination to draw part of the image data. - // ignore: implicit_dynamic_function - js_util.callMethod(targetContext, 'drawImage', - [offScreenCanvas ?? canvasElement!, 0, 0, width, height, - 0, 0, width, height]); - } - - /// Converts canvas contents to an image and returns as data URL. - Future toDataUrl() { - final Completer completer = Completer(); - if (offScreenCanvas != null) { - offScreenCanvas!.convertToBlob().then((html.Blob value) { - final html.FileReader fileReader = html.FileReader(); - fileReader.onLoad.listen((html.ProgressEvent event) { - completer.complete( - // ignore: implicit_dynamic_function - js_util.getProperty(js_util.getProperty(event, 'target') as Object, 'result') as String, - ); - }); - fileReader.readAsDataUrl(value); - }); - return completer.future; - } else { - return Future.value(canvasElement!.toDataUrl()); - } - } - - /// Draws an image to canvas for both offscreen canvas canvas context2d. - void drawImage(Object image, int x, int y, int width, int height) { - // ignore: implicit_dynamic_function - js_util.callMethod( - getContext2d()!, 'drawImage', [image, x, y, width, height]); - } - - /// Feature detects OffscreenCanvas. - static bool get supported => _supported ??= - js_util.hasProperty(html.window, 'OffscreenCanvas'); -} diff --git a/lib/web_ui/lib/src/engine/html/path/path_ref.dart b/lib/web_ui/lib/src/engine/html/path/path_ref.dart index 200fe9d45f4fb..1cbc082a836a4 100644 --- a/lib/web_ui/lib/src/engine/html/path/path_ref.dart +++ b/lib/web_ui/lib/src/engine/html/path/path_ref.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:js_util' as js_util; import 'dart:math' as math; import 'dart:typed_data'; @@ -429,10 +428,8 @@ class PathRef { resetToSize(verbCount, pointCount, weightCount, additionalReserveVerbs, additionalReservePoints); - // ignore: implicit_dynamic_function - js_util.callMethod(_fVerbs, 'set', [ref._fVerbs]); - // ignore: implicit_dynamic_function - js_util.callMethod(fPoints, 'set', [ref.fPoints]); + _fVerbs.setAll(0, ref._fVerbs); + fPoints.setAll(0, ref.fPoints); if (ref._conicWeights == null) { _conicWeights = null; } else { @@ -458,8 +455,7 @@ class PathRef { if (newLength > _fPointsCapacity) { _fPointsCapacity = newLength + 10; final Float32List newPoints = Float32List(_fPointsCapacity * 2); - // ignore: implicit_dynamic_function - js_util.callMethod(newPoints, 'set', [fPoints]); + newPoints.setAll(0, fPoints); fPoints = newPoints; } _fPointsLength = newLength; @@ -469,8 +465,7 @@ class PathRef { if (newLength > _fVerbsCapacity) { _fVerbsCapacity = newLength + 8; final Uint8List newVerbs = Uint8List(_fVerbsCapacity); - // ignore: implicit_dynamic_function - js_util.callMethod(newVerbs, 'set', [_fVerbs]); + newVerbs.setAll(0, _fVerbs); _fVerbs = newVerbs; } _fVerbsLength = newLength; @@ -481,8 +476,7 @@ class PathRef { _conicWeightsCapacity = newLength + 4; final Float32List newWeights = Float32List(_conicWeightsCapacity); if (_conicWeights != null) { - // ignore: implicit_dynamic_function - js_util.callMethod(newWeights, 'set', [_conicWeights]); + newWeights.setAll(0, _conicWeights!); } _conicWeights = newWeights; } diff --git a/lib/web_ui/lib/src/engine/html/render_vertices.dart b/lib/web_ui/lib/src/engine/html/render_vertices.dart index 2795443441283..73b612d84c23d 100644 --- a/lib/web_ui/lib/src/engine/html/render_vertices.dart +++ b/lib/web_ui/lib/src/engine/html/render_vertices.dart @@ -3,13 +3,13 @@ // found in the LICENSE file. import 'dart:html' as html; -import 'dart:js_util' as js_util; import 'dart:math' as math; import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; import '../browser_detection.dart'; +import '../safe_browser_api.dart'; import '../util.dart'; import '../vector_math.dart'; import 'painting.dart'; @@ -17,7 +17,6 @@ import 'shaders/image_shader.dart'; import 'shaders/normalized_gradient.dart'; import 'shaders/shader_builder.dart'; import 'shaders/vertex_shaders.dart'; -import 'shaders/webgl_context.dart'; GlRenderer? glRenderer; @@ -204,15 +203,15 @@ class _WebGlRenderer implements GlRenderer { bufferVertexData(gl, positions, 1.0); // Setup data format for attribute. - // ignore: implicit_dynamic_function - js_util.callMethod(gl.glContext, 'vertexAttribPointer', [ + vertexAttribPointerGlContext( + gl.glContext, positionAttributeLocation, 2, gl.kFloat, false, 0, 0, - ]); + ); final int vertexCount = positions.length ~/ 2; Object? texture; @@ -234,9 +233,15 @@ class _WebGlRenderer implements GlRenderer { gl.bufferData(vertices.colors, gl.kStaticDraw); } final Object colorLoc = gl.getAttributeLocation(glProgram.program, 'color'); - // ignore: implicit_dynamic_function - js_util.callMethod(gl.glContext, 'vertexAttribPointer', - [colorLoc, 4, gl.kUnsignedByte, true, 0, 0]); + vertexAttribPointerGlContext( + gl.glContext, + colorLoc, + 4, + gl.kUnsignedByte, + true, + 0, + 0, + ); gl.enableVertexAttribArray(colorLoc); } else { // Copy image it to the texture. @@ -376,9 +381,15 @@ class _WebGlRenderer implements GlRenderer { gl.bindArrayBuffer(positionsBuffer); gl.bufferData(vertices, gl.kStaticDraw); // Point an attribute to the currently bound vertex buffer object. - // ignore: implicit_dynamic_function - js_util.callMethod(gl.glContext, 'vertexAttribPointer', - [0, 2, gl.kFloat, false, 0, 0]); + vertexAttribPointerGlContext( + gl.glContext, + 0, + 2, + gl.kFloat, + false, + 0, + 0, + ); gl.enableVertexAttribArray(0); // Setup color buffer. @@ -392,9 +403,15 @@ class _WebGlRenderer implements GlRenderer { 0xFF00FFFF, ]); gl.bufferData(colors, gl.kStaticDraw); - // ignore: implicit_dynamic_function - js_util.callMethod(gl.glContext, 'vertexAttribPointer', - [1, 4, gl.kUnsignedByte, true, 0, 0]); + vertexAttribPointerGlContext( + gl.glContext, + 1, + 4, + gl.kUnsignedByte, + true, + 0, + 0, + ); gl.enableVertexAttribArray(1); final Object? indexBuffer = gl.createBuffer(); diff --git a/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart b/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart index a5f85b1dd7b88..86aeb0a3e92f0 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/image_shader.dart @@ -3,18 +3,16 @@ // found in the LICENSE file. import 'dart:html' as html; -import 'dart:js_util' as js_util; import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; import '../../browser_detection.dart'; import '../../html_image_codec.dart'; +import '../../safe_browser_api.dart'; import '../../vector_math.dart'; -import '../offscreen_canvas.dart'; import '../render_vertices.dart'; import 'vertex_shaders.dart'; -import 'webgl_context.dart'; class EngineImageShader implements ui.ImageShader { EngineImageShader(ui.Image image, this.tileModeX, this.tileModeY, @@ -95,19 +93,17 @@ class EngineImageShader implements ui.ImageShader { /// To draw image flipped we set translate and scale and pass /// negative width/height to drawImage. if (flipX != 1 || flipY != 1) { - // ignore: implicit_dynamic_function - js_util.callMethod(renderContext, 'scale', [flipX, flipY]); + scaleCanvas2D(renderContext, flipX, flipY); } - // ignore: implicit_dynamic_function - js_util.callMethod(renderContext, 'drawImage', [ + drawImageCanvas2D( + renderContext, image.imgElement, - if (x == 0) 0 else -2 * imageWidth, - if (y == 0) 0 else -2 * imageHeight, - ]); + x == 0 ? 0 : -2 * imageWidth, + y == 0 ? 0 : -2 * imageHeight, + ); if (flipX != 1 || flipY != 1) { /// Restore transform. This is faster than save/restore on context. - // ignore: implicit_dynamic_function - js_util.callMethod(renderContext, 'scale', [flipX, flipY]); + scaleCanvas2D(renderContext, flipX, flipY); } } } @@ -208,15 +204,15 @@ class EngineImageShader implements ui.ImageShader { bufferVertexData(gl, vertices, ui.window.devicePixelRatio); /// Setup data format for attribute. - // ignore: implicit_dynamic_function - js_util.callMethod(gl.glContext, 'vertexAttribPointer', [ + vertexAttribPointerGlContext( + gl.glContext, positionAttributeLocation, 2, gl.kFloat, false, 0, 0, - ]); + ); /// Copy image to the texture. final Object? texture = gl.createTexture(); diff --git a/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart b/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart index a7f5b16923eaf..6cba78f11d6a7 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/normalized_gradient.dart @@ -6,8 +6,8 @@ import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; +import '../../safe_browser_api.dart'; import 'shader_builder.dart'; -import 'webgl_context.dart'; /// Converts colors and stops to typed array of bias, scale and threshold to use /// in shaders. diff --git a/lib/web_ui/lib/src/engine/html/shaders/shader.dart b/lib/web_ui/lib/src/engine/html/shaders/shader.dart index a8d0948641818..0ae291470b0f1 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/shader.dart +++ b/lib/web_ui/lib/src/engine/html/shaders/shader.dart @@ -9,16 +9,15 @@ import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; import '../../browser_detection.dart'; +import '../../safe_browser_api.dart'; import '../../util.dart'; import '../../validators.dart'; import '../../vector_math.dart'; -import '../offscreen_canvas.dart'; import '../path/path_utils.dart'; import '../render_vertices.dart'; import 'normalized_gradient.dart'; import 'shader_builder.dart'; import 'vertex_shaders.dart'; -import 'webgl_context.dart'; const double kFltEpsilon = 1.19209290E-07; // == 1 / (2 ^ 23) const double kFltEpsilonSquared = 1.19209290E-07 * 1.19209290E-07; diff --git a/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart b/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart deleted file mode 100644 index 7b346a045b903..0000000000000 --- a/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart +++ /dev/null @@ -1,619 +0,0 @@ -// 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 'dart:html' as html; -import 'dart:js_util' as js_util; -import 'dart:math' as math; -import 'dart:typed_data'; - -import 'package:ui/ui.dart' as ui; - -import '../../browser_detection.dart'; -import '../../vector_math.dart'; -import '../offscreen_canvas.dart'; - -/// Compiled and cached gl program. -class GlProgram { - final Object program; - GlProgram(this.program); -} - -/// JS Interop helper for webgl apis. -class GlContext { - final Object glContext; - final bool isOffscreen; - dynamic _kCompileStatus; - dynamic _kArrayBuffer; - dynamic _kElementArrayBuffer; - dynamic _kStaticDraw; - dynamic _kFloat; - dynamic _kColorBufferBit; - dynamic _kTexture2D; - dynamic _kTextureWrapS; - dynamic _kTextureWrapT; - dynamic _kRepeat; - dynamic _kClampToEdge; - dynamic _kMirroredRepeat; - dynamic _kTriangles; - dynamic _kLinkStatus; - dynamic _kUnsignedByte; - dynamic _kUnsignedShort; - dynamic _kRGBA; - dynamic _kLinear; - dynamic _kTextureMinFilter; - int? _kTexture0; - - Object? _canvas; - int? _widthInPixels; - int? _heightInPixels; - static late Map _programCache; - - factory GlContext(OffScreenCanvas offScreenCanvas) { - return OffScreenCanvas.supported - ? GlContext._fromOffscreenCanvas(offScreenCanvas.offScreenCanvas!) - : GlContext._fromCanvasElement( - offScreenCanvas.canvasElement!, webGLVersion == WebGLVersion.webgl1); - } - - GlContext._fromOffscreenCanvas(html.OffscreenCanvas canvas) - : glContext = canvas.getContext('webgl2', {'premultipliedAlpha': false})!, - isOffscreen = true { - _programCache = {}; - _canvas = canvas; - } - - GlContext._fromCanvasElement(html.CanvasElement canvas, bool useWebGl1) - : glContext = canvas.getContext(useWebGl1 ? 'webgl' : 'webgl2', - {'premultipliedAlpha': false})!, - isOffscreen = false { - _programCache = {}; - _canvas = canvas; - } - - void setViewportSize(int width, int height) { - _widthInPixels = width; - _heightInPixels = height; - } - - /// Draws Gl context contents to canvas context. - void drawImage(html.CanvasRenderingContext2D context, - double left, double top) { - // Actual size of canvas may be larger than viewport size. Use - // source/destination to draw part of the image data. - // ignore: implicit_dynamic_function - js_util.callMethod(context, 'drawImage', - [_canvas, 0, 0, _widthInPixels, _heightInPixels, - left, top, _widthInPixels, _heightInPixels]); - } - - GlProgram cacheProgram( - String vertexShaderSource, String fragmentShaderSource) { - final String cacheKey = '$vertexShaderSource||$fragmentShaderSource'; - GlProgram? cachedProgram = _programCache[cacheKey]; - if (cachedProgram == null) { - // Create and compile shaders. - final Object vertexShader = compileShader('VERTEX_SHADER', vertexShaderSource); - final Object fragmentShader = - compileShader('FRAGMENT_SHADER', fragmentShaderSource); - // Create a gl program and link shaders. - final Object program = createProgram(); - attachShader(program, vertexShader); - attachShader(program, fragmentShader); - linkProgram(program); - cachedProgram = GlProgram(program); - _programCache[cacheKey] = cachedProgram; - } - return cachedProgram; - } - - Object compileShader(String shaderType, String source) { - final Object? shader = _createShader(shaderType); - if (shader == null) { - throw Exception(error); - } - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'shaderSource', [shader, source]); - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'compileShader', [shader]); - // ignore: implicit_dynamic_function - final bool shaderStatus = js_util.callMethod( - glContext, - 'getShaderParameter', - [shader, compileStatus], - ) as bool; - if (!shaderStatus) { - throw Exception('Shader compilation failed: ${getShaderInfoLog(shader)}'); - } - return shader; - } - Object createProgram() => - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'createProgram', const []) as Object; - - void attachShader(Object? program, Object shader) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'attachShader', [program, shader]); - } - - void linkProgram(Object program) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'linkProgram', [program]); - // ignore: implicit_dynamic_function - final bool programStatus = js_util.callMethod( - glContext, - 'getProgramParameter', - [program, kLinkStatus], - ) as bool; - if (!programStatus) { - throw Exception(getProgramInfoLog(program)); - } - } - - void useProgram(GlProgram program) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'useProgram', [program.program]); - } - - Object? createBuffer() => - js_util.callMethod(glContext, 'createBuffer', const []); - - void bindArrayBuffer(Object? buffer) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'bindBuffer', [kArrayBuffer, buffer]); - } - - Object? createVertexArray() => - js_util.callMethod(glContext, 'createVertexArray', const []); - - void bindVertexArray(Object vertexObjectArray) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'bindVertexArray', - [vertexObjectArray]); - } - - void unbindVertexArray() { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'bindVertexArray', - [null]); - } - - void bindElementArrayBuffer(Object? buffer) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'bindBuffer', [kElementArrayBuffer, buffer]); - } - - Object? createTexture() => - js_util.callMethod(glContext, 'createTexture', const []); - - void generateMipmap(dynamic target) => - js_util.callMethod(glContext, 'generateMipmap', [target]); - - void bindTexture(dynamic target, Object? buffer) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'bindTexture', [target, buffer]); - } - - void activeTexture(int textureUnit) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'activeTexture', [textureUnit]); - } - - void texImage2D(dynamic target, int level, dynamic internalFormat, - dynamic format, dynamic dataType, - dynamic pixels, {int? width, int? height, int border = 0}) { - if (width == null) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'texImage2D', [ - target, level, internalFormat, format, dataType, pixels]); - } else { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'texImage2D', [ - target, level, internalFormat, width, height, border, format, dataType, - pixels]); - } - } - - void texParameteri(dynamic target, dynamic parameterName, dynamic value) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'texParameteri', [ - target, parameterName, value]); - } - - void deleteBuffer(Object buffer) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'deleteBuffer', [buffer]); - } - - void bufferData(TypedData? data, dynamic type) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'bufferData', [kArrayBuffer, data, type]); - } - - void bufferElementData(TypedData? data, dynamic type) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'bufferData', [kElementArrayBuffer, data, type]); - } - - void enableVertexAttribArray(dynamic index) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'enableVertexAttribArray', [index]); - } - - /// Clear background. - void clear() { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'clear', [kColorBufferBit]); - } - - /// Destroys gl context. - void dispose() { - // ignore: implicit_dynamic_function - js_util.callMethod( - _getExtension('WEBGL_lose_context') as Object, - 'loseContext', - const [], - ); - } - - void deleteProgram(Object program) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'deleteProgram', [program]); - } - - void deleteShader(Object shader) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'deleteShader', [shader]); - } - - dynamic _getExtension(String extensionName) => - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'getExtension', [extensionName]); - - void drawTriangles(int triangleCount, ui.VertexMode vertexMode) { - final dynamic mode = _triangleTypeFromMode(vertexMode); - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'drawArrays', [mode, 0, triangleCount]); - } - - void drawElements(dynamic type, int indexCount, dynamic indexType) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'drawElements', [type, indexCount, indexType, 0]); - } - - /// Sets affine transformation from normalized device coordinates - /// to window coordinates - void viewport(double x, double y, double width, double height) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'viewport', [x, y, width, height]); - } - - dynamic _triangleTypeFromMode(ui.VertexMode mode) { - switch (mode) { - case ui.VertexMode.triangles: - return kTriangles; - case ui.VertexMode.triangleFan: - return kTriangleFan; - case ui.VertexMode.triangleStrip: - return kTriangleStrip; - } - } - - Object? _createShader(String shaderType) => js_util.callMethod( - // ignore: implicit_dynamic_function - glContext, 'createShader', [js_util.getProperty(glContext, shaderType)]); - - /// Error state of gl context. - // ignore: implicit_dynamic_function - dynamic get error => js_util.callMethod(glContext, 'getError', const []); - - /// Shader compiler error, if this returns [kFalse], to get details use - /// [getShaderInfoLog]. - dynamic get compileStatus => - // ignore: implicit_dynamic_function - _kCompileStatus ??= js_util.getProperty(glContext, 'COMPILE_STATUS'); - - dynamic get kArrayBuffer => - // ignore: implicit_dynamic_function - _kArrayBuffer ??= js_util.getProperty(glContext, 'ARRAY_BUFFER'); - - dynamic get kElementArrayBuffer => - // ignore: implicit_dynamic_function - _kElementArrayBuffer ??= js_util.getProperty(glContext, - 'ELEMENT_ARRAY_BUFFER'); - - dynamic get kLinkStatus => - // ignore: implicit_dynamic_function - _kLinkStatus ??= js_util.getProperty(glContext, 'LINK_STATUS'); - - // ignore: implicit_dynamic_function - dynamic get kFloat => _kFloat ??= js_util.getProperty(glContext, 'FLOAT'); - - // ignore: implicit_dynamic_function - dynamic get kRGBA => _kRGBA ??= js_util.getProperty(glContext, 'RGBA'); - - dynamic get kUnsignedByte => - // ignore: implicit_dynamic_function - _kUnsignedByte ??= js_util.getProperty(glContext, 'UNSIGNED_BYTE'); - - dynamic get kUnsignedShort => - // ignore: implicit_dynamic_function - _kUnsignedShort ??= js_util.getProperty(glContext, 'UNSIGNED_SHORT'); - - dynamic get kStaticDraw => - // ignore: implicit_dynamic_function - _kStaticDraw ??= js_util.getProperty(glContext, 'STATIC_DRAW'); - - dynamic get kTriangles => - // ignore: implicit_dynamic_function - _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLES'); - - dynamic get kTriangleFan => - // ignore: implicit_dynamic_function - _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_FAN'); - - dynamic get kTriangleStrip => - // ignore: implicit_dynamic_function - _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_STRIP'); - - dynamic get kColorBufferBit => - // ignore: implicit_dynamic_function - _kColorBufferBit ??= js_util.getProperty(glContext, 'COLOR_BUFFER_BIT'); - - dynamic get kTexture2D => - // ignore: implicit_dynamic_function - _kTexture2D ??= js_util.getProperty(glContext, 'TEXTURE_2D'); - - int get kTexture0 => - // ignore: implicit_dynamic_function - _kTexture0 ??= js_util.getProperty(glContext, 'TEXTURE0') as int; - - dynamic get kTextureWrapS => - // ignore: implicit_dynamic_function - _kTextureWrapS ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_S'); - - dynamic get kTextureWrapT => - // ignore: implicit_dynamic_function - _kTextureWrapT ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_T'); - - dynamic get kRepeat => - // ignore: implicit_dynamic_function - _kRepeat ??= js_util.getProperty(glContext, 'REPEAT'); - - dynamic get kClampToEdge => - // ignore: implicit_dynamic_function - _kClampToEdge ??= js_util.getProperty(glContext, 'CLAMP_TO_EDGE'); - - dynamic get kMirroredRepeat => - // ignore: implicit_dynamic_function - _kMirroredRepeat ??= js_util.getProperty(glContext, 'MIRRORED_REPEAT'); - - dynamic get kLinear => - // ignore: implicit_dynamic_function - _kLinear ??= js_util.getProperty(glContext, 'LINEAR'); - - dynamic get kTextureMinFilter => - // ignore: implicit_dynamic_function - _kTextureMinFilter ??= js_util.getProperty(glContext, - 'TEXTURE_MIN_FILTER'); - - /// Returns reference to uniform in program. - Object getUniformLocation(Object program, String uniformName) { - final Object? res = js_util - .callMethod(glContext, 'getUniformLocation', [program, uniformName]); - if (res == null) { - throw Exception('$uniformName not found'); - } else { - return res; - } - } - - /// Returns true if uniform exists. - bool containsUniform(Object program, String uniformName) { - final Object? res = js_util - .callMethod(glContext, 'getUniformLocation', [program, uniformName]); - return res != null; - } - - /// Returns reference to uniform in program. - Object getAttributeLocation(Object program, String attribName) { - final Object? res = js_util - .callMethod(glContext, 'getAttribLocation', [program, attribName]); - if (res == null) { - throw Exception('$attribName not found'); - } else { - return res; - } - } - - /// Sets float uniform value. - void setUniform1f(Object uniform, double value) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'uniform1f', [uniform, value]); - } - - /// Sets vec2 uniform values. - void setUniform2f(Object uniform, double value1, double value2) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'uniform2f', [uniform, value1, value2]); - } - - /// Sets vec4 uniform values. - void setUniform4f(Object uniform, double value1, double value2, double value3, - double value4) { - // ignore: implicit_dynamic_function - js_util.callMethod( - glContext, 'uniform4f', [uniform, value1, value2, value3, value4]); - } - - /// Sets mat4 uniform values. - void setUniformMatrix4fv(Object uniform, bool transpose, Float32List value) { - // ignore: implicit_dynamic_function - js_util.callMethod( - glContext, 'uniformMatrix4fv', [uniform, transpose, value]); - } - - /// Shader compile error log. - dynamic getShaderInfoLog(Object glShader) { - // ignore: implicit_dynamic_function - return js_util.callMethod(glContext, 'getShaderInfoLog', [glShader]); - } - - /// Errors that occurred during failed linking or validation of program - /// objects. Typically called after [linkProgram]. - String? getProgramInfoLog(Object glProgram) { - // ignore: implicit_dynamic_function - return js_util.callMethod(glContext, 'getProgramInfoLog', [glProgram]) as String?; - } - - int? get drawingBufferWidth => - // ignore: implicit_dynamic_function - js_util.getProperty(glContext, 'drawingBufferWidth') as int?; - int? get drawingBufferHeight => - // ignore: implicit_dynamic_function - js_util.getProperty(glContext, 'drawingBufferWidth') as int?; - - /// Reads gl contents as image data. - /// - /// Warning: data is read bottom up (flipped). - html.ImageData readImageData() { - const int kBytesPerPixel = 4; - final int bufferWidth = _widthInPixels!; - final int bufferHeight = _heightInPixels!; - if (browserEngine == BrowserEngine.webkit || - browserEngine == BrowserEngine.firefox) { - final Uint8List pixels = - Uint8List(bufferWidth * bufferHeight * kBytesPerPixel); - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'readPixels', - [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); - return html.ImageData( - Uint8ClampedList.fromList(pixels), bufferWidth, bufferHeight); - } else { - final Uint8ClampedList pixels = - Uint8ClampedList(bufferWidth * bufferHeight * kBytesPerPixel); - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'readPixels', - [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); - return html.ImageData(pixels, bufferWidth, bufferHeight); - } - } - - /// Returns image data in a form that can be used to create Canvas - /// context patterns. - Object? readPatternData() { - // When using OffscreenCanvas and transferToImageBitmap is supported by - // browser create ImageBitmap otherwise use more expensive canvas - // allocation. - if (_canvas != null && - js_util.hasProperty(_canvas!, 'transferToImageBitmap')) { - // ignore: implicit_dynamic_function - js_util.callMethod(_canvas!, 'getContext', ['webgl2']); - final Object? imageBitmap = js_util.callMethod(_canvas!, 'transferToImageBitmap', - []); - return imageBitmap; - } else { - final html.CanvasElement canvas = html.CanvasElement(width: _widthInPixels, height: _heightInPixels); - final html.CanvasRenderingContext2D ctx = canvas.context2D; - drawImage(ctx, 0, 0); - return canvas; - } - } - - /// Returns image data in data url format. - String toImageUrl() { - final html.CanvasElement canvas = html.CanvasElement(width: _widthInPixels, height: _heightInPixels); - final html.CanvasRenderingContext2D ctx = canvas.context2D; - drawImage(ctx, 0, 0); - final String dataUrl = canvas.toDataUrl(); - canvas.width = 0; - canvas.height = 0; - return dataUrl; - } -} - -// ignore: avoid_classes_with_only_static_members -/// Creates gl context from cached OffscreenCanvas for webgl rendering to image. -class GlContextCache { - static int _maxPixelWidth = 0; - static int _maxPixelHeight = 0; - static GlContext? _cachedContext; - static OffScreenCanvas? _offScreenCanvas; - - static void dispose() { - _maxPixelWidth = 0; - _maxPixelHeight = 0; - _cachedContext = null; - _offScreenCanvas?.dispose(); - } - - static GlContext? createGlContext(int widthInPixels, int heightInPixels) { - if (widthInPixels > _maxPixelWidth || heightInPixels > _maxPixelHeight) { - _cachedContext?.dispose(); - _cachedContext = null; - _offScreenCanvas = null; - _maxPixelWidth = math.max(_maxPixelWidth, widthInPixels); - _maxPixelHeight = math.max(_maxPixelHeight, widthInPixels); - } - _offScreenCanvas ??= OffScreenCanvas(widthInPixels, heightInPixels); - _cachedContext ??= GlContext(_offScreenCanvas!); - _cachedContext!.setViewportSize(widthInPixels, heightInPixels); - return _cachedContext; - } -} - -void setupVertexTransforms( - GlContext gl, - GlProgram glProgram, - double offsetX, - double offsetY, - double widthInPixels, - double heightInPixels, - Matrix4 transform) { - final Object transformUniform = - gl.getUniformLocation(glProgram.program, 'u_ctransform'); - final Matrix4 transformAtOffset = transform.clone() - ..translate(-offsetX, -offsetY); - gl.setUniformMatrix4fv(transformUniform, false, transformAtOffset.storage); - - // Set uniform to scale 0..width/height pixels coordinates to -1..1 - // clipspace range and flip the Y axis. - final Object resolution = gl.getUniformLocation(glProgram.program, 'u_scale'); - gl.setUniform4f(resolution, 2.0 / widthInPixels.toDouble(), - -2.0 / heightInPixels.toDouble(), 1, 1); - final Object shift = gl.getUniformLocation(glProgram.program, 'u_shift'); - gl.setUniform4f(shift, -1, 1, 0, 0); -} - -void setupTextureTransform( - GlContext gl, GlProgram glProgram, double offsetx, double offsety, double sx, double sy) { - final Object scalar = gl.getUniformLocation(glProgram.program, 'u_textransform'); - gl.setUniform4f(scalar, sx, sy, offsetx, offsety); -} - -void bufferVertexData(GlContext gl, Float32List positions, - double devicePixelRatio) { - if (devicePixelRatio == 1.0) { - gl.bufferData(positions, gl.kStaticDraw); - } else { - final int length = positions.length; - final Float32List scaledList = Float32List(length); - for (int i = 0; i < length; i++) { - scaledList[i] = positions[i] * devicePixelRatio; - } - gl.bufferData(scaledList, gl.kStaticDraw); - } -} - -dynamic tileModeToGlWrapping(GlContext gl, ui.TileMode tileMode) { - switch (tileMode) { - case ui.TileMode.clamp: - return gl.kClampToEdge; - case ui.TileMode.decal: - return gl.kClampToEdge; - case ui.TileMode.mirror: - return gl.kMirroredRepeat; - case ui.TileMode.repeated: - return gl.kRepeat; - } -} diff --git a/lib/web_ui/lib/src/engine/html_image_codec.dart b/lib/web_ui/lib/src/engine/html_image_codec.dart index 234fa1fec8126..7acd8f89a6fcb 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -4,21 +4,19 @@ import 'dart:async'; import 'dart:html' as html; -import 'dart:js_util' as js_util; import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; import 'browser_detection.dart'; +import 'safe_browser_api.dart'; import 'util.dart'; -Object? get _jsImageDecodeFunction => js_util.getProperty( - // ignore: implicit_dynamic_function - js_util.getProperty( - // ignore: implicit_dynamic_function - js_util.getProperty(html.window, 'Image') as Object, +Object? get _jsImageDecodeFunction => getJsProperty( + getJsProperty( + getJsProperty(html.window, 'Image'), 'prototype', - ) as Object, + ), 'decode', ); final bool _supportsDecode = _jsImageDecodeFunction != null; @@ -48,7 +46,7 @@ class HtmlCodec implements ui.Codec { if (_supportsDecode) { final html.ImageElement imgElement = html.ImageElement(); imgElement.src = src; - js_util.setProperty(imgElement, 'decoding', 'async'); + setJsProperty(imgElement, 'decoding', 'async'); imgElement.decode().then((dynamic _) { chunkCallback?.call(100, 100); int naturalWidth = imgElement.naturalWidth; diff --git a/lib/web_ui/lib/src/engine/initialization.dart b/lib/web_ui/lib/src/engine/initialization.dart new file mode 100644 index 0000000000000..8018f14e01325 --- /dev/null +++ b/lib/web_ui/lib/src/engine/initialization.dart @@ -0,0 +1,196 @@ +// 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 'dart:developer' as developer; +import 'dart:html' as html; +import 'dart:typed_data'; + +import 'package:ui/src/engine/embedder.dart'; +import 'package:ui/src/engine/keyboard.dart'; +import 'package:ui/src/engine/mouse_cursor.dart'; +import 'package:ui/src/engine/navigation.dart'; +import 'package:ui/src/engine/platform_dispatcher.dart'; +import 'package:ui/src/engine/platform_views/content_manager.dart'; +import 'package:ui/src/engine/profiler.dart'; +import 'package:ui/src/engine/safe_browser_api.dart'; +import 'package:ui/src/engine/window.dart'; +import 'package:ui/ui.dart' as ui; + +/// The mode the app is running in. +/// Keep these in sync with the same constants on the framework-side under foundation/constants.dart. +const bool kReleaseMode = + bool.fromEnvironment('dart.vm.product', defaultValue: false); +/// A constant that is true if the application was compiled in profile mode. +const bool kProfileMode = + bool.fromEnvironment('dart.vm.profile', defaultValue: false); +/// A constant that is true if the application was compiled in debug mode. +const bool kDebugMode = !kReleaseMode && !kProfileMode; +/// Returns mode of the app is running in as a string. +String get buildMode => kReleaseMode + ? 'release' + : kProfileMode + ? 'profile' + : 'debug'; + +/// A benchmark metric that includes frame-related computations prior to +/// submitting layer and picture operations to the underlying renderer, such as +/// HTML and CanvasKit. During this phase we compute transforms, clips, and +/// other information needed for rendering. +const String kProfilePrerollFrame = 'preroll_frame'; + +/// A benchmark metric that includes submitting layer and picture information +/// to the renderer. +const String kProfileApplyFrame = 'apply_frame'; + +bool _engineInitialized = false; + +final List _hotRestartListeners = []; + +/// Requests that [listener] is called just before hot restarting the app. +void registerHotRestartListener(ui.VoidCallback listener) { + _hotRestartListeners.add(listener); +} + +/// Pretends that hot restart is about to happen. +/// +/// Useful in tests to check that the engine performs appropriate clean-ups, +/// such as removing static DOM listeners, prior to allowing the Dart runtime +/// to re-initialize the program. +void debugEmulateHotRestart() { + for (final ui.VoidCallback listener in _hotRestartListeners) { + listener(); + } +} + +/// This method performs one-time initialization of the Web environment that +/// supports the Flutter framework. +/// +/// This is only available on the Web, as native Flutter configures the +/// environment in the native embedder. +void initializeEngine() { + if (_engineInitialized) { + return; + } + + // Setup the hook that allows users to customize URL strategy before running + // the app. + _addUrlStrategyListener(); + + // Called by the Web runtime just before hot restarting the app. + // + // This extension cleans up resources that are registered with browser's + // global singletons that Dart compiler is unable to clean-up automatically. + // + // This extension does not need to clean-up Dart statics. Those are cleaned + // up by the compiler. + developer.registerExtension('ext.flutter.disassemble', (_, __) { + for (final ui.VoidCallback listener in _hotRestartListeners) { + listener(); + } + return Future.value( + developer.ServiceExtensionResponse.result('OK')); + }); + + _engineInitialized = true; + + // Initialize the FlutterViewEmbedder before initializing framework bindings. + ensureFlutterViewEmbedderInitialized(); + + if (Profiler.isBenchmarkMode) { + Profiler.ensureInitialized(); + } + + bool waitingForAnimation = false; + scheduleFrameCallback = () { + // We're asked to schedule a frame and call `frameHandler` when the frame + // fires. + if (!waitingForAnimation) { + waitingForAnimation = true; + html.window.requestAnimationFrame((num highResTime) { + frameTimingsOnVsync(); + + // Reset immediately, because `frameHandler` can schedule more frames. + waitingForAnimation = false; + + // We have to convert high-resolution time to `int` so we can construct + // a `Duration` out of it. However, high-res time is supplied in + // milliseconds as a double value, with sub-millisecond information + // hidden in the fraction. So we first multiply it by 1000 to uncover + // microsecond precision, and only then convert to `int`. + final int highResTimeMicroseconds = (1000 * highResTime).toInt(); + + // In Flutter terminology "building a frame" consists of "beginning + // frame" and "drawing frame". + // + // We do not call `frameTimingsOnBuildFinish` from here because + // part of the rasterization process, particularly in the HTML + // renderer, takes place in the `SceneBuilder.build()`. + frameTimingsOnBuildStart(); + if (EnginePlatformDispatcher.instance.onBeginFrame != null) { + EnginePlatformDispatcher.instance.invokeOnBeginFrame( + Duration(microseconds: highResTimeMicroseconds)); + } + + if (EnginePlatformDispatcher.instance.onDrawFrame != null) { + // TODO(yjbanov): technically Flutter flushes microtasks between + // onBeginFrame and onDrawFrame. We don't, which hasn't + // been an issue yet, but eventually we'll have to + // implement it properly. + EnginePlatformDispatcher.instance.invokeOnDrawFrame(); + } + }); + } + }; + + Keyboard.initialize(); + MouseCursor.initialize(); +} + +void _addUrlStrategyListener() { + jsSetUrlStrategy = allowInterop((JsUrlStrategy? jsStrategy) { + customUrlStrategy = + jsStrategy == null ? null : CustomUrlStrategy.fromJs(jsStrategy); + }); + registerHotRestartListener(() { + jsSetUrlStrategy = null; + }); +} + +/// The shared instance of PlatformViewManager shared across the engine to handle +/// rendering of PlatformViews into the web app. +// TODO(dit): How to make this overridable from tests? +final PlatformViewManager platformViewManager = PlatformViewManager(); + +/// Converts a matrix represented using [Float64List] to one represented using +/// [Float32List]. +/// +/// 32-bit precision is sufficient because Flutter Engine itself (as well as +/// Skia) use 32-bit precision under the hood anyway. +/// +/// 32-bit matrices require 2x less memory and in V8 they are allocated on the +/// JavaScript heap, thus avoiding a malloc. +/// +/// See also: +/// * https://bugs.chromium.org/p/v8/issues/detail?id=9199 +/// * https://bugs.chromium.org/p/v8/issues/detail?id=2022 +Float32List toMatrix32(Float64List matrix64) { + final Float32List matrix32 = Float32List(16); + matrix32[15] = matrix64[15]; + matrix32[14] = matrix64[14]; + matrix32[13] = matrix64[13]; + matrix32[12] = matrix64[12]; + matrix32[11] = matrix64[11]; + matrix32[10] = matrix64[10]; + matrix32[9] = matrix64[9]; + matrix32[8] = matrix64[8]; + matrix32[7] = matrix64[7]; + matrix32[6] = matrix64[6]; + matrix32[5] = matrix64[5]; + matrix32[4] = matrix64[4]; + matrix32[3] = matrix64[3]; + matrix32[2] = matrix64[2]; + matrix32[1] = matrix64[1]; + matrix32[0] = matrix64[0]; + return matrix32; +} diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 7620518f95842..cf87ac56869bd 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:html' as html; -import 'dart:js_util' as js_util; import 'dart:typed_data'; import 'package:meta/meta.dart'; @@ -22,6 +21,7 @@ import 'mouse_cursor.dart'; import 'platform_views/message_handler.dart'; import 'plugins.dart'; import 'profiler.dart'; +import 'safe_browser_api.dart'; import 'semantics.dart'; import 'services.dart'; import 'text_editing/text_editing.dart'; @@ -420,7 +420,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { return; case 'HapticFeedback.vibrate': final String? type = decoded.arguments as String?; - _vibrate(_getHapticFeedbackDuration(type)); + vibrate(_getHapticFeedbackDuration(type)); replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); return; case 'SystemChrome.setApplicationSwitcherDescription': @@ -1091,44 +1091,6 @@ const double _defaultRootFontSize = 16.0; /// Finds the text scale factor of the browser by looking at the computed style /// of the browser's element. double findBrowserTextScaleFactor() { - final num fontSize = _parseFontSize(html.document.documentElement!) ?? _defaultRootFontSize; + final num fontSize = parseFontSize(html.document.documentElement!) ?? _defaultRootFontSize; return fontSize / _defaultRootFontSize; } - -/// Parses the font size of [element] and returns the value without a unit. -num? _parseFontSize(html.Element element) { - num? fontSize; - - if (js_util.hasProperty(element, 'computedStyleMap')) { - // Use the newer `computedStyleMap` API available on some browsers. - final dynamic computedStyleMap = - // ignore: implicit_dynamic_function - js_util.callMethod(element, 'computedStyleMap', []); - if (computedStyleMap is Object) { - final dynamic fontSizeObject = - // ignore: implicit_dynamic_function - js_util.callMethod(computedStyleMap, 'get', ['font-size']); - if (fontSizeObject is Object) { - // ignore: implicit_dynamic_function - fontSize = js_util.getProperty(fontSizeObject, 'value') as num; - } - } - } - - if (fontSize == null) { - // Fallback to `getComputedStyle`. - final String fontSizeString = element.getComputedStyle().fontSize; - fontSize = parseFloat(fontSizeString); - } - - return fontSize; -} - -/// Provides haptic feedback. -void _vibrate(int durationMs) { - final html.Navigator navigator = html.window.navigator; - if (js_util.hasProperty(navigator, 'vibrate')) { - // ignore: implicit_dynamic_function - js_util.callMethod(navigator, 'vibrate', [durationMs]); - } -} diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 71aec20b0ae7c..d81100f3913a2 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -3,8 +3,6 @@ // found in the LICENSE file. import 'dart:html' as html; -import 'dart:js' as js; -import 'dart:js_util' as js_util; import 'dart:math' as math; import 'package:meta/meta.dart'; @@ -14,6 +12,7 @@ import '../engine.dart' show registerHotRestartListener; import 'browser_detection.dart'; import 'platform_dispatcher.dart'; import 'pointer_converter.dart'; +import 'safe_browser_api.dart'; import 'semantics.dart'; /// Set this flag to true to see all the fired events in the console. @@ -143,9 +142,9 @@ class PointerBinding { class PointerSupportDetector { const PointerSupportDetector(); - bool get hasPointerEvents => js_util.hasProperty(html.window, 'PointerEvent'); - bool get hasTouchEvents => js_util.hasProperty(html.window, 'TouchEvent'); - bool get hasMouseEvents => js_util.hasProperty(html.window, 'MouseEvent'); + bool get hasPointerEvents => hasJsProperty(html.window, 'PointerEvent'); + bool get hasTouchEvents => hasJsProperty(html.window, 'TouchEvent'); + bool get hasMouseEvents => hasJsProperty(html.window, 'MouseEvent'); @override String toString() => @@ -180,14 +179,7 @@ abstract class _BaseAdapter { // For native listener, we will need to remove it through native javascript // api. _nativeListeners.forEach((String eventName, html.EventListener listener) { - // ignore: implicit_dynamic_function - js_util.callMethod( - glassPaneElement, - 'removeEventListener', [ - 'wheel', - listener, - ] - ); + removeJsEventListener(glassPaneElement, 'wheel', listener); }); _listeners.clear(); _nativeListeners.clear(); @@ -294,20 +286,12 @@ mixin _WheelEventListenerMixin on _BaseAdapter { } void _addWheelEventListener(html.EventListener handler) { - // ignore: implicit_dynamic_function - final Object eventOptions = js_util.newObject() as Object; - final html.EventListener jsHandler = js.allowInterop((html.Event event) => handler(event)); + final Object eventOptions = createPlainJsObject({ + 'passive': false, + }); + final html.EventListener jsHandler = allowInterop((html.Event event) => handler(event)); _BaseAdapter._nativeListeners['wheel'] = jsHandler; - js_util.setProperty(eventOptions, 'passive', false); - // ignore: implicit_dynamic_function - js_util.callMethod( - glassPaneElement, - 'addEventListener', [ - 'wheel', - jsHandler, - eventOptions - ] - ); + addJsEventListener(glassPaneElement, 'wheel', jsHandler, eventOptions); } void _handleWheelEvent(html.Event e) { @@ -618,7 +602,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { List _expandEvents(html.PointerEvent event) { // For browsers that don't support `getCoalescedEvents`, we fallback to // using the original event. - if (js_util.hasProperty(event, 'getCoalescedEvents')) { + if (hasJsProperty(event, 'getCoalescedEvents')) { final List coalescedEvents = event.getCoalescedEvents().cast(); // Some events don't perform coalescing, so they return an empty list. In diff --git a/lib/web_ui/lib/src/engine/profiler.dart b/lib/web_ui/lib/src/engine/profiler.dart index 59a9466e1f0c8..369c47489e570 100644 --- a/lib/web_ui/lib/src/engine/profiler.dart +++ b/lib/web_ui/lib/src/engine/profiler.dart @@ -4,14 +4,11 @@ import 'dart:async'; import 'dart:html' as html; -import 'dart:js_util' as js_util; import 'package:ui/ui.dart' as ui; import 'platform_dispatcher.dart'; - -/// A function that receives a benchmark [value] labeleb by [name]. -typedef OnBenchmark = void Function(String name, double value); +import 'safe_browser_api.dart'; /// A function that computes a value of type [R]. /// @@ -105,12 +102,16 @@ class Profiler { void benchmark(String name, double value) { _checkBenchmarkMode(); - final OnBenchmark? onBenchmark = - // ignore: implicit_dynamic_function - js_util.getProperty(html.window, '_flutter_internal_on_benchmark') as OnBenchmark?; - if (onBenchmark != null) { - onBenchmark(name, value); - } + // First get the value as `Object?` then use `as` cast to check the type. + // This is because the type cast in `getJsProperty` is optimized + // out at certain optimization levels in dart2js, leading to obscure errors + // later on. + final Object? onBenchmark = getJsProperty( + html.window, + '_flutter_internal_on_benchmark', + ); + onBenchmark as OnBenchmark?; + onBenchmark?.call(name, value); } } diff --git a/lib/web_ui/lib/src/engine/safe_browser_api.dart b/lib/web_ui/lib/src/engine/safe_browser_api.dart new file mode 100644 index 0000000000000..4e5b17ff077a8 --- /dev/null +++ b/lib/web_ui/lib/src/engine/safe_browser_api.dart @@ -0,0 +1,1007 @@ +// 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. + +/// JavaScript API bindings for browser APIs. +/// +/// The public surface of this API must be safe to use. In particular, using the +/// API of this library it must not be possible to execute arbitrary code from +/// strings by injecting it into HTML or URLs. + +@JS() +library browser_api; + +import 'dart:async'; +import 'dart:html' as html; +import 'dart:js' as js; +import 'dart:js_util' as js_util; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:js/js.dart'; +import 'package:ui/ui.dart' as ui; + +import 'browser_detection.dart'; +import 'platform_dispatcher.dart'; +import 'vector_math.dart'; + +/// Creates JavaScript object populated with [properties]. +/// +/// This is equivalent to writing `{}` in plain JavaScript. +Object createPlainJsObject([Map? properties]) { + if (properties != null) { + return js.JsObject.jsify(properties); + } else { + return js_util.newObject(); + } +} + +/// Returns true if [object] has property [name], false otherwise. +/// +/// This is equivalent to writing `name in object` in plain JavaScript. +bool hasJsProperty(Object object, String name) { + return js_util.hasProperty(object, name); +} + +/// Returns the value of property [name] from a JavaScript [object]. +/// +/// This is equivalent to writing `object.name` in plain JavaScript. +T getJsProperty(Object object, String name) { + return js_util.getProperty(object, name); +} + +const Set _safeJsProperties = { + 'decoding', + '__flutter_state', +}; + +/// Sets the value of property [name] on a JavaScript [object]. +/// +/// This is equivalent to writing `object.name = value` in plain JavaScript. +T setJsProperty(Object object, String name, T value) { + assert( + _safeJsProperties.contains(name), + 'Attempted to set property "$name" on a JavaScript object. This property ' + 'has not been checked for safety. Possible solutions to this problem:\n' + ' - Do not set this property.\n' + ' - Use a `dart:html` API that does the same thing.\n' + ' - Ensure that the property is safe then add it to _safeJsProperties set.', + ); + return js_util.setProperty(object, name, value); +} + +/// Wraps function [f] to be callable from JavaScript. +F allowInterop(F f) { + return js.allowInterop(f); +} + +/// Converts a JavaScript `Promise` into Dart [Future]. +Future promiseToFuture(Object jsPromise) { + return js_util.promiseToFuture(jsPromise); +} + +/// A function that receives a benchmark [value] labeleb by [name]. +typedef OnBenchmark = void Function(String name, double value); + +/// Adds an event [listener] of type [type] to the [target]. +/// +/// [eventOptions] supply additional configuration parameters. +/// +/// This is different from [html.Element.addEventListener] in that the listener +/// is added as a plain JavaScript function, as opposed to a Dart function. +/// +/// To remove the listener, call [removeJsEventListener]. +void addJsEventListener(Object target, String type, Function listener, Object eventOptions) { + js_util.callMethod( + target, + 'addEventListener', [ + type, + listener, + eventOptions, + ] + ); +} + +/// Removes an event listener that was added using [addJsEventListener]. +void removeJsEventListener(Object target, String type, Function listener) { + js_util.callMethod( + target, + 'removeEventListener', [ + type, + listener, + ] + ); +} + +/// The signature of the `parseFloat` JavaScript function. +typedef _JsParseFloat = num? Function(String source); + +/// The JavaScript-side `parseFloat` function. +@JS('parseFloat') +external _JsParseFloat get _jsParseFloat; + +/// Parses a string [source] into a double. +/// +/// Uses the JavaScript `parseFloat` function instead of Dart's [double.parse] +/// because the latter can't parse strings like "20px". +/// +/// Returns null if it fails to parse. +num? parseFloat(String source) { + // Using JavaScript's `parseFloat` here because it can parse values + // like "20px", while Dart's `double.tryParse` fails. + final num? result = _jsParseFloat(source); + + if (result == null || result.isNaN) { + return null; + } + return result; +} + +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'); + +/// Used to decide if the browser tab still has the focus. +/// +/// This information is useful for deciding on the blur behavior. +/// See [DefaultTextEditingStrategy]. +/// +/// This getter calls the `hasFocus` method of the `Document` interface. +/// See for more details: +/// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus +bool get windowHasFocus => + js_util.callMethod(html.document, 'hasFocus', []); + +/// Parses the font size of [element] and returns the value without a unit. +num? parseFontSize(html.Element element) { + num? fontSize; + + if (hasJsProperty(element, 'computedStyleMap')) { + // Use the newer `computedStyleMap` API available on some browsers. + final Object? computedStyleMap = + js_util.callMethod(element, 'computedStyleMap', []); + if (computedStyleMap is Object) { + final Object? fontSizeObject = + js_util.callMethod(computedStyleMap, 'get', ['font-size']); + if (fontSizeObject is Object) { + fontSize = js_util.getProperty(fontSizeObject, 'value'); + } + } + } + + if (fontSize == null) { + // Fallback to `getComputedStyle`. + final String fontSizeString = element.getComputedStyle().fontSize; + fontSize = parseFloat(fontSizeString); + } + + return fontSize; +} + +/// Provides haptic feedback. +void vibrate(int durationMs) { + final html.Navigator navigator = html.window.navigator; + if (hasJsProperty(navigator, 'vibrate')) { + js_util.callMethod(navigator, 'vibrate', [durationMs]); + } +} + +/// Creates a `` but anticipates that the result may be null. +/// +/// The [html.CanvasElement] factory assumes that element allocation will +/// succeed and will return a non-null element. This is not always true. For +/// example, when Safari on iOS runs out of memory it returns null. +html.CanvasElement? tryCreateCanvasElement(int width, int height) { + final html.CanvasElement? canvas = js_util.callMethod( + html.document, + 'createElement', + ['CANVAS'], + ); + if (canvas != null) { + try { + canvas.width = width; + canvas.height = height; + } catch (e) { + // It seems the tribal knowledge of why we anticipate an exception while + // setting width/height on a non-null canvas and why it's OK to return + // null in this case has been lost. Kudos to the one who can recover it + // and leave a proper comment here! + return null; + } + return canvas; + } +} + +@JS('window.ImageDecoder') +external Object? get _imageDecoderConstructor; + +/// Whether the current browser supports `ImageDecoder`. +bool browserSupportsImageDecoder = + _imageDecoderConstructor != null && browserEngine == BrowserEngine.blink; + +/// Sets the value of [browserSupportsImageDecoder] to its default value. +void debugResetBrowserSupportsImageDecoder() { + browserSupportsImageDecoder = + _imageDecoderConstructor != null; +} + +/// Corresponds to JavaScript's `Promise`. +/// +/// This type doesn't need any members. Instead, it should be first converted +/// to Dart's [Future] using [promiseToFuture] then interacted with through the +/// [Future] API. +@JS() +@anonymous +class JsPromise {} + +/// Corresponds to the browser's `ImageDecoder` type. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#imagedecoder-interface +@JS('window.ImageDecoder') +class ImageDecoder { + external ImageDecoder(ImageDecoderOptions options); + external ImageTrackList get tracks; + external bool get complete; + external JsPromise decode(DecodeOptions options); + external void close(); +} + +/// Options passed to the `ImageDecoder` constructor. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#imagedecoderinit-interface +@JS() +@anonymous +class ImageDecoderOptions { + external factory ImageDecoderOptions({ + required String type, + required Uint8List data, + required String premultiplyAlpha, + required int? desiredWidth, + required int? desiredHeight, + required String colorSpaceConversion, + required bool preferAnimation, + }); +} + +/// The result of [ImageDecoder.decode]. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#imagedecoderesult-interface +@JS() +@anonymous +class DecodeResult { + external VideoFrame get image; + external bool get complete; +} + +/// Options passed to [ImageDecoder.decode]. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#dictdef-imagedecodeoptions +@JS() +@anonymous +class DecodeOptions { + external factory DecodeOptions({ + required int frameIndex, + }); +} + +/// The only frame in a static image, or one of the frames in an animated one. +/// +/// This class maps to the `VideoFrame` type provided by the browser. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#videoframe-interface +@JS() +@anonymous +class VideoFrame { + external int allocationSize(); + external JsPromise copyTo(Uint8List destination); + external String? get format; + external int get codedWidth; + external int get codedHeight; + external int get displayWidth; + external int get displayHeight; + external int? get duration; + external void close(); +} + +/// Corresponds to the browser's `ImageTrackList` type. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#imagetracklist-interface +@JS() +@anonymous +class ImageTrackList { + external JsPromise get ready; + external ImageTrack? get selectedTrack; +} + +/// Corresponds to the browser's `ImageTrack` type. +/// +/// See also: +/// +/// * https://www.w3.org/TR/webcodecs/#imagetrack +@JS() +@anonymous +class ImageTrack { + external int get repetitionCount; + external int get frameCount; +} + +void scaleCanvas2D(Object context2d, num x, num y) { + js_util.callMethod(context2d, 'scale', [x, y]); +} + +void drawImageCanvas2D(Object context2d, Object imageSource, num width, num height) { + js_util.callMethod(context2d, 'drawImage', [ + imageSource, + width, + height, + ]); +} + +void vertexAttribPointerGlContext( + Object glContext, + Object index, + num size, + Object type, + bool normalized, + num stride, + num offset, +) { + js_util.callMethod(glContext, 'vertexAttribPointer', [ + index, + size, + type, + normalized, + stride, + offset, + ]); +} + +/// Compiled and cached gl program. +class GlProgram { + final Object program; + GlProgram(this.program); +} + +/// JS Interop helper for webgl apis. +class GlContext { + final Object glContext; + final bool isOffscreen; + Object? _kCompileStatus; + Object? _kArrayBuffer; + Object? _kElementArrayBuffer; + Object? _kStaticDraw; + Object? _kFloat; + Object? _kColorBufferBit; + Object? _kTexture2D; + Object? _kTextureWrapS; + Object? _kTextureWrapT; + Object? _kRepeat; + Object? _kClampToEdge; + Object? _kMirroredRepeat; + Object? _kTriangles; + Object? _kLinkStatus; + Object? _kUnsignedByte; + Object? _kUnsignedShort; + Object? _kRGBA; + Object? _kLinear; + Object? _kTextureMinFilter; + int? _kTexture0; + + Object? _canvas; + int? _widthInPixels; + int? _heightInPixels; + static late Map _programCache; + + factory GlContext(OffScreenCanvas offScreenCanvas) { + return OffScreenCanvas.supported + ? GlContext._fromOffscreenCanvas(offScreenCanvas.offScreenCanvas!) + : GlContext._fromCanvasElement( + offScreenCanvas.canvasElement!, webGLVersion == WebGLVersion.webgl1); + } + + GlContext._fromOffscreenCanvas(html.OffscreenCanvas canvas) + : glContext = canvas.getContext('webgl2', {'premultipliedAlpha': false})!, + isOffscreen = true { + _programCache = {}; + _canvas = canvas; + } + + GlContext._fromCanvasElement(html.CanvasElement canvas, bool useWebGl1) + : glContext = canvas.getContext(useWebGl1 ? 'webgl' : 'webgl2', + {'premultipliedAlpha': false})!, + isOffscreen = false { + _programCache = {}; + _canvas = canvas; + } + + void setViewportSize(int width, int height) { + _widthInPixels = width; + _heightInPixels = height; + } + + /// Draws Gl context contents to canvas context. + void drawImage(html.CanvasRenderingContext2D context, + double left, double top) { + // Actual size of canvas may be larger than viewport size. Use + // source/destination to draw part of the image data. + js_util.callMethod(context, 'drawImage', + [_canvas, 0, 0, _widthInPixels, _heightInPixels, + left, top, _widthInPixels, _heightInPixels]); + } + + GlProgram cacheProgram( + String vertexShaderSource, String fragmentShaderSource) { + final String cacheKey = '$vertexShaderSource||$fragmentShaderSource'; + GlProgram? cachedProgram = _programCache[cacheKey]; + if (cachedProgram == null) { + // Create and compile shaders. + final Object vertexShader = compileShader('VERTEX_SHADER', vertexShaderSource); + final Object fragmentShader = + compileShader('FRAGMENT_SHADER', fragmentShaderSource); + // Create a gl program and link shaders. + final Object program = createProgram(); + attachShader(program, vertexShader); + attachShader(program, fragmentShader); + linkProgram(program); + cachedProgram = GlProgram(program); + _programCache[cacheKey] = cachedProgram; + } + return cachedProgram; + } + + Object compileShader(String shaderType, String source) { + final Object? shader = _createShader(shaderType); + if (shader == null) { + throw Exception(error); + } + js_util.callMethod(glContext, 'shaderSource', [shader, source]); + js_util.callMethod(glContext, 'compileShader', [shader]); + final bool shaderStatus = js_util.callMethod( + glContext, + 'getShaderParameter', + [shader, compileStatus], + ); + if (!shaderStatus) { + throw Exception('Shader compilation failed: ${getShaderInfoLog(shader)}'); + } + return shader; + } + Object createProgram() => + js_util.callMethod(glContext, 'createProgram', const []); + + void attachShader(Object? program, Object shader) { + js_util.callMethod(glContext, 'attachShader', [program, shader]); + } + + void linkProgram(Object program) { + js_util.callMethod(glContext, 'linkProgram', [program]); + final bool programStatus = js_util.callMethod( + glContext, + 'getProgramParameter', + [program, kLinkStatus], + ); + if (!programStatus) { + throw Exception(getProgramInfoLog(program)); + } + } + + void useProgram(GlProgram program) { + js_util.callMethod(glContext, 'useProgram', [program.program]); + } + + Object? createBuffer() => + js_util.callMethod(glContext, 'createBuffer', const []); + + void bindArrayBuffer(Object? buffer) { + js_util.callMethod(glContext, 'bindBuffer', [kArrayBuffer, buffer]); + } + + Object? createVertexArray() => + js_util.callMethod(glContext, 'createVertexArray', const []); + + void bindVertexArray(Object vertexObjectArray) { + js_util.callMethod(glContext, 'bindVertexArray', + [vertexObjectArray]); + } + + void unbindVertexArray() { + js_util.callMethod(glContext, 'bindVertexArray', + [null]); + } + + void bindElementArrayBuffer(Object? buffer) { + js_util.callMethod(glContext, 'bindBuffer', [kElementArrayBuffer, buffer]); + } + + Object? createTexture() => + js_util.callMethod(glContext, 'createTexture', const []); + + void generateMipmap(dynamic target) => + js_util.callMethod(glContext, 'generateMipmap', [target]); + + void bindTexture(dynamic target, Object? buffer) { + js_util.callMethod(glContext, 'bindTexture', [target, buffer]); + } + + void activeTexture(int textureUnit) { + js_util.callMethod(glContext, 'activeTexture', [textureUnit]); + } + + void texImage2D(dynamic target, int level, dynamic internalFormat, + dynamic format, dynamic dataType, + dynamic pixels, {int? width, int? height, int border = 0}) { + if (width == null) { + js_util.callMethod(glContext, 'texImage2D', [ + target, level, internalFormat, format, dataType, pixels]); + } else { + js_util.callMethod(glContext, 'texImage2D', [ + target, level, internalFormat, width, height, border, format, dataType, + pixels]); + } + } + + void texParameteri(dynamic target, dynamic parameterName, dynamic value) { + js_util.callMethod(glContext, 'texParameteri', [ + target, parameterName, value]); + } + + void deleteBuffer(Object buffer) { + js_util.callMethod(glContext, 'deleteBuffer', [buffer]); + } + + void bufferData(TypedData? data, dynamic type) { + js_util.callMethod(glContext, 'bufferData', [kArrayBuffer, data, type]); + } + + void bufferElementData(TypedData? data, dynamic type) { + js_util.callMethod(glContext, 'bufferData', [kElementArrayBuffer, data, type]); + } + + void enableVertexAttribArray(dynamic index) { + js_util.callMethod(glContext, 'enableVertexAttribArray', [index]); + } + + /// Clear background. + void clear() { + js_util.callMethod(glContext, 'clear', [kColorBufferBit]); + } + + /// Destroys gl context. + void dispose() { + final Object? loseContextExtension = _getExtension('WEBGL_lose_context'); + if (loseContextExtension != null) { + js_util.callMethod( + loseContextExtension, + 'loseContext', + const [], + ); + } + } + + void deleteProgram(Object program) { + js_util.callMethod(glContext, 'deleteProgram', [program]); + } + + void deleteShader(Object shader) { + js_util.callMethod(glContext, 'deleteShader', [shader]); + } + + Object? _getExtension(String extensionName) => + js_util.callMethod(glContext, 'getExtension', [extensionName]); + + void drawTriangles(int triangleCount, ui.VertexMode vertexMode) { + final dynamic mode = _triangleTypeFromMode(vertexMode); + js_util.callMethod(glContext, 'drawArrays', [mode, 0, triangleCount]); + } + + void drawElements(dynamic type, int indexCount, dynamic indexType) { + js_util.callMethod(glContext, 'drawElements', [type, indexCount, indexType, 0]); + } + + /// Sets affine transformation from normalized device coordinates + /// to window coordinates + void viewport(double x, double y, double width, double height) { + js_util.callMethod(glContext, 'viewport', [x, y, width, height]); + } + + Object _triangleTypeFromMode(ui.VertexMode mode) { + switch (mode) { + case ui.VertexMode.triangles: + return kTriangles; + case ui.VertexMode.triangleFan: + return kTriangleFan; + case ui.VertexMode.triangleStrip: + return kTriangleStrip; + } + } + + Object? _createShader(String shaderType) => js_util.callMethod( + glContext, 'createShader', [js_util.getProperty(glContext, shaderType)]); + + /// Error state of gl context. + Object? get error => js_util.callMethod(glContext, 'getError', const []); + + /// Shader compiler error, if this returns [kFalse], to get details use + /// [getShaderInfoLog]. + Object? get compileStatus => + _kCompileStatus ??= js_util.getProperty(glContext, 'COMPILE_STATUS'); + + Object? get kArrayBuffer => + _kArrayBuffer ??= js_util.getProperty(glContext, 'ARRAY_BUFFER'); + + Object? get kElementArrayBuffer => + _kElementArrayBuffer ??= js_util.getProperty(glContext, + 'ELEMENT_ARRAY_BUFFER'); + + Object get kLinkStatus => + _kLinkStatus ??= js_util.getProperty(glContext, 'LINK_STATUS'); + + Object get kFloat => _kFloat ??= js_util.getProperty(glContext, 'FLOAT'); + + Object? get kRGBA => _kRGBA ??= js_util.getProperty(glContext, 'RGBA'); + + Object get kUnsignedByte => + _kUnsignedByte ??= js_util.getProperty(glContext, 'UNSIGNED_BYTE'); + + Object? get kUnsignedShort => + _kUnsignedShort ??= js_util.getProperty(glContext, 'UNSIGNED_SHORT'); + + Object? get kStaticDraw => + _kStaticDraw ??= js_util.getProperty(glContext, 'STATIC_DRAW'); + + Object get kTriangles => + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLES'); + + Object get kTriangleFan => + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_FAN'); + + Object get kTriangleStrip => + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_STRIP'); + + Object? get kColorBufferBit => + _kColorBufferBit ??= js_util.getProperty(glContext, 'COLOR_BUFFER_BIT'); + + Object? get kTexture2D => + _kTexture2D ??= js_util.getProperty(glContext, 'TEXTURE_2D'); + + int get kTexture0 => + _kTexture0 ??= js_util.getProperty(glContext, 'TEXTURE0'); + + Object? get kTextureWrapS => + _kTextureWrapS ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_S'); + + Object? get kTextureWrapT => + _kTextureWrapT ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_T'); + + Object? get kRepeat => + _kRepeat ??= js_util.getProperty(glContext, 'REPEAT'); + + Object? get kClampToEdge => + _kClampToEdge ??= js_util.getProperty(glContext, 'CLAMP_TO_EDGE'); + + Object? get kMirroredRepeat => + _kMirroredRepeat ??= js_util.getProperty(glContext, 'MIRRORED_REPEAT'); + + Object? get kLinear => + _kLinear ??= js_util.getProperty(glContext, 'LINEAR'); + + Object? get kTextureMinFilter => + _kTextureMinFilter ??= js_util.getProperty(glContext, + 'TEXTURE_MIN_FILTER'); + + /// Returns reference to uniform in program. + Object getUniformLocation(Object program, String uniformName) { + final Object? res = js_util + .callMethod(glContext, 'getUniformLocation', [program, uniformName]); + if (res == null) { + throw Exception('$uniformName not found'); + } else { + return res; + } + } + + /// Returns true if uniform exists. + bool containsUniform(Object program, String uniformName) { + final Object? res = js_util + .callMethod(glContext, 'getUniformLocation', [program, uniformName]); + return res != null; + } + + /// Returns reference to uniform in program. + Object getAttributeLocation(Object program, String attribName) { + final Object? res = js_util + .callMethod(glContext, 'getAttribLocation', [program, attribName]); + if (res == null) { + throw Exception('$attribName not found'); + } else { + return res; + } + } + + /// Sets float uniform value. + void setUniform1f(Object uniform, double value) { + js_util.callMethod(glContext, 'uniform1f', [uniform, value]); + } + + /// Sets vec2 uniform values. + void setUniform2f(Object uniform, double value1, double value2) { + js_util.callMethod(glContext, 'uniform2f', [uniform, value1, value2]); + } + + /// Sets vec4 uniform values. + void setUniform4f(Object uniform, double value1, double value2, double value3, + double value4) { + js_util.callMethod( + glContext, 'uniform4f', [uniform, value1, value2, value3, value4]); + } + + /// Sets mat4 uniform values. + void setUniformMatrix4fv(Object uniform, bool transpose, Float32List value) { + js_util.callMethod( + glContext, 'uniformMatrix4fv', [uniform, transpose, value]); + } + + /// Shader compile error log. + Object? getShaderInfoLog(Object glShader) { + return js_util.callMethod(glContext, 'getShaderInfoLog', [glShader]); + } + + /// Errors that occurred during failed linking or validation of program + /// objects. Typically called after [linkProgram]. + String? getProgramInfoLog(Object glProgram) { + return js_util.callMethod(glContext, 'getProgramInfoLog', [glProgram]); + } + + int? get drawingBufferWidth => + js_util.getProperty(glContext, 'drawingBufferWidth'); + int? get drawingBufferHeight => + js_util.getProperty(glContext, 'drawingBufferWidth'); + + /// Reads gl contents as image data. + /// + /// Warning: data is read bottom up (flipped). + html.ImageData readImageData() { + const int kBytesPerPixel = 4; + final int bufferWidth = _widthInPixels!; + final int bufferHeight = _heightInPixels!; + if (browserEngine == BrowserEngine.webkit || + browserEngine == BrowserEngine.firefox) { + final Uint8List pixels = + Uint8List(bufferWidth * bufferHeight * kBytesPerPixel); + js_util.callMethod(glContext, 'readPixels', + [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); + return html.ImageData( + Uint8ClampedList.fromList(pixels), bufferWidth, bufferHeight); + } else { + final Uint8ClampedList pixels = + Uint8ClampedList(bufferWidth * bufferHeight * kBytesPerPixel); + js_util.callMethod(glContext, 'readPixels', + [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); + return html.ImageData(pixels, bufferWidth, bufferHeight); + } + } + + /// Returns image data in a form that can be used to create Canvas + /// context patterns. + Object? readPatternData() { + // When using OffscreenCanvas and transferToImageBitmap is supported by + // browser create ImageBitmap otherwise use more expensive canvas + // allocation. + if (_canvas != null && + js_util.hasProperty(_canvas!, 'transferToImageBitmap')) { + // TODO(yjbanov): find out why we need to call getContext and ignore the return value. + js_util.callMethod(_canvas!, 'getContext', ['webgl2']); + final Object? imageBitmap = js_util.callMethod(_canvas!, 'transferToImageBitmap', + []); + return imageBitmap; + } else { + final html.CanvasElement canvas = html.CanvasElement(width: _widthInPixels, height: _heightInPixels); + final html.CanvasRenderingContext2D ctx = canvas.context2D; + drawImage(ctx, 0, 0); + return canvas; + } + } + + /// Returns image data in data url format. + String toImageUrl() { + final html.CanvasElement canvas = html.CanvasElement(width: _widthInPixels, height: _heightInPixels); + final html.CanvasRenderingContext2D ctx = canvas.context2D; + drawImage(ctx, 0, 0); + final String dataUrl = canvas.toDataUrl(); + canvas.width = 0; + canvas.height = 0; + return dataUrl; + } +} + +// ignore: avoid_classes_with_only_static_members +/// Creates gl context from cached OffscreenCanvas for webgl rendering to image. +class GlContextCache { + static int _maxPixelWidth = 0; + static int _maxPixelHeight = 0; + static GlContext? _cachedContext; + static OffScreenCanvas? _offScreenCanvas; + + static void dispose() { + _maxPixelWidth = 0; + _maxPixelHeight = 0; + _cachedContext = null; + _offScreenCanvas?.dispose(); + } + + static GlContext? createGlContext(int widthInPixels, int heightInPixels) { + if (widthInPixels > _maxPixelWidth || heightInPixels > _maxPixelHeight) { + _cachedContext?.dispose(); + _cachedContext = null; + _offScreenCanvas = null; + _maxPixelWidth = math.max(_maxPixelWidth, widthInPixels); + _maxPixelHeight = math.max(_maxPixelHeight, widthInPixels); + } + _offScreenCanvas ??= OffScreenCanvas(widthInPixels, heightInPixels); + _cachedContext ??= GlContext(_offScreenCanvas!); + _cachedContext!.setViewportSize(widthInPixels, heightInPixels); + return _cachedContext; + } +} + +void setupVertexTransforms( + GlContext gl, + GlProgram glProgram, + double offsetX, + double offsetY, + double widthInPixels, + double heightInPixels, + Matrix4 transform) { + final Object transformUniform = + gl.getUniformLocation(glProgram.program, 'u_ctransform'); + final Matrix4 transformAtOffset = transform.clone() + ..translate(-offsetX, -offsetY); + gl.setUniformMatrix4fv(transformUniform, false, transformAtOffset.storage); + + // Set uniform to scale 0..width/height pixels coordinates to -1..1 + // clipspace range and flip the Y axis. + final Object resolution = gl.getUniformLocation(glProgram.program, 'u_scale'); + gl.setUniform4f(resolution, 2.0 / widthInPixels.toDouble(), + -2.0 / heightInPixels.toDouble(), 1, 1); + final Object shift = gl.getUniformLocation(glProgram.program, 'u_shift'); + gl.setUniform4f(shift, -1, 1, 0, 0); +} + +void setupTextureTransform( + GlContext gl, GlProgram glProgram, double offsetx, double offsety, double sx, double sy) { + final Object scalar = gl.getUniformLocation(glProgram.program, 'u_textransform'); + gl.setUniform4f(scalar, sx, sy, offsetx, offsety); +} + +void bufferVertexData(GlContext gl, Float32List positions, + double devicePixelRatio) { + if (devicePixelRatio == 1.0) { + gl.bufferData(positions, gl.kStaticDraw); + } else { + final int length = positions.length; + final Float32List scaledList = Float32List(length); + for (int i = 0; i < length; i++) { + scaledList[i] = positions[i] * devicePixelRatio; + } + gl.bufferData(scaledList, gl.kStaticDraw); + } +} + +dynamic tileModeToGlWrapping(GlContext gl, ui.TileMode tileMode) { + switch (tileMode) { + case ui.TileMode.clamp: + return gl.kClampToEdge; + case ui.TileMode.decal: + return gl.kClampToEdge; + case ui.TileMode.mirror: + return gl.kMirroredRepeat; + case ui.TileMode.repeated: + return gl.kRepeat; + } +} + +/// Polyfill for html.OffscreenCanvas that is not supported on some browsers. +class OffScreenCanvas { + html.OffscreenCanvas? offScreenCanvas; + html.CanvasElement? canvasElement; + int width; + int height; + static bool? _supported; + + OffScreenCanvas(this.width, this.height) { + if (OffScreenCanvas.supported) { + offScreenCanvas = html.OffscreenCanvas(width, height); + } else { + canvasElement = html.CanvasElement( + width: width, + height: height, + ); + canvasElement!.className = 'gl-canvas'; + final double cssWidth = width / EnginePlatformDispatcher.browserDevicePixelRatio; + final double cssHeight = height / EnginePlatformDispatcher.browserDevicePixelRatio; + canvasElement!.style + ..position = 'absolute' + ..width = '${cssWidth}px' + ..height = '${cssHeight}px'; + } + } + + void dispose() { + offScreenCanvas = null; + canvasElement = null; + } + + /// Returns CanvasRenderContext2D or OffscreenCanvasRenderingContext2D to + /// paint into. + Object? getContext2d() { + return offScreenCanvas != null + ? offScreenCanvas!.getContext('2d') + : canvasElement!.getContext('2d'); + } + + /// Feature detection for transferToImageBitmap on OffscreenCanvas. + bool get transferToImageBitmapSupported => + js_util.hasProperty(offScreenCanvas!, 'transferToImageBitmap'); + + /// Creates an ImageBitmap object from the most recently rendered image + /// of the OffscreenCanvas. + /// + /// !Warning API still in experimental status, feature detect before using. + Object? transferToImageBitmap() { + return js_util.callMethod(offScreenCanvas!, 'transferToImageBitmap', + []); + } + + /// Draws canvas contents to a rendering context. + void transferImage(Object targetContext) { + // Actual size of canvas may be larger than viewport size. Use + // source/destination to draw part of the image data. + js_util.callMethod(targetContext, 'drawImage', + [offScreenCanvas ?? canvasElement!, 0, 0, width, height, + 0, 0, width, height]); + } + + /// Converts canvas contents to an image and returns as data URL. + Future toDataUrl() { + final Completer completer = Completer(); + if (offScreenCanvas != null) { + offScreenCanvas!.convertToBlob().then((html.Blob value) { + final html.FileReader fileReader = html.FileReader(); + fileReader.onLoad.listen((html.ProgressEvent event) { + completer.complete( + js_util.getProperty(js_util.getProperty(event, 'target'), 'result'), + ); + }); + fileReader.readAsDataUrl(value); + }); + return completer.future; + } else { + return Future.value(canvasElement!.toDataUrl()); + } + } + + /// Draws an image to canvas for both offscreen canvas canvas context2d. + void drawImage(Object image, int x, int y, int width, int height) { + js_util.callMethod( + getContext2d()!, 'drawImage', [image, x, y, width, height]); + } + + /// Feature detects OffscreenCanvas. + static bool get supported => _supported ??= + js_util.hasProperty(html.window, 'OffscreenCanvas'); +} diff --git a/lib/web_ui/lib/src/engine/text/font_collection.dart b/lib/web_ui/lib/src/engine/text/font_collection.dart index 39e52148f19e9..f8822e880cc87 100644 --- a/lib/web_ui/lib/src/engine/text/font_collection.dart +++ b/lib/web_ui/lib/src/engine/text/font_collection.dart @@ -5,11 +5,11 @@ import 'dart:async'; import 'dart:convert'; import 'dart:html' as html; -import 'dart:js_util' as js_util; import 'dart:typed_data'; import '../assets.dart'; import '../browser_detection.dart'; +import '../safe_browser_api.dart'; import '../util.dart'; import 'layout_service.dart'; @@ -185,18 +185,7 @@ class FontManager { try { final html.FontFace fontFace = html.FontFace(family, asset, descriptors); _fontLoadingFutures.add(fontFace.load().then((_) { - // We could do: - // ``` - // html.document.fonts!.add(fontFace); - // ``` - // But dart:html expects the return value to be non-null, and Firefox - // returns null. This causes the app to crash in Firefox with a null - // check exception. - // - // TODO(mdebbar): Revert this once the dart:html type is fixed. - // https://github.com/dart-lang/sdk/issues/45676 - // ignore: implicit_dynamic_function - js_util.callMethod(html.document.fonts!, 'add', [fontFace]); + html.document.fonts!.add(fontFace); }, onError: (dynamic e) { printWarning('Error while trying to load font family "$family":\n$e'); })); @@ -323,9 +312,3 @@ class _PolyfillFontManager extends FontManager { _fontLoadingFutures.add(completer.future); } } - -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'); diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index ba89068469add..5b9c0437ffb89 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -14,6 +14,7 @@ import '../browser_detection.dart'; import '../embedder.dart'; import '../host_node.dart'; import '../platform_dispatcher.dart'; +import '../safe_browser_api.dart'; import '../semantics.dart'; import '../services.dart'; import '../text/paragraph.dart'; @@ -1180,7 +1181,7 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy { // On iOS, blur is trigerred in the following cases: // // 1. The browser app is sent to the background (or the tab is changed). In - // this case, the window loses focus (see [flutterViewEmbedder.windowHasFocus]), + // this case, the window loses focus (see [windowHasFocus]), // so we close the input connection with the framework. // 2. The user taps on another focusable element. In this case, we refocus // the input field and wait for the framework to manage the focus change. @@ -1189,7 +1190,7 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy { // okay because the virtual keyboard will hide, and as soon as the user // taps the text field again, the virtual keyboard will come up. subscriptions.add(activeDomElement.onBlur.listen((_) { - if (flutterViewEmbedder.windowHasFocus) { + if (windowHasFocus) { activeDomElement.focus(); } else { owner.sendTextConnectionClosedToFrameworkIfAny(); @@ -1301,11 +1302,11 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy { subscriptions.add(html.document.onSelectionChange.listen(handleChange)); subscriptions.add(activeDomElement.onBlur.listen((_) { - if (flutterViewEmbedder.windowHasFocus) { + if (windowHasFocus) { // Chrome on Android will hide the onscreen keyboard when you tap outside // the text box. Instead, we want the framework to tell us to hide the // keyboard via `TextInput.clearClient` or `TextInput.hide`. Therefore - // refocus as long as [flutterViewEmbedder.windowHasFocus] is true. + // refocus as long as [windowHasFocus] is true. activeDomElement.focus(); } else { owner.sendTextConnectionClosedToFrameworkIfAny(); diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index eaba1dd2e0d41..5bc4d6787ed27 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -2,19 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@JS() library util; import 'dart:async'; import 'dart:html' as html; -import 'dart:js_util' as js_util; import 'dart:math' as math; import 'dart:typed_data'; -import 'package:js/js.dart'; import 'package:ui/ui.dart' as ui; import 'browser_detection.dart'; +import 'safe_browser_api.dart'; import 'vector_math.dart'; /// Generic callback signature, used by [_futurize]. @@ -395,8 +393,7 @@ String colorComponentsToCssString(int r, int g, int b, int a) { /// Firefox exception without interfering with others (potentially useful /// for the programmer). bool isNsErrorFailureException(Object e) { - // ignore: implicit_dynamic_function - return js_util.getProperty(e, 'name') == 'NS_ERROR_FAILURE'; + return getJsProperty(e, 'name') == 'NS_ERROR_FAILURE'; } /// From: https://developer.mozilla.org/en-US/docs/Web/CSS/font-family#Syntax @@ -648,28 +645,6 @@ extension JsonExtensions on Map { } } -typedef JsParseFloat = num? Function(String source); - -@JS('parseFloat') -external JsParseFloat get _jsParseFloat; - -/// Parses a string [source] into a double. -/// -/// Uses the JavaScript `parseFloat` function instead of Dart's [double.parse] -/// because the latter can't parse strings like "20px". -/// -/// Returns null if it fails to parse. -num? parseFloat(String source) { - // Using JavaScript's `parseFloat` here because it can parse values - // like "20px", while Dart's `double.tryParse` fails. - final num? result = _jsParseFloat(source); - - if (result == null || result.isNaN) { - return null; - } - return result; -} - /// Prints a list of bytes in hex format. /// /// Bytes are separated by one space and are padded on the left to always show @@ -736,8 +711,7 @@ void drawEllipse( double startAngle, double endAngle, bool antiClockwise) { - // ignore: implicit_dynamic_function - _ellipseFeatureDetected ??= js_util.getProperty(context, 'ellipse') != null; + _ellipseFeatureDetected ??= getJsProperty(context, 'ellipse') != null; if (_ellipseFeatureDetected!) { context.ellipse(centerX, centerY, radiusX, radiusY, rotation, startAngle, endAngle, antiClockwise); diff --git a/lib/web_ui/lib/src/engine/web_experiments.dart b/lib/web_ui/lib/src/engine/web_experiments.dart deleted file mode 100644 index 7dea7f8e98622..0000000000000 --- a/lib/web_ui/lib/src/engine/web_experiments.dart +++ /dev/null @@ -1,33 +0,0 @@ -// 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 'dart:js' as js; - -import '../engine.dart' show registerHotRestartListener; - -/// A bag of all experiment flags in the web engine. -/// -/// This class also handles platform messages that can be sent to enable/disable -/// certain experiments at runtime without the need to access engine internals. -class WebExperiments { - WebExperiments._() { - js.context['_flutter_internal_update_experiment'] = updateExperiment; - registerHotRestartListener(() { - js.context['_flutter_internal_update_experiment'] = null; - }); - } - - static WebExperiments ensureInitialized() { - return WebExperiments.instance ?? - (WebExperiments.instance = WebExperiments._()); - } - - static WebExperiments? instance; - - /// Reset all experimental flags to their default values. - void reset() {} - - /// Used to enable/disable experimental flags in the web engine. - void updateExperiment(String name, bool? enabled) {} -} diff --git a/lib/web_ui/test/canvas_test.dart b/lib/web_ui/test/canvas_test.dart index 76a61b57d929f..5335677bbf5b0 100644 --- a/lib/web_ui/test/canvas_test.dart +++ b/lib/web_ui/test/canvas_test.dart @@ -17,10 +17,6 @@ void main() { } void testMain() { - setUpAll(() { - WebExperiments.ensureInitialized(); - }); - group('EngineCanvas', () { late MockEngineCanvas mockCanvas; late ui.Paragraph paragraph; diff --git a/lib/web_ui/test/engine/platform_dispatcher_test.dart b/lib/web_ui/test/engine/platform_dispatcher_test.dart index 7941a37bd2182..0007cc67243ac 100644 --- a/lib/web_ui/test/engine/platform_dispatcher_test.dart +++ b/lib/web_ui/test/engine/platform_dispatcher_test.dart @@ -60,9 +60,8 @@ void testMain() { test('responds correctly to flutter/platform Clipboard.getData failure', () async { // Patch browser so that clipboard api is not available. - final dynamic originalClipboard = - // ignore: implicit_dynamic_function - js_util.getProperty(html.window.navigator, 'clipboard'); + final Object? originalClipboard = + js_util.getProperty(html.window.navigator, 'clipboard'); js_util.setProperty(html.window.navigator, 'clipboard', null); const MethodCodec codec = JSONMethodCodec(); final Completer completer = Completer(); diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index 1fd6533b63018..1d341e71106ca 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -2325,8 +2325,7 @@ mixin _ButtonedEventMixin on _BasicEventContext { required double? deltaX, required double? deltaY, }) { - // ignore: implicit_dynamic_function - final Function jsWheelEvent = js_util.getProperty(html.window, 'WheelEvent') as Function; + final Function jsWheelEvent = js_util.getProperty(html.window, 'WheelEvent'); final List eventArgs = [ 'wheel', { @@ -2337,11 +2336,10 @@ mixin _ButtonedEventMixin on _BasicEventContext { 'deltaY': deltaY, } ]; - // ignore: implicit_dynamic_function - return js_util.callConstructor( + return js_util.callConstructor( jsWheelEvent, js_util.jsify(eventArgs) as List, - ) as html.Event; + ); } } @@ -2555,8 +2553,7 @@ class _MouseEventContext extends _BasicEventContext double? clientY, }) { final Function jsMouseEvent = - // ignore: implicit_dynamic_function - js_util.getProperty(html.window, 'MouseEvent') as Function; + js_util.getProperty(html.window, 'MouseEvent'); final List eventArgs = [ type, { @@ -2566,11 +2563,10 @@ class _MouseEventContext extends _BasicEventContext 'clientY': clientY, } ]; - // ignore: implicit_dynamic_function - return js_util.callConstructor( + return js_util.callConstructor( jsMouseEvent, js_util.jsify(eventArgs) as List, - ) as html.MouseEvent; + ); } } diff --git a/lib/web_ui/test/engine/surface/scene_builder_test.dart b/lib/web_ui/test/engine/surface/scene_builder_test.dart index 94ab92e3c6787..b6a1d1569c264 100644 --- a/lib/web_ui/test/engine/surface/scene_builder_test.dart +++ b/lib/web_ui/test/engine/surface/scene_builder_test.dart @@ -921,7 +921,6 @@ HtmlImage createTestImage({int width = 100, int height = 50}) { ctx.fillRect(66, 0, 33, 50); ctx.fill(); final html.ImageElement imageElement = html.ImageElement(); - // ignore: implicit_dynamic_function - imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; + imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); return HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/engine/web_experiments_test.dart b/lib/web_ui/test/engine/web_experiments_test.dart deleted file mode 100644 index 0e0516ebd0880..0000000000000 --- a/lib/web_ui/test/engine/web_experiments_test.dart +++ /dev/null @@ -1,40 +0,0 @@ -// 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 'dart:html' as html; -import 'dart:js_util' as js_util; - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; -import 'package:ui/src/engine/web_experiments.dart'; - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -void testMain() { - setUp(() { - WebExperiments.ensureInitialized(); - }); - - tearDown(() { - WebExperiments.instance!.reset(); - }); - - test('js interop throws on wrong type', () { - expect(() => jsUpdateExperiment(123, true), throwsA(anything)); - expect(() => jsUpdateExperiment('foo', 123), throwsA(anything)); - expect(() => jsUpdateExperiment('foo', 'bar'), throwsA(anything)); - expect(() => jsUpdateExperiment(false, 'foo'), throwsA(anything)); - }); -} - -void jsUpdateExperiment(dynamic name, dynamic enabled) { - // ignore: implicit_dynamic_function - js_util.callMethod( - html.window, - '_flutter_internal_update_experiment', - [name, enabled], - ); -} diff --git a/lib/web_ui/test/html/canvas_clip_path_golden_test.dart b/lib/web_ui/test/html/canvas_clip_path_golden_test.dart index 0be4e70c1e559..a390aa3cb78f0 100644 --- a/lib/web_ui/test/html/canvas_clip_path_golden_test.dart +++ b/lib/web_ui/test/html/canvas_clip_path_golden_test.dart @@ -140,7 +140,6 @@ engine.HtmlImage createTestImage({int width = 200, int height = 150}) { ctx.fillRect(2 * width / 3, 0, width / 3, height); ctx.fill(); final html.ImageElement imageElement = html.ImageElement(); - // ignore: implicit_dynamic_function - imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; + imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); return engine.HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/html/compositing/canvas_blend_golden_test.dart b/lib/web_ui/test/html/compositing/canvas_blend_golden_test.dart index e143b8f0949cb..4599f9f13dc02 100644 --- a/lib/web_ui/test/html/compositing/canvas_blend_golden_test.dart +++ b/lib/web_ui/test/html/compositing/canvas_blend_golden_test.dart @@ -120,7 +120,6 @@ HtmlImage createTestImage() { ctx.fillRect(66, 0, 33, 50); ctx.fill(); final html.ImageElement imageElement = html.ImageElement(); - // ignore: implicit_dynamic_function - imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; + imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); return HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/html/compositing/color_filter_golden_test.dart b/lib/web_ui/test/html/compositing/color_filter_golden_test.dart index f07e0ab22724b..6365c9af801c7 100644 --- a/lib/web_ui/test/html/compositing/color_filter_golden_test.dart +++ b/lib/web_ui/test/html/compositing/color_filter_golden_test.dart @@ -189,7 +189,6 @@ HtmlImage createTestImage({int width = 200, int height = 150}) { ctx.fillRect(2 * width / 3, 0, width / 3, height); ctx.fill(); final html.ImageElement imageElement = html.ImageElement(); - // ignore: implicit_dynamic_function - imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; + imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); return HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/html/drawing/canvas_draw_image_golden_test.dart b/lib/web_ui/test/html/drawing/canvas_draw_image_golden_test.dart index a4c9461d52763..0b1a6523cf664 100644 --- a/lib/web_ui/test/html/drawing/canvas_draw_image_golden_test.dart +++ b/lib/web_ui/test/html/drawing/canvas_draw_image_golden_test.dart @@ -734,8 +734,7 @@ HtmlImage createTestImage({int width = 100, int height = 50}) { ctx.fillRect(66, 0, 33, 50); ctx.fill(); final html.ImageElement imageElement = html.ImageElement(); - // ignore: implicit_dynamic_function - imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; + imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); return HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/html/drawing/draw_vertices_golden_test.dart b/lib/web_ui/test/html/drawing/draw_vertices_golden_test.dart index 0d3c64fd99631..e570872c96f0d 100644 --- a/lib/web_ui/test/html/drawing/draw_vertices_golden_test.dart +++ b/lib/web_ui/test/html/drawing/draw_vertices_golden_test.dart @@ -424,7 +424,6 @@ Future createTestImage({int width = 50, int height = 40}) { imageElement.onLoad.listen((html.Event event) { completer.complete(HtmlImage(imageElement, width, height)); }); - // ignore: implicit_dynamic_function - imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; + imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); return completer.future; } diff --git a/lib/web_ui/test/html/shaders/image_shader_golden_test.dart b/lib/web_ui/test/html/shaders/image_shader_golden_test.dart index 70156d3e28270..f47fe6dc17626 100644 --- a/lib/web_ui/test/html/shaders/image_shader_golden_test.dart +++ b/lib/web_ui/test/html/shaders/image_shader_golden_test.dart @@ -145,7 +145,6 @@ HtmlImage createTestImage() { ctx.fillRect(width2, width2, width2, width2); ctx.fill(); final html.ImageElement imageElement = html.ImageElement(); - // ignore: implicit_dynamic_function - imageElement.src = js_util.callMethod(canvas, 'toDataURL', []) as String; + imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); return HtmlImage(imageElement, width, height); } diff --git a/lib/web_ui/test/keyboard_test.dart b/lib/web_ui/test/keyboard_test.dart index cf3004a78c1d8..3da3fb8d8685c 100644 --- a/lib/web_ui/test/keyboard_test.dart +++ b/lib/web_ui/test/keyboard_test.dart @@ -658,8 +658,7 @@ html.KeyboardEvent dispatchKeyboardEvent( target ??= html.window; final Function jsKeyboardEvent = - // ignore: implicit_dynamic_function - js_util.getProperty(html.window, 'KeyboardEvent') as Function; + js_util.getProperty(html.window, 'KeyboardEvent'); final List eventArgs = [ type, { @@ -675,11 +674,10 @@ html.KeyboardEvent dispatchKeyboardEvent( 'cancelable': true, } ]; - // ignore: implicit_dynamic_function - final html.KeyboardEvent event = js_util.callConstructor( + final html.KeyboardEvent event = js_util.callConstructor( jsKeyboardEvent, js_util.jsify(eventArgs) as List, - ) as html.KeyboardEvent; + ); target.dispatchEvent(event); return event; diff --git a/lib/web_ui/test/paragraph_builder_test.dart b/lib/web_ui/test/paragraph_builder_test.dart index 9671a9d70648e..08613a9a79028 100644 --- a/lib/web_ui/test/paragraph_builder_test.dart +++ b/lib/web_ui/test/paragraph_builder_test.dart @@ -4,7 +4,6 @@ import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; -import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart'; void main() { @@ -12,10 +11,6 @@ void main() { } void testMain() { - setUpAll(() { - WebExperiments.ensureInitialized(); - }); - test('Should be able to build and layout a paragraph', () { final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle()); builder.addText('Hello'); diff --git a/lib/web_ui/test/text/canvas_paragraph_builder_test.dart b/lib/web_ui/test/text/canvas_paragraph_builder_test.dart index 64693293032cb..82283e9b9ada4 100644 --- a/lib/web_ui/test/text/canvas_paragraph_builder_test.dart +++ b/lib/web_ui/test/text/canvas_paragraph_builder_test.dart @@ -35,10 +35,6 @@ void main() { Future testMain() async { await initializeTestFlutterViewEmbedder(); - setUpAll(() { - WebExperiments.ensureInitialized(); - }); - test('Builds a text-only canvas paragraph', () { final EngineParagraphStyle style = EngineParagraphStyle(fontSize: 13.0); final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style); diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index ee935a3d8d1fc..3a37ab76743b4 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -2147,8 +2147,7 @@ KeyboardEvent dispatchKeyboardEvent( String type, { required int keyCode, }) { - // ignore: implicit_dynamic_function - final Function jsKeyboardEvent = js_util.getProperty(window, 'KeyboardEvent') as Function; + final Function jsKeyboardEvent = js_util.getProperty(window, 'KeyboardEvent'); final List eventArgs = [ type, { @@ -2156,9 +2155,10 @@ KeyboardEvent dispatchKeyboardEvent( 'cancelable': true, } ]; - final KeyboardEvent event = - // ignore: implicit_dynamic_function - js_util.callConstructor(jsKeyboardEvent, js_util.jsify(eventArgs) as List?) as KeyboardEvent; + final KeyboardEvent event = js_util.callConstructor( + jsKeyboardEvent, + js_util.jsify(eventArgs) as List?, + ); target.dispatchEvent(event); return event; diff --git a/lib/web_ui/test/window_test.dart b/lib/web_ui/test/window_test.dart index 2aeeea42f5e9b..49cbd1dd274cb 100644 --- a/lib/web_ui/test/window_test.dart +++ b/lib/web_ui/test/window_test.dart @@ -445,8 +445,7 @@ void testMain() { } void jsSetUrlStrategy(dynamic strategy) { - // ignore: implicit_dynamic_function - js_util.callMethod( + js_util.callMethod( html.window, '_flutter_web_set_location_strategy', [strategy], diff --git a/web_sdk/sdk_rewriter.dart b/web_sdk/sdk_rewriter.dart index eef534363113b..f7dfbf8d5e923 100644 --- a/web_sdk/sdk_rewriter.dart +++ b/web_sdk/sdk_rewriter.dart @@ -34,29 +34,29 @@ export 'dart:_engine' ]; final List engineLibraryPatterns = [ - AllReplacer(RegExp(r'library\s+engine;'), 'library dart._engine;'), - AllReplacer(RegExp(r''' -import\s*'../ui.dart' as ui; -'''), r''' -import 'dart:ui' as ui; -'''), - AllReplacer(RegExp(r''' -import\s*'package:ui/ui.dart' as ui; -'''), r''' + AllReplacer(RegExp(r'library\s+engine;'), ''' +@JS() +library dart._engine; + +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert' hide Codec; +import 'dart:developer' as developer; +import 'dart:html' as html; +import 'dart:js' as js; +import 'dart:js_util' as js_util; +import 'dart:_js_annotations'; +import 'dart:math' as math; +import 'dart:svg' as svg; +import 'dart:typed_data'; import 'dart:ui' as ui; '''), - // Remove imports of engine part files. - AllReplacer(RegExp(r"import\s*'engine/.*';"), ''), // Replace exports of engine files with "part" directives. MappedReplacer(RegExp(r''' export\s*'engine/(.*)'; '''), (Match match) => ''' part 'engine/${match.group(1)}'; '''), - AllReplacer( - RegExp(r"import\s*'package:js/js.dart'"), - r"import 'dart:_js_annotations'", - ), ]; final List enginePartsPatterns = [ @@ -111,6 +111,7 @@ String rewriteFile(String source, {required String filePath, required bool isUi, replacementPatterns.addAll(uiPatterns); } else if (isEngine) { if (filePath.endsWith('lib/src/engine.dart')) { + _validateEngineSource(filePath, source); replacementPatterns.addAll(engineLibraryPatterns); } else { source = _preprocessEnginePartFile(source); @@ -123,6 +124,49 @@ String rewriteFile(String source, {required String filePath, required bool isUi, return source; } +// Enforces a particular structure in engine.dart source code. +// +// Code in `engine.dart` must only be made of the library directive, exports, +// and code comments. Imports are disallowed. Instead, the required imports are +// added by this script during the rewrite. +void _validateEngineSource(String engineDartPath, String engineDartCode) { + const List expectedLines = [ + 'library engine;', + ]; + + final List lines = engineDartCode.split('\n'); + for (int i = 0; i < lines.length; i += 1) { + final int lineNumber = i + 1; + final String line = lines[i].trim(); + + if (line.isEmpty) { + // Emply lines are OK + continue; + } + + if (expectedLines.contains(line)) { + // Expected; let it pass. + continue; + } + + if (line.startsWith('//')) { + // Comments are OK + continue; + } + + if (line.startsWith('export')) { + // Exports are OK + continue; + } + + throw Exception( + 'on line $lineNumber: unexpected code in $engineDartPath. This file ' + 'may only contain comments and exports. Found:\n' + '$line' + ); + } +} + String _preprocessEnginePartFile(String source) { if (source.startsWith('part of engine;') || source.contains('\npart of engine;')) { // The file hasn't been migrated yet. diff --git a/web_sdk/test/api_conform_test.dart b/web_sdk/test/api_conform_test.dart index 929e0aa7902e8..fbe42aac06a83 100644 --- a/web_sdk/test/api_conform_test.dart +++ b/web_sdk/test/api_conform_test.dart @@ -35,16 +35,17 @@ CompilationUnit _parseAndCheckDart(String path) { } void main() { + final String flutterDir = Platform.environment['FLUTTER_DIR']!; // These files just contain imports to the part files; - final CompilationUnit uiUnit = _parseAndCheckDart('lib/ui/ui.dart'); - final CompilationUnit webUnit = _parseAndCheckDart('lib/web_ui/lib/ui.dart'); + final CompilationUnit uiUnit = _parseAndCheckDart('$flutterDir/lib/ui/ui.dart'); + final CompilationUnit webUnit = _parseAndCheckDart('$flutterDir/lib/web_ui/lib/ui.dart'); final Map uiClasses = {}; final Map webClasses = {}; // Gather all public classes from each library. For now we are skipping // other top level members. - _collectPublicClasses(uiUnit, uiClasses, 'lib/ui/'); - _collectPublicClasses(webUnit, webClasses, 'lib/web_ui/lib/'); + _collectPublicClasses(uiUnit, uiClasses, '$flutterDir/lib/ui/'); + _collectPublicClasses(webUnit, webClasses, '$flutterDir/lib/web_ui/lib/'); if (uiClasses.isEmpty || webClasses.isEmpty) { print('Warning: did not resolve any classes.'); diff --git a/web_sdk/test/js_access_test.dart b/web_sdk/test/js_access_test.dart new file mode 100644 index 0000000000000..81ab8c567f95f --- /dev/null +++ b/web_sdk/test/js_access_test.dart @@ -0,0 +1,236 @@ +// 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. + +/// Checks that JavaScript API is accessed properly. +/// +/// JavaScript access needs to be audited to make sure it follows security best +/// practices. To do that, all JavaScript access is consolidated into a small +/// number of libraries that change infrequently. These libraries are manually +/// audited on every change. All other code accesses JavaScript through these +/// libraries and does not require audit. + +import 'dart:io'; + +import 'package:test/test.dart'; + +// Libraries that allow making arbitrary calls to JavaScript. +const List _jsAccessLibraries = [ + 'dart:js', + 'dart:js_util', + 'package:js', +]; + +// Libraries that are allowed to make direct calls to JavaScript. These +// libraries must be reviewed carefully to make sure JavaScript APIs are used +// safely. +const List _auditedLibraries = [ + 'lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart', + 'lib/web_ui/lib/src/engine/safe_browser_api.dart', +]; + +Future main(List args) async { + bool areAssertionsEnabled = false; + assert(() { + areAssertionsEnabled = true; + return true; + }()); + + if (!areAssertionsEnabled) { + throw ArgumentError( + 'This test must run with --enable-asserts', + ); + } + + test('Self-test', () { + // A library that doesn't directly access JavaScript API should pass. + { + final _CheckResult result = _checkFile( + File('lib/web_ui/lib/src/engine/alarm_clock.dart'), +''' +// A comment +import 'dart:async'; +import 'package:ui/ui.dart' as ui; +export 'foo.dart'; +''', + ); + expect(result.passed, isTrue); + expect(result.failed, isFalse); + expect(result.violations, isEmpty); + } + + // Multi-line imports should fail. + { + final _CheckResult result = _checkFile( + File('lib/web_ui/lib/src/engine/alarm_clock.dart'), +''' +import 'dart:html' + show HtmlElement; +import 'dart:async'; +import 'package:ui/ui.dart' + as ui; +''', + ); + expect(result.failed, isTrue); + expect(result.violations, [ + "on line 1: import is broken up into multiple lines: import 'dart:html'", + "on line 4: import is broken up into multiple lines: import 'package:ui/ui.dart'", + ]); + } + + // A library that doesn't directly access JavaScript API should pass. + expect( + _checkFile( + File('lib/web_ui/lib/src/engine/alarm_clock.dart'), +''' +import 'dart:async'; +import 'package:ui/ui.dart' as ui; +''', + ).passed, + isTrue, + ); + + // A non-audited library that directly accesses JavaScript API should fail. + for (final String jsAccessLibrary in _jsAccessLibraries) { + final _CheckResult result = _checkFile( + File('lib/web_ui/lib/src/engine/alarm_clock.dart'), + ''' + import 'dart:async'; + import 'package:ui/ui.dart' as ui; + import '$jsAccessLibrary'; + ''', + ); + expect(result.passed, isFalse); + expect(result.failed, isTrue); + expect(result.violations, [ + 'on line 3: library accesses $jsAccessLibrary directly', + ]); + } + + // Audited libraries that directly accesses JavaScript API should pass. + for (final String auditedLibrary in _auditedLibraries) { + for (final String jsAccessLibrary in _jsAccessLibraries) { + expect( + _checkFile( + File(auditedLibrary), + ''' + import 'dart:async'; + import 'package:ui/ui.dart' as ui; + import '$jsAccessLibrary'; + ''', + ).passed, + isTrue, + ); + } + } + }); + + test('Check JavaScript access', () async { + final Directory webUiLibDir = Directory('lib/web_ui/lib'); + final List dartFiles = webUiLibDir + .listSync(recursive: true) + .whereType() + .where((File file) => file.path.endsWith('.dart')) + .toList(); + + expect(dartFiles, isNotEmpty); + + final List<_CheckResult> results = <_CheckResult>[]; + for (final File dartFile in dartFiles) { + results.add(_checkFile( + dartFile, + await dartFile.readAsString(), + )); + } + + if (results.any((_CheckResult result) => result.failed)) { + // Sort to show failures last. + results.sort((_CheckResult a, _CheckResult b) { + final int aSortKey = a.passed ? 1 : 0; + final int bSortKey = b.passed ? 1 : 0; + return bSortKey - aSortKey; + }); + int passedCount = 0; + int failedCount = 0; + for (final _CheckResult result in results) { + if (result.passed) { + passedCount += 1; + print('PASSED: ${result.file.path}'); + } else { + failedCount += 1; + print('FAILED: ${result.file.path}'); + for (final String violation in result.violations) { + print(' $violation'); + } + } + } + expect(passedCount + failedCount, dartFiles.length); + print('$passedCount files passed. $failedCount files contain violations.'); + fail('Some file contain violations. See log messages above for details.'); + } + }); +} + +_CheckResult _checkFile(File dartFile, String code) { + final List violations = []; + final List lines = code.split('\n'); + for (int i = 0; i < lines.length; i += 1) { + final int lineNumber = i + 1; + final String line = lines[i].trim(); + final bool isImport = line.startsWith('import'); + if (!isImport) { + continue; + } + + final bool isProperlyFormattedImport = line.endsWith(';'); + if (!isProperlyFormattedImport) { + violations.add('on line $lineNumber: import is broken up into multiple lines: $line'); + continue; + } + + if (line.contains('"')) { + violations.add('on line $lineNumber: import is using double quotes instead of single quotes: $line'); + continue; + } + + final bool isAuditedLibrary = _auditedLibraries.contains(dartFile.path); + + if (isAuditedLibrary) { + // This library is allowed to access JavaScript API directly. + continue; + } + + for (final String jsAccessLibrary in _jsAccessLibraries) { + if (line.contains("'$jsAccessLibrary'")) { + violations.add('on line $lineNumber: library accesses $jsAccessLibrary directly'); + continue; + } + } + } + + if (violations.isEmpty) { + return _CheckResult.passed(dartFile); + } else { + return _CheckResult.failed(dartFile, violations); + } +} + +class _CheckResult { + _CheckResult.passed(this.file) : violations = const []; + + _CheckResult.failed(this.file, this.violations) : assert(violations.isNotEmpty); + + /// The Dart file that was checked. + final File file; + + /// If the check failed, contains the descriptions of violations. + /// + /// If the check passed, this is empty. + final List violations; + + /// Whether the file passed the check. + bool get passed => violations.isEmpty; + + /// Whether the file failed the check. + bool get failed => !passed; +} diff --git a/web_sdk/test/sdk_rewriter_test.dart b/web_sdk/test/sdk_rewriter_test.dart index 559e530c99bfd..69c707f99cb86 100644 --- a/web_sdk/test/sdk_rewriter_test.dart +++ b/web_sdk/test/sdk_rewriter_test.dart @@ -7,40 +7,43 @@ import 'package:test/test.dart'; import '../sdk_rewriter.dart'; void main() { - test('handles imports/exports correctly in the engine library file', () { + test('handles exports correctly in the engine library file', () { const String source = ''' -library engine; +// Comment 1 -import '../ui.dart' as ui; -import 'package:ui/ui.dart' as ui; +library engine; -import 'package:some_package/some_package.dart'; +// Comment 2 -import 'engine/file1.dart'; export 'engine/file1.dart'; - -import'engine/file2.dart'; export'engine/file2.dart'; - -import 'engine/file3.dart'; export 'engine/file3.dart'; '''; const String expected = ''' +// Comment 1 + +@JS() library dart._engine; -import 'dart:ui' as ui; +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert' hide Codec; +import 'dart:developer' as developer; +import 'dart:html' as html; +import 'dart:js' as js; +import 'dart:js_util' as js_util; +import 'dart:_js_annotations'; +import 'dart:math' as math; +import 'dart:svg' as svg; +import 'dart:typed_data'; import 'dart:ui' as ui; -import 'package:some_package/some_package.dart'; +// Comment 2 part 'engine/file1.dart'; - - part 'engine/file2.dart'; - - part 'engine/file3.dart'; '''; @@ -53,6 +56,36 @@ part 'engine/file3.dart'; expect(result, expected); }); + test('complains about non-compliant engine.dart file', () { + const String source = ''' +library engine; + +import 'dart:something'; +export 'engine/file1.dart'; +export 'engine/file3.dart'; +'''; + + Object? caught; + try { + rewriteFile( + source, + filePath: '/path/to/lib/web_ui/lib/src/engine.dart', + isUi: false, + isEngine: true, + ); + } catch(error) { + caught = error; + } + expect(caught, isA()); + expect( + '$caught', + 'Exception: on line 3: unexpected code in /path/to/lib/web_ui/lib/src/engine.dart. ' + 'This file may only contain comments and exports. Found:\n' + 'import \'dart:something\';', + ); + }); + + test('removes imports/exports from engine files', () { const String source = ''' import 'package:some_package/some_package.dart';