From 80e12d40ded8a7a4930d12cbe16c1d51da5c9e9e Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 30 Nov 2021 12:47:35 -0800 Subject: [PATCH 1/6] [web] consolidate and check JavaScript access --- ci/analyze.sh | 1 + lib/web_ui/lib/src/engine.dart | 352 +---------- lib/web_ui/lib/src/engine/canvas_pool.dart | 27 +- .../src/engine/canvaskit/canvaskit_api.dart | 61 ++ .../lib/src/engine/canvaskit/image.dart | 1 + .../engine/canvaskit/image_web_codecs.dart | 144 +---- .../src/engine/canvaskit/initialization.dart | 75 +-- lib/web_ui/lib/src/engine/configuration.dart | 5 + lib/web_ui/lib/src/engine/embedder.dart | 20 +- .../lib/src/engine/html/offscreen_canvas.dart | 103 ---- .../lib/src/engine/html/path/path_ref.dart | 16 +- .../lib/src/engine/html/render_vertices.dart | 45 +- .../src/engine/html/shaders/image_shader.dart | 26 +- .../html/shaders/normalized_gradient.dart | 2 +- .../lib/src/engine/html/shaders/shader.dart | 3 +- .../lib/src/engine/html_image_codec.dart | 14 +- lib/web_ui/lib/src/engine/initialization.dart | 196 +++++++ .../lib/src/engine/platform_dispatcher.dart | 44 +- .../lib/src/engine/pointer_binding.dart | 38 +- lib/web_ui/lib/src/engine/profiler.dart | 9 +- ...bgl_context.dart => safe_browser_api.dart} | 550 ++++++++++++++++-- .../lib/src/engine/text/font_collection.dart | 21 +- .../src/engine/text_editing/text_editing.dart | 9 +- lib/web_ui/lib/src/engine/util.dart | 31 +- .../lib/src/engine/web_experiments.dart | 33 -- lib/web_ui/test/canvas_test.dart | 4 - .../test/engine/web_experiments_test.dart | 9 - lib/web_ui/test/paragraph_builder_test.dart | 5 - .../text/canvas_paragraph_builder_test.dart | 4 - web_sdk/test/js_access_test.dart | 280 +++++++++ 30 files changed, 1171 insertions(+), 957 deletions(-) delete mode 100644 lib/web_ui/lib/src/engine/html/offscreen_canvas.dart create mode 100644 lib/web_ui/lib/src/engine/initialization.dart rename lib/web_ui/lib/src/engine/{html/shaders/webgl_context.dart => safe_browser_api.dart} (56%) delete mode 100644 lib/web_ui/lib/src/engine/web_experiments.dart create mode 100644 web_sdk/test/js_access_test.dart diff --git a/ci/analyze.sh b/ci/analyze.sh index 1d5aabb5d09db..5212f16cf8ac4 100755 --- a/ci/analyze.sh +++ b/ci/analyze.sh @@ -80,3 +80,4 @@ echo "" echo "Checking web_ui api conformance..." (cd "$FLUTTER_DIR/web_sdk"; "$DART" pub get) (cd "$FLUTTER_DIR"; "$DART" "web_sdk/test/api_conform_test.dart") +(cd "$FLUTTER_DIR"; "$DART" "web_sdk/test/js_access_test.dart") diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 5148cb4b0c66c..7d6312a938cdd 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -14,14 +14,16 @@ library engine; // The code that performs the transformations lives in: // - https://github.com/flutter/engine/blob/main/web_sdk/sdk_rewriter.dart +// ignore: unused_import 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; +import 'dart:collection' show ListBase, IterableBase, DoubleLinkedQueue, DoubleLinkedQueueEntry; // ignore: unused_import import 'dart:convert' hide Codec; +// ignore: unused_import import 'dart:developer' as developer; +// ignore: unused_import import 'dart:html' as html; // ignore: unused_import import 'dart:js' as js; @@ -31,487 +33,177 @@ import 'dart:js_util' as js_util; import 'dart:math' as math; // ignore: unused_import import 'dart:svg' as svg; +// ignore: unused_import import 'dart:typed_data'; - +// ignore: unused_import import 'package:js/js.dart'; // ignore: unused_import import 'package:meta/meta.dart'; - +// ignore: unused_import import '../ui.dart' as ui; - // ignore: unused_import import 'engine/configuration.dart'; +// ignore: unused_import import 'engine/embedder.dart'; +// ignore: unused_import +import 'engine/initialization.dart'; +// ignore: unused_import import 'engine/keyboard.dart'; +// ignore: unused_import import 'engine/mouse_cursor.dart'; +// ignore: unused_import import 'engine/navigation/js_url_strategy.dart'; +// ignore: unused_import import 'engine/navigation/url_strategy.dart'; +// ignore: unused_import import 'engine/platform_dispatcher.dart'; +// ignore: unused_import import 'engine/platform_views/content_manager.dart'; +// ignore: unused_import import 'engine/profiler.dart'; -import 'engine/web_experiments.dart'; +// ignore: unused_import +import 'engine/safe_browser_api.dart'; +// ignore: unused_import import 'engine/window.dart'; 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..3a4e1136ebdfa 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,20 @@ /// 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 +48,7 @@ class CkBrowserImageDecoder implements ui.Codec { } try { - final _ImageDecoder webDecoder = _ImageDecoder(_ImageDecoderOptions( + final ImageDecoder webDecoder = ImageDecoder(ImageDecoderOptions( type: contentType, data: data, @@ -82,13 +66,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 +93,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 +129,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 +168,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 +303,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..108527590ce57 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,14 @@ 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?; + getJsProperty(html.window, _staleHotRestartStore) as List?; if (_staleHotRestartState == null) { _staleHotRestartState = []; - js_util.setProperty( + setJsProperty( html.window, _staleHotRestartStore, _staleHotRestartState); } @@ -367,7 +355,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_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..3d24df24d9eea 100644 --- a/lib/web_ui/lib/src/engine/profiler.dart +++ b/lib/web_ui/lib/src/engine/profiler.dart @@ -4,11 +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'; +import 'safe_browser_api.dart'; /// A function that receives a benchmark [value] labeleb by [name]. typedef OnBenchmark = void Function(String name, double value); @@ -106,11 +106,8 @@ class Profiler { _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); - } + getJsProperty(html.window, '_flutter_internal_on_benchmark'); + onBenchmark?.call(name, value); } } diff --git a/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart b/lib/web_ui/lib/src/engine/safe_browser_api.dart similarity index 56% rename from lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart rename to lib/web_ui/lib/src/engine/safe_browser_api.dart index 7b346a045b903..ea589c2a3a1f9 100644 --- a/lib/web_ui/lib/src/engine/html/shaders/webgl_context.dart +++ b/lib/web_ui/lib/src/engine/safe_browser_api.dart @@ -2,16 +2,375 @@ // 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 '../../vector_math.dart'; -import '../offscreen_canvas.dart'; +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); +} + +/// 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 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 (hasJsProperty(navigator, 'vibrate')) { + // ignore: implicit_dynamic_function + 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 { @@ -23,25 +382,25 @@ class GlProgram { 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; + 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; @@ -305,62 +664,59 @@ class GlContext { /// Error state of gl context. // ignore: implicit_dynamic_function - dynamic get error => js_util.callMethod(glContext, 'getError', const []); + Object? get error => js_util.callMethod(glContext, 'getError', const []); /// Shader compiler error, if this returns [kFalse], to get details use /// [getShaderInfoLog]. - dynamic get compileStatus => + Object? get compileStatus => // ignore: implicit_dynamic_function _kCompileStatus ??= js_util.getProperty(glContext, 'COMPILE_STATUS'); - dynamic get kArrayBuffer => + Object? get kArrayBuffer => // ignore: implicit_dynamic_function _kArrayBuffer ??= js_util.getProperty(glContext, 'ARRAY_BUFFER'); - dynamic get kElementArrayBuffer => + Object? 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'); + Object get kLinkStatus => + _kLinkStatus ??= js_util.getProperty(glContext, 'LINK_STATUS'); - // ignore: implicit_dynamic_function - dynamic get kFloat => _kFloat ??= js_util.getProperty(glContext, 'FLOAT'); + Object get kFloat => _kFloat ??= js_util.getProperty(glContext, 'FLOAT'); // ignore: implicit_dynamic_function - dynamic get kRGBA => _kRGBA ??= js_util.getProperty(glContext, 'RGBA'); + Object? get kRGBA => _kRGBA ??= js_util.getProperty(glContext, 'RGBA'); - dynamic get kUnsignedByte => - // ignore: implicit_dynamic_function - _kUnsignedByte ??= js_util.getProperty(glContext, 'UNSIGNED_BYTE'); + Object get kUnsignedByte => + _kUnsignedByte ??= js_util.getProperty(glContext, 'UNSIGNED_BYTE'); - dynamic get kUnsignedShort => + Object? get kUnsignedShort => // ignore: implicit_dynamic_function _kUnsignedShort ??= js_util.getProperty(glContext, 'UNSIGNED_SHORT'); - dynamic get kStaticDraw => + Object? get kStaticDraw => // ignore: implicit_dynamic_function _kStaticDraw ??= js_util.getProperty(glContext, 'STATIC_DRAW'); - dynamic get kTriangles => + Object? get kTriangles => // ignore: implicit_dynamic_function _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLES'); - dynamic get kTriangleFan => + Object? get kTriangleFan => // ignore: implicit_dynamic_function _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_FAN'); - dynamic get kTriangleStrip => + Object? get kTriangleStrip => // ignore: implicit_dynamic_function _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_STRIP'); - dynamic get kColorBufferBit => + Object? get kColorBufferBit => // ignore: implicit_dynamic_function _kColorBufferBit ??= js_util.getProperty(glContext, 'COLOR_BUFFER_BIT'); - dynamic get kTexture2D => + Object? get kTexture2D => // ignore: implicit_dynamic_function _kTexture2D ??= js_util.getProperty(glContext, 'TEXTURE_2D'); @@ -368,31 +724,31 @@ class GlContext { // ignore: implicit_dynamic_function _kTexture0 ??= js_util.getProperty(glContext, 'TEXTURE0') as int; - dynamic get kTextureWrapS => + Object? get kTextureWrapS => // ignore: implicit_dynamic_function _kTextureWrapS ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_S'); - dynamic get kTextureWrapT => + Object? get kTextureWrapT => // ignore: implicit_dynamic_function _kTextureWrapT ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_T'); - dynamic get kRepeat => + Object? get kRepeat => // ignore: implicit_dynamic_function _kRepeat ??= js_util.getProperty(glContext, 'REPEAT'); - dynamic get kClampToEdge => + Object? get kClampToEdge => // ignore: implicit_dynamic_function _kClampToEdge ??= js_util.getProperty(glContext, 'CLAMP_TO_EDGE'); - dynamic get kMirroredRepeat => + Object? get kMirroredRepeat => // ignore: implicit_dynamic_function _kMirroredRepeat ??= js_util.getProperty(glContext, 'MIRRORED_REPEAT'); - dynamic get kLinear => + Object? get kLinear => // ignore: implicit_dynamic_function _kLinear ??= js_util.getProperty(glContext, 'LINEAR'); - dynamic get kTextureMinFilter => + Object? get kTextureMinFilter => // ignore: implicit_dynamic_function _kTextureMinFilter ??= js_util.getProperty(glContext, 'TEXTURE_MIN_FILTER'); @@ -454,7 +810,7 @@ class GlContext { } /// Shader compile error log. - dynamic getShaderInfoLog(Object glShader) { + Object? getShaderInfoLog(Object glShader) { // ignore: implicit_dynamic_function return js_util.callMethod(glContext, 'getShaderInfoLog', [glShader]); } @@ -617,3 +973,97 @@ dynamic tileModeToGlWrapping(GlContext gl, ui.TileMode tileMode) { 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. + // 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/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..fe19c463a4e92 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 @@ -737,7 +712,7 @@ void drawEllipse( 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/web_experiments_test.dart b/lib/web_ui/test/engine/web_experiments_test.dart index 0e0516ebd0880..d482fb04d9897 100644 --- a/lib/web_ui/test/engine/web_experiments_test.dart +++ b/lib/web_ui/test/engine/web_experiments_test.dart @@ -7,21 +7,12 @@ 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)); 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/web_sdk/test/js_access_test.dart b/web_sdk/test/js_access_test.dart new file mode 100644 index 0000000000000..736521eda6484 --- /dev/null +++ b/web_sdk/test/js_access_test.dart @@ -0,0 +1,280 @@ +// 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', +]; + +const List _auditedLibraries = [ + 'lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart', + 'lib/web_ui/lib/src/engine/safe_browser_api.dart', + 'lib/web_ui/lib/src/engine.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 engine.dart', () async { + const List expectedLines = [ + '@JS()', + 'library engine;', + ]; + + final File engineFile = File('lib/web_ui/lib/src/engine.dart'); + final String engineDartCode = await engineFile.readAsString(); + 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('import ')) { + // Imports are OK + continue; + } + + if (line.startsWith('export ')) { + // Exports are OK + continue; + } + + fail( + 'on line $lineNumber: unexpected code in ${engineFile.path}. This file ' + 'may only contain comments, imports, and exports. Found:\n' + '$line' + ); + } + }); + + 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; +} From 6d82c90c9d0c49e88a1c281c7f4af113698176fc Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 30 Nov 2021 14:46:36 -0800 Subject: [PATCH 2/6] clean up ignore: implicit_dynamic_function --- lib/web_ui/lib/src/engine/embedder.dart | 7 +- .../lib/src/engine/safe_browser_api.dart | 203 ++++++------------ lib/web_ui/lib/src/engine/util.dart | 3 +- .../test/engine/platform_dispatcher_test.dart | 5 +- .../test/engine/pointer_binding_test.dart | 16 +- .../engine/surface/scene_builder_test.dart | 3 +- .../test/engine/web_experiments_test.dart | 3 +- .../html/canvas_clip_path_golden_test.dart | 3 +- .../compositing/canvas_blend_golden_test.dart | 3 +- .../compositing/color_filter_golden_test.dart | 3 +- .../canvas_draw_image_golden_test.dart | 3 +- .../drawing/draw_vertices_golden_test.dart | 3 +- .../shaders/image_shader_golden_test.dart | 3 +- lib/web_ui/test/keyboard_test.dart | 8 +- lib/web_ui/test/text_editing_test.dart | 10 +- lib/web_ui/test/window_test.dart | 3 +- 16 files changed, 97 insertions(+), 182 deletions(-) diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index 108527590ce57..6a27d910916cc 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -106,9 +106,7 @@ class FlutterViewEmbedder { void _setupHotRestart() { // This persists across hot restarts to clear stale DOM. - _staleHotRestartState = - // ignore: implicit_dynamic_function - getJsProperty(html.window, _staleHotRestartStore) as List?; + _staleHotRestartState = getJsProperty?>(html.window, _staleHotRestartStore); if (_staleHotRestartState == null) { _staleHotRestartState = []; setJsProperty( @@ -354,8 +352,7 @@ class FlutterViewEmbedder { // Creates a [HostNode] into a `root` [html.Element]. HostNode _createHostNode(html.Element root) { - // ignore: implicit_dynamic_function - if (getJsProperty(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/safe_browser_api.dart b/lib/web_ui/lib/src/engine/safe_browser_api.dart index ea589c2a3a1f9..165bbad46799e 100644 --- a/lib/web_ui/lib/src/engine/safe_browser_api.dart +++ b/lib/web_ui/lib/src/engine/safe_browser_api.dart @@ -158,16 +158,13 @@ num? parseFontSize(html.Element element) { if (hasJsProperty(element, 'computedStyleMap')) { // Use the newer `computedStyleMap` API available on some browsers. - final dynamic computedStyleMap = - // ignore: implicit_dynamic_function - js_util.callMethod(element, 'computedStyleMap', []); + final Object? computedStyleMap = + js_util.callMethod(element, 'computedStyleMap', []); if (computedStyleMap is Object) { - final dynamic fontSizeObject = - // ignore: implicit_dynamic_function - js_util.callMethod(computedStyleMap, 'get', ['font-size']); + final Object? fontSizeObject = + js_util.callMethod(computedStyleMap, 'get', ['font-size']); if (fontSizeObject is Object) { - // ignore: implicit_dynamic_function - fontSize = js_util.getProperty(fontSizeObject, 'value') as num; + fontSize = js_util.getProperty(fontSizeObject, 'value'); } } } @@ -185,8 +182,7 @@ num? parseFontSize(html.Element element) { void vibrate(int durationMs) { final html.Navigator navigator = html.window.navigator; if (hasJsProperty(navigator, 'vibrate')) { - // ignore: implicit_dynamic_function - js_util.callMethod(navigator, 'vibrate', [durationMs]); + js_util.callMethod(navigator, 'vibrate', [durationMs]); } } @@ -440,8 +436,7 @@ class GlContext { 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', + js_util.callMethod(context, 'drawImage', [_canvas, 0, 0, _widthInPixels, _heightInPixels, left, top, _widthInPixels, _heightInPixels]); } @@ -471,75 +466,63 @@ class GlContext { 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( + js_util.callMethod(glContext, 'shaderSource', [shader, source]); + js_util.callMethod(glContext, 'compileShader', [shader]); + 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; + js_util.callMethod(glContext, 'createProgram', const []); void attachShader(Object? program, Object shader) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'attachShader', [program, shader]); + 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( + js_util.callMethod(glContext, 'linkProgram', [program]); + 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]); + 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]); + 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', + js_util.callMethod(glContext, 'bindVertexArray', [vertexObjectArray]); } void unbindVertexArray() { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'bindVertexArray', + js_util.callMethod(glContext, 'bindVertexArray', [null]); } void bindElementArrayBuffer(Object? buffer) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'bindBuffer', [kElementArrayBuffer, buffer]); + js_util.callMethod(glContext, 'bindBuffer', [kElementArrayBuffer, buffer]); } Object? createTexture() => @@ -549,105 +532,91 @@ class GlContext { js_util.callMethod(glContext, 'generateMipmap', [target]); void bindTexture(dynamic target, Object? buffer) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'bindTexture', [target, buffer]); + js_util.callMethod(glContext, 'bindTexture', [target, buffer]); } void activeTexture(int textureUnit) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'activeTexture', [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) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'texImage2D', [ + js_util.callMethod(glContext, 'texImage2D', [ target, level, internalFormat, format, dataType, pixels]); } else { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'texImage2D', [ + 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', [ + js_util.callMethod(glContext, 'texParameteri', [ target, parameterName, value]); } void deleteBuffer(Object buffer) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'deleteBuffer', [buffer]); + js_util.callMethod(glContext, 'deleteBuffer', [buffer]); } void bufferData(TypedData? data, dynamic type) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'bufferData', [kArrayBuffer, data, type]); + 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]); + js_util.callMethod(glContext, 'bufferData', [kElementArrayBuffer, data, type]); } void enableVertexAttribArray(dynamic index) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'enableVertexAttribArray', [index]); + js_util.callMethod(glContext, 'enableVertexAttribArray', [index]); } /// Clear background. void clear() { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'clear', [kColorBufferBit]); + 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 [], - ); + final Object? loseContextExtension = _getExtension('WEBGL_lose_context'); + if (loseContextExtension != null) { + js_util.callMethod( + loseContextExtension, + 'loseContext', + const [], + ); + } } void deleteProgram(Object program) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'deleteProgram', [program]); + js_util.callMethod(glContext, 'deleteProgram', [program]); } void deleteShader(Object shader) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'deleteShader', [shader]); + js_util.callMethod(glContext, 'deleteShader', [shader]); } - dynamic _getExtension(String extensionName) => - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'getExtension', [extensionName]); + Object? _getExtension(String extensionName) => + 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]); + 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]); + 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]); + js_util.callMethod(glContext, 'viewport', [x, y, width, height]); } - dynamic _triangleTypeFromMode(ui.VertexMode mode) { + Object _triangleTypeFromMode(ui.VertexMode mode) { switch (mode) { case ui.VertexMode.triangles: return kTriangles; @@ -659,25 +628,20 @@ class GlContext { } Object? _createShader(String shaderType) => js_util.callMethod( - // ignore: implicit_dynamic_function - glContext, 'createShader', [js_util.getProperty(glContext, shaderType)]); + glContext, 'createShader', [js_util.getProperty(glContext, shaderType)]); /// Error state of gl context. - // ignore: implicit_dynamic_function Object? get error => js_util.callMethod(glContext, 'getError', const []); /// Shader compiler error, if this returns [kFalse], to get details use /// [getShaderInfoLog]. Object? get compileStatus => - // ignore: implicit_dynamic_function _kCompileStatus ??= js_util.getProperty(glContext, 'COMPILE_STATUS'); Object? get kArrayBuffer => - // ignore: implicit_dynamic_function _kArrayBuffer ??= js_util.getProperty(glContext, 'ARRAY_BUFFER'); Object? get kElementArrayBuffer => - // ignore: implicit_dynamic_function _kElementArrayBuffer ??= js_util.getProperty(glContext, 'ELEMENT_ARRAY_BUFFER'); @@ -686,70 +650,54 @@ class GlContext { Object get kFloat => _kFloat ??= js_util.getProperty(glContext, 'FLOAT'); - // ignore: implicit_dynamic_function Object? get kRGBA => _kRGBA ??= js_util.getProperty(glContext, 'RGBA'); Object get kUnsignedByte => _kUnsignedByte ??= js_util.getProperty(glContext, 'UNSIGNED_BYTE'); Object? get kUnsignedShort => - // ignore: implicit_dynamic_function _kUnsignedShort ??= js_util.getProperty(glContext, 'UNSIGNED_SHORT'); Object? get kStaticDraw => - // ignore: implicit_dynamic_function _kStaticDraw ??= js_util.getProperty(glContext, 'STATIC_DRAW'); - Object? get kTriangles => - // ignore: implicit_dynamic_function - _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLES'); + Object get kTriangles => + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLES'); - Object? get kTriangleFan => - // ignore: implicit_dynamic_function - _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_FAN'); + Object get kTriangleFan => + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_FAN'); - Object? get kTriangleStrip => - // ignore: implicit_dynamic_function - _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_STRIP'); + Object get kTriangleStrip => + _kTriangles ??= js_util.getProperty(glContext, 'TRIANGLE_STRIP'); Object? get kColorBufferBit => - // ignore: implicit_dynamic_function _kColorBufferBit ??= js_util.getProperty(glContext, 'COLOR_BUFFER_BIT'); Object? 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; + _kTexture0 ??= js_util.getProperty(glContext, 'TEXTURE0'); Object? get kTextureWrapS => - // ignore: implicit_dynamic_function _kTextureWrapS ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_S'); Object? get kTextureWrapT => - // ignore: implicit_dynamic_function _kTextureWrapT ??= js_util.getProperty(glContext, 'TEXTURE_WRAP_T'); Object? get kRepeat => - // ignore: implicit_dynamic_function _kRepeat ??= js_util.getProperty(glContext, 'REPEAT'); Object? get kClampToEdge => - // ignore: implicit_dynamic_function _kClampToEdge ??= js_util.getProperty(glContext, 'CLAMP_TO_EDGE'); Object? get kMirroredRepeat => - // ignore: implicit_dynamic_function _kMirroredRepeat ??= js_util.getProperty(glContext, 'MIRRORED_REPEAT'); Object? get kLinear => - // ignore: implicit_dynamic_function _kLinear ??= js_util.getProperty(glContext, 'LINEAR'); Object? get kTextureMinFilter => - // ignore: implicit_dynamic_function _kTextureMinFilter ??= js_util.getProperty(glContext, 'TEXTURE_MIN_FILTER'); @@ -784,50 +732,42 @@ class GlContext { /// Sets float uniform value. void setUniform1f(Object uniform, double value) { - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'uniform1f', [uniform, value]); + 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]); + 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( + 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( + js_util.callMethod( glContext, 'uniformMatrix4fv', [uniform, transpose, value]); } /// Shader compile error log. Object? 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?; + return js_util.callMethod(glContext, 'getProgramInfoLog', [glProgram]); } int? get drawingBufferWidth => - // ignore: implicit_dynamic_function - js_util.getProperty(glContext, 'drawingBufferWidth') as int?; + js_util.getProperty(glContext, 'drawingBufferWidth'); int? get drawingBufferHeight => - // ignore: implicit_dynamic_function - js_util.getProperty(glContext, 'drawingBufferWidth') as int?; + js_util.getProperty(glContext, 'drawingBufferWidth'); /// Reads gl contents as image data. /// @@ -840,16 +780,14 @@ class GlContext { browserEngine == BrowserEngine.firefox) { final Uint8List pixels = Uint8List(bufferWidth * bufferHeight * kBytesPerPixel); - // ignore: implicit_dynamic_function - js_util.callMethod(glContext, 'readPixels', + 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', + js_util.callMethod(glContext, 'readPixels', [0, 0, bufferWidth, bufferHeight, kRGBA, kUnsignedByte, pixels]); return html.ImageData(pixels, bufferWidth, bufferHeight); } @@ -863,8 +801,8 @@ class GlContext { // allocation. if (_canvas != null && js_util.hasProperty(_canvas!, 'transferToImageBitmap')) { - // ignore: implicit_dynamic_function - js_util.callMethod(_canvas!, 'getContext', ['webgl2']); + // 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; @@ -1030,8 +968,7 @@ class OffScreenCanvas { 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', + js_util.callMethod(targetContext, 'drawImage', [offScreenCanvas ?? canvasElement!, 0, 0, width, height, 0, 0, width, height]); } @@ -1044,8 +981,7 @@ class OffScreenCanvas { 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, + js_util.getProperty(js_util.getProperty(event, 'target'), 'result'), ); }); fileReader.readAsDataUrl(value); @@ -1058,8 +994,7 @@ class OffScreenCanvas { /// 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( + js_util.callMethod( getContext2d()!, 'drawImage', [image, x, y, width, height]); } diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index fe19c463a4e92..5bc4d6787ed27 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -711,8 +711,7 @@ void drawEllipse( double startAngle, double endAngle, bool antiClockwise) { - // ignore: implicit_dynamic_function - _ellipseFeatureDetected ??= getJsProperty(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/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 index d482fb04d9897..b22e42fe0328b 100644 --- a/lib/web_ui/test/engine/web_experiments_test.dart +++ b/lib/web_ui/test/engine/web_experiments_test.dart @@ -22,8 +22,7 @@ void testMain() { } void jsUpdateExperiment(dynamic name, dynamic enabled) { - // ignore: implicit_dynamic_function - js_util.callMethod( + 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/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], From d9741590c4e8cee0473ecd907dafc6a788e2bf5b Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 30 Nov 2021 14:56:39 -0800 Subject: [PATCH 3/6] comments in js_access_test.dart --- web_sdk/test/js_access_test.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web_sdk/test/js_access_test.dart b/web_sdk/test/js_access_test.dart index 736521eda6484..828bd8c021e24 100644 --- a/web_sdk/test/js_access_test.dart +++ b/web_sdk/test/js_access_test.dart @@ -21,6 +21,9 @@ const List _jsAccessLibraries = [ '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', From d70d796d01fbe355d1fa03ea045cfaf503b2c5bc Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Wed, 1 Dec 2021 12:24:03 -0800 Subject: [PATCH 4/6] addres comments; update licenses_flutter --- ci/analyze.sh | 2 +- ci/licenses_golden/licenses_flutter | 5 ++-- .../engine/canvaskit/image_web_codecs.dart | 1 - lib/web_ui/lib/src/engine/profiler.dart | 14 +++++---- .../lib/src/engine/safe_browser_api.dart | 17 +++++++++++ .../test/engine/web_experiments_test.dart | 30 ------------------- 6 files changed, 29 insertions(+), 40 deletions(-) delete mode 100644 lib/web_ui/test/engine/web_experiments_test.dart diff --git a/ci/analyze.sh b/ci/analyze.sh index 5212f16cf8ac4..e5e5262071934 100755 --- a/ci/analyze.sh +++ b/ci/analyze.sh @@ -80,4 +80,4 @@ echo "" echo "Checking web_ui api conformance..." (cd "$FLUTTER_DIR/web_sdk"; "$DART" pub get) (cd "$FLUTTER_DIR"; "$DART" "web_sdk/test/api_conform_test.dart") -(cd "$FLUTTER_DIR"; "$DART" "web_sdk/test/js_access_test.dart") +(cd "$FLUTTER_DIR"; "$DART" --enable-asserts "web_sdk/test/js_access_test.dart") 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/canvaskit/image_web_codecs.dart b/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart index 3a4e1136ebdfa..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,7 +7,6 @@ /// See also: /// /// * `image_wasm_codecs.dart`, which uses codecs supplied by the CanvasKit WASM bundle. -library image_web_codecs; import 'dart:async'; import 'dart:html' as html; diff --git a/lib/web_ui/lib/src/engine/profiler.dart b/lib/web_ui/lib/src/engine/profiler.dart index 3d24df24d9eea..369c47489e570 100644 --- a/lib/web_ui/lib/src/engine/profiler.dart +++ b/lib/web_ui/lib/src/engine/profiler.dart @@ -10,9 +10,6 @@ import 'package:ui/ui.dart' as ui; import 'platform_dispatcher.dart'; import 'safe_browser_api.dart'; -/// A function that receives a benchmark [value] labeleb by [name]. -typedef OnBenchmark = void Function(String name, double value); - /// A function that computes a value of type [R]. /// /// Functions of this signature can be passed to [timeAction] for performance @@ -105,8 +102,15 @@ class Profiler { void benchmark(String name, double value) { _checkBenchmarkMode(); - final OnBenchmark? onBenchmark = - getJsProperty(html.window, '_flutter_internal_on_benchmark'); + // 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 index 165bbad46799e..afb93d86382a1 100644 --- a/lib/web_ui/lib/src/engine/safe_browser_api.dart +++ b/lib/web_ui/lib/src/engine/safe_browser_api.dart @@ -80,6 +80,23 @@ 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); + +void callFlutterInternalOnBenchmark(String name, double value) { + // BEFORE: + // final OnBenchmark? onBenchmark = + // // ignore: implicit_dynamic_function + // js_util.getProperty(html.window, '_flutter_internal_on_benchmark') as OnBenchmark?; + + // AFTER: + final OnBenchmark? onBenchmark = + js_util.getProperty(html.window, '_flutter_internal_on_benchmark'); + if (onBenchmark != null) { + onBenchmark(name, value); + } +} + /// Adds an event [listener] of type [type] to the [target]. /// /// [eventOptions] supply additional configuration parameters. 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 b22e42fe0328b..0000000000000 --- a/lib/web_ui/test/engine/web_experiments_test.dart +++ /dev/null @@ -1,30 +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'; - -void main() { - internalBootstrapBrowserTest(() => testMain); -} - -void testMain() { - 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) { - js_util.callMethod( - html.window, - '_flutter_internal_update_experiment', - [name, enabled], - ); -} From 6f0c26e6d26d88c99144b4693b5a44e193a5cf33 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Thu, 2 Dec 2021 10:58:30 -0800 Subject: [PATCH 5/6] make engine.dart exrpots-only --- lib/web_ui/lib/src/engine.dart | 67 ++--------------- .../lib/src/engine/safe_browser_api.dart | 14 ---- web_sdk/sdk_rewriter.dart | 74 +++++++++++++++---- web_sdk/test/js_access_test.dart | 47 ------------ 4 files changed, 67 insertions(+), 135 deletions(-) diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 7d6312a938cdd..f2128eac51b14 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -2,69 +2,18 @@ // 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 +// +// - https://github.com/flutter/engine/blob/main/web_sdk/sdk_rewriter.dart -// ignore: unused_import -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; -// ignore: unused_import -import 'dart:developer' as developer; -// ignore: unused_import -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; -// ignore: unused_import -import 'dart:typed_data'; -// ignore: unused_import -import 'package:js/js.dart'; -// ignore: unused_import -import 'package:meta/meta.dart'; -// ignore: unused_import -import '../ui.dart' as ui; -// ignore: unused_import -import 'engine/configuration.dart'; -// ignore: unused_import -import 'engine/embedder.dart'; -// ignore: unused_import -import 'engine/initialization.dart'; -// ignore: unused_import -import 'engine/keyboard.dart'; -// ignore: unused_import -import 'engine/mouse_cursor.dart'; -// ignore: unused_import -import 'engine/navigation/js_url_strategy.dart'; -// ignore: unused_import -import 'engine/navigation/url_strategy.dart'; -// ignore: unused_import -import 'engine/platform_dispatcher.dart'; -// ignore: unused_import -import 'engine/platform_views/content_manager.dart'; -// ignore: unused_import -import 'engine/profiler.dart'; -// ignore: unused_import -import 'engine/safe_browser_api.dart'; -// ignore: unused_import -import 'engine/window.dart'; +library engine; export 'engine/alarm_clock.dart'; export 'engine/assets.dart'; diff --git a/lib/web_ui/lib/src/engine/safe_browser_api.dart b/lib/web_ui/lib/src/engine/safe_browser_api.dart index afb93d86382a1..4e5b17ff077a8 100644 --- a/lib/web_ui/lib/src/engine/safe_browser_api.dart +++ b/lib/web_ui/lib/src/engine/safe_browser_api.dart @@ -83,20 +83,6 @@ Future promiseToFuture(Object jsPromise) { /// A function that receives a benchmark [value] labeleb by [name]. typedef OnBenchmark = void Function(String name, double value); -void callFlutterInternalOnBenchmark(String name, double value) { - // BEFORE: - // final OnBenchmark? onBenchmark = - // // ignore: implicit_dynamic_function - // js_util.getProperty(html.window, '_flutter_internal_on_benchmark') as OnBenchmark?; - - // AFTER: - final OnBenchmark? onBenchmark = - js_util.getProperty(html.window, '_flutter_internal_on_benchmark'); - if (onBenchmark != null) { - onBenchmark(name, value); - } -} - /// Adds an event [listener] of type [type] to the [target]. /// /// [eventOptions] supply additional configuration parameters. diff --git a/web_sdk/sdk_rewriter.dart b/web_sdk/sdk_rewriter.dart index eef534363113b..de667110145ef 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/js_access_test.dart b/web_sdk/test/js_access_test.dart index 828bd8c021e24..81ab8c567f95f 100644 --- a/web_sdk/test/js_access_test.dart +++ b/web_sdk/test/js_access_test.dart @@ -27,7 +27,6 @@ const List _jsAccessLibraries = [ const List _auditedLibraries = [ 'lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart', 'lib/web_ui/lib/src/engine/safe_browser_api.dart', - 'lib/web_ui/lib/src/engine.dart', ]; Future main(List args) async { @@ -126,52 +125,6 @@ import 'package:ui/ui.dart' as ui; } }); - test('Check engine.dart', () async { - const List expectedLines = [ - '@JS()', - 'library engine;', - ]; - - final File engineFile = File('lib/web_ui/lib/src/engine.dart'); - final String engineDartCode = await engineFile.readAsString(); - 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('import ')) { - // Imports are OK - continue; - } - - if (line.startsWith('export ')) { - // Exports are OK - continue; - } - - fail( - 'on line $lineNumber: unexpected code in ${engineFile.path}. This file ' - 'may only contain comments, imports, and exports. Found:\n' - '$line' - ); - } - }); - test('Check JavaScript access', () async { final Directory webUiLibDir = Directory('lib/web_ui/lib'); final List dartFiles = webUiLibDir From 94d137ec7fa7e68f3990bc0eaabcba893667816b Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Thu, 2 Dec 2021 16:25:48 -0800 Subject: [PATCH 6/6] fix sdk_rewriter_test.dart and ci/analyze.sh --- ci/analyze.sh | 10 +++-- web_sdk/sdk_rewriter.dart | 2 +- web_sdk/test/api_conform_test.dart | 9 ++-- web_sdk/test/sdk_rewriter_test.dart | 65 ++++++++++++++++++++++------- 4 files changed, 62 insertions(+), 24 deletions(-) diff --git a/ci/analyze.sh b/ci/analyze.sh index e5e5262071934..017ca38fd265a 100755 --- a/ci/analyze.sh +++ b/ci/analyze.sh @@ -77,7 +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") -(cd "$FLUTTER_DIR"; "$DART" --enable-asserts "web_sdk/test/js_access_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/web_sdk/sdk_rewriter.dart b/web_sdk/sdk_rewriter.dart index de667110145ef..f7dfbf8d5e923 100644 --- a/web_sdk/sdk_rewriter.dart +++ b/web_sdk/sdk_rewriter.dart @@ -154,7 +154,7 @@ void _validateEngineSource(String engineDartPath, String engineDartCode) { continue; } - if (line.startsWith('export ')) { + if (line.startsWith('export')) { // Exports are OK continue; } 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/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';